Home | History | Annotate | Download | only in search
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.server.search;
     18 
     19 import android.app.SearchManager;
     20 import android.app.SearchableInfo;
     21 import android.content.ComponentName;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.pm.ActivityInfo;
     25 import android.content.pm.ApplicationInfo;
     26 import android.content.pm.PackageManager;
     27 import android.content.pm.ResolveInfo;
     28 import android.os.Bundle;
     29 import android.provider.Settings;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Collections;
     35 import java.util.Comparator;
     36 import java.util.HashMap;
     37 import java.util.List;
     38 
     39 /**
     40  * This class maintains the information about all searchable activities.
     41  */
     42 public class Searchables {
     43 
     44     private static final String LOG_TAG = "Searchables";
     45 
     46     // static strings used for XML lookups, etc.
     47     // TODO how should these be documented for the developer, in a more structured way than
     48     // the current long wordy javadoc in SearchManager.java ?
     49     private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable";
     50     private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*";
     51 
     52     private Context mContext;
     53 
     54     private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null;
     55     private ArrayList<SearchableInfo> mSearchablesList = null;
     56     private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null;
     57     // Contains all installed activities that handle the global search
     58     // intent.
     59     private List<ResolveInfo> mGlobalSearchActivities;
     60     private ComponentName mCurrentGlobalSearchActivity = null;
     61     private ComponentName mWebSearchActivity = null;
     62 
     63     public static String GOOGLE_SEARCH_COMPONENT_NAME =
     64             "com.android.googlesearch/.GoogleSearch";
     65     public static String ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME =
     66             "com.google.android.providers.enhancedgooglesearch/.Launcher";
     67 
     68     /**
     69      *
     70      * @param context Context to use for looking up activities etc.
     71      */
     72     public Searchables (Context context) {
     73         mContext = context;
     74     }
     75 
     76     /**
     77      * Look up, or construct, based on the activity.
     78      *
     79      * The activities fall into three cases, based on meta-data found in
     80      * the manifest entry:
     81      * <ol>
     82      * <li>The activity itself implements search.  This is indicated by the
     83      * presence of a "android.app.searchable" meta-data attribute.
     84      * The value is a reference to an XML file containing search information.</li>
     85      * <li>A related activity implements search.  This is indicated by the
     86      * presence of a "android.app.default_searchable" meta-data attribute.
     87      * The value is a string naming the activity implementing search.  In this
     88      * case the factory will "redirect" and return the searchable data.</li>
     89      * <li>No searchability data is provided.  We return null here and other
     90      * code will insert the "default" (e.g. contacts) search.
     91      *
     92      * TODO: cache the result in the map, and check the map first.
     93      * TODO: it might make sense to implement the searchable reference as
     94      * an application meta-data entry.  This way we don't have to pepper each
     95      * and every activity.
     96      * TODO: can we skip the constructor step if it's a non-searchable?
     97      * TODO: does it make sense to plug the default into a slot here for
     98      * automatic return?  Probably not, but it's one way to do it.
     99      *
    100      * @param activity The name of the current activity, or null if the
    101      * activity does not define any explicit searchable metadata.
    102      */
    103     public SearchableInfo getSearchableInfo(ComponentName activity) {
    104         // Step 1.  Is the result already hashed?  (case 1)
    105         SearchableInfo result;
    106         synchronized (this) {
    107             result = mSearchablesMap.get(activity);
    108             if (result != null) return result;
    109         }
    110 
    111         // Step 2.  See if the current activity references a searchable.
    112         // Note:  Conceptually, this could be a while(true) loop, but there's
    113         // no point in implementing reference chaining here and risking a loop.
    114         // References must point directly to searchable activities.
    115 
    116         ActivityInfo ai = null;
    117         try {
    118             ai = mContext.getPackageManager().
    119                        getActivityInfo(activity, PackageManager.GET_META_DATA );
    120             String refActivityName = null;
    121 
    122             // First look for activity-specific reference
    123             Bundle md = ai.metaData;
    124             if (md != null) {
    125                 refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
    126             }
    127             // If not found, try for app-wide reference
    128             if (refActivityName == null) {
    129                 md = ai.applicationInfo.metaData;
    130                 if (md != null) {
    131                     refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
    132                 }
    133             }
    134 
    135             // Irrespective of source, if a reference was found, follow it.
    136             if (refActivityName != null)
    137             {
    138                 // This value is deprecated, return null
    139                 if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
    140                     return null;
    141                 }
    142                 String pkg = activity.getPackageName();
    143                 ComponentName referredActivity;
    144                 if (refActivityName.charAt(0) == '.') {
    145                     referredActivity = new ComponentName(pkg, pkg + refActivityName);
    146                 } else {
    147                     referredActivity = new ComponentName(pkg, refActivityName);
    148                 }
    149 
    150                 // Now try the referred activity, and if found, cache
    151                 // it against the original name so we can skip the check
    152                 synchronized (this) {
    153                     result = mSearchablesMap.get(referredActivity);
    154                     if (result != null) {
    155                         mSearchablesMap.put(activity, result);
    156                         return result;
    157                     }
    158                 }
    159             }
    160         } catch (PackageManager.NameNotFoundException e) {
    161             // case 3: no metadata
    162         }
    163 
    164         // Step 3.  None found. Return null.
    165         return null;
    166 
    167     }
    168 
    169     /**
    170      * Builds an entire list (suitable for display) of
    171      * activities that are searchable, by iterating the entire set of
    172      * ACTION_SEARCH & ACTION_WEB_SEARCH intents.
    173      *
    174      * Also clears the hash of all activities -> searches which will
    175      * refill as the user clicks "search".
    176      *
    177      * This should only be done at startup and again if we know that the
    178      * list has changed.
    179      *
    180      * TODO: every activity that provides a ACTION_SEARCH intent should
    181      * also provide searchability meta-data.  There are a bunch of checks here
    182      * that, if data is not found, silently skip to the next activity.  This
    183      * won't help a developer trying to figure out why their activity isn't
    184      * showing up in the list, but an exception here is too rough.  I would
    185      * like to find a better notification mechanism.
    186      *
    187      * TODO: sort the list somehow?  UI choice.
    188      */
    189     public void buildSearchableList() {
    190         // These will become the new values at the end of the method
    191         HashMap<ComponentName, SearchableInfo> newSearchablesMap
    192                                 = new HashMap<ComponentName, SearchableInfo>();
    193         ArrayList<SearchableInfo> newSearchablesList
    194                                 = new ArrayList<SearchableInfo>();
    195         ArrayList<SearchableInfo> newSearchablesInGlobalSearchList
    196                                 = new ArrayList<SearchableInfo>();
    197 
    198         final PackageManager pm = mContext.getPackageManager();
    199 
    200         // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers.
    201         List<ResolveInfo> searchList;
    202         final Intent intent = new Intent(Intent.ACTION_SEARCH);
    203         searchList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
    204 
    205         List<ResolveInfo> webSearchInfoList;
    206         final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH);
    207         webSearchInfoList = pm.queryIntentActivities(webSearchIntent, PackageManager.GET_META_DATA);
    208 
    209         // analyze each one, generate a Searchables record, and record
    210         if (searchList != null || webSearchInfoList != null) {
    211             int search_count = (searchList == null ? 0 : searchList.size());
    212             int web_search_count = (webSearchInfoList == null ? 0 : webSearchInfoList.size());
    213             int count = search_count + web_search_count;
    214             for (int ii = 0; ii < count; ii++) {
    215                 // for each component, try to find metadata
    216                 ResolveInfo info = (ii < search_count)
    217                         ? searchList.get(ii)
    218                         : webSearchInfoList.get(ii - search_count);
    219                 ActivityInfo ai = info.activityInfo;
    220                 // Check first to avoid duplicate entries.
    221                 if (newSearchablesMap.get(new ComponentName(ai.packageName, ai.name)) == null) {
    222                     SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai);
    223                     if (searchable != null) {
    224                         newSearchablesList.add(searchable);
    225                         newSearchablesMap.put(searchable.getSearchActivity(), searchable);
    226                         if (searchable.shouldIncludeInGlobalSearch()) {
    227                             newSearchablesInGlobalSearchList.add(searchable);
    228                         }
    229                     }
    230                 }
    231             }
    232         }
    233 
    234         List<ResolveInfo> newGlobalSearchActivities = findGlobalSearchActivities();
    235 
    236         // Find the global search activity
    237         ComponentName newGlobalSearchActivity = findGlobalSearchActivity(
    238                 newGlobalSearchActivities);
    239 
    240         // Find the web search activity
    241         ComponentName newWebSearchActivity = findWebSearchActivity(newGlobalSearchActivity);
    242 
    243         // Store a consistent set of new values
    244         synchronized (this) {
    245             mSearchablesMap = newSearchablesMap;
    246             mSearchablesList = newSearchablesList;
    247             mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList;
    248             mGlobalSearchActivities = newGlobalSearchActivities;
    249             mCurrentGlobalSearchActivity = newGlobalSearchActivity;
    250             mWebSearchActivity = newWebSearchActivity;
    251         }
    252     }
    253     /**
    254      * Returns a sorted list of installed search providers as per
    255      * the following heuristics:
    256      *
    257      * (a) System apps are given priority over non system apps.
    258      * (b) Among system apps and non system apps, the relative ordering
    259      * is defined by their declared priority.
    260      */
    261     private List<ResolveInfo> findGlobalSearchActivities() {
    262         // Step 1 : Query the package manager for a list
    263         // of activities that can handle the GLOBAL_SEARCH intent.
    264         Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
    265         PackageManager pm = mContext.getPackageManager();
    266         List<ResolveInfo> activities =
    267                 pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    268 
    269         if (activities != null && !activities.isEmpty()) {
    270             // Step 2: Rank matching activities according to our heuristics.
    271             Collections.sort(activities, GLOBAL_SEARCH_RANKER);
    272         }
    273 
    274         return activities;
    275     }
    276 
    277     /**
    278      * Finds the global search activity.
    279      */
    280     private ComponentName findGlobalSearchActivity(List<ResolveInfo> installed) {
    281         // Fetch the global search provider from the system settings,
    282         // and if it's still installed, return it.
    283         final String searchProviderSetting = getGlobalSearchProviderSetting();
    284         if (!TextUtils.isEmpty(searchProviderSetting)) {
    285             final ComponentName globalSearchComponent = ComponentName.unflattenFromString(
    286                     searchProviderSetting);
    287             if (globalSearchComponent != null && isInstalled(globalSearchComponent)) {
    288                 return globalSearchComponent;
    289             }
    290         }
    291 
    292         return getDefaultGlobalSearchProvider(installed);
    293     }
    294 
    295     /**
    296      * Checks whether the global search provider with a given
    297      * component name is installed on the system or not. This deals with
    298      * cases such as the removal of an installed provider.
    299      */
    300     private boolean isInstalled(ComponentName globalSearch) {
    301         Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
    302         intent.setComponent(globalSearch);
    303 
    304         PackageManager pm = mContext.getPackageManager();
    305         List<ResolveInfo> activities =
    306                 pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    307 
    308         if (activities != null && !activities.isEmpty()) {
    309             return true;
    310         }
    311 
    312         return false;
    313     }
    314 
    315     private static final Comparator<ResolveInfo> GLOBAL_SEARCH_RANKER =
    316             new Comparator<ResolveInfo>() {
    317         @Override
    318         public int compare(ResolveInfo lhs, ResolveInfo rhs) {
    319             if (lhs == rhs) {
    320                 return 0;
    321             }
    322             boolean lhsSystem = isSystemApp(lhs);
    323             boolean rhsSystem = isSystemApp(rhs);
    324 
    325             if (lhsSystem && !rhsSystem) {
    326                 return -1;
    327             } else if (rhsSystem && !lhsSystem) {
    328                 return 1;
    329             } else {
    330                 // Either both system engines, or both non system
    331                 // engines.
    332                 //
    333                 // Note, this isn't a typo. Higher priority numbers imply
    334                 // higher priority, but are "lower" in the sort order.
    335                 return rhs.priority - lhs.priority;
    336             }
    337         }
    338     };
    339 
    340     /**
    341      * @return true iff. the resolve info corresponds to a system application.
    342      */
    343     private static final boolean isSystemApp(ResolveInfo res) {
    344         return (res.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
    345     }
    346 
    347     /**
    348      * Returns the highest ranked search provider as per the
    349      * ranking defined in {@link #getGlobalSearchActivities()}.
    350      */
    351     private ComponentName getDefaultGlobalSearchProvider(List<ResolveInfo> providerList) {
    352         if (providerList != null && !providerList.isEmpty()) {
    353             ActivityInfo ai = providerList.get(0).activityInfo;
    354             return new ComponentName(ai.packageName, ai.name);
    355         }
    356 
    357         Log.w(LOG_TAG, "No global search activity found");
    358         return null;
    359     }
    360 
    361     private String getGlobalSearchProviderSetting() {
    362         return Settings.Secure.getString(mContext.getContentResolver(),
    363                 Settings.Secure.SEARCH_GLOBAL_SEARCH_ACTIVITY);
    364     }
    365 
    366     /**
    367      * Finds the web search activity.
    368      *
    369      * Only looks in the package of the global search activity.
    370      */
    371     private ComponentName findWebSearchActivity(ComponentName globalSearchActivity) {
    372         if (globalSearchActivity == null) {
    373             return null;
    374         }
    375         Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
    376         intent.setPackage(globalSearchActivity.getPackageName());
    377         PackageManager pm = mContext.getPackageManager();
    378         List<ResolveInfo> activities =
    379                 pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    380 
    381         if (activities != null && !activities.isEmpty()) {
    382             ActivityInfo ai = activities.get(0).activityInfo;
    383             // TODO: do some sanity checks here?
    384             return new ComponentName(ai.packageName, ai.name);
    385         }
    386         Log.w(LOG_TAG, "No web search activity found");
    387         return null;
    388     }
    389 
    390     /**
    391      * Returns the list of searchable activities.
    392      */
    393     public synchronized ArrayList<SearchableInfo> getSearchablesList() {
    394         ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList);
    395         return result;
    396     }
    397 
    398     /**
    399      * Returns a list of the searchable activities that can be included in global search.
    400      */
    401     public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() {
    402         return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList);
    403     }
    404 
    405     /**
    406      * Returns a list of activities that handle the global search intent.
    407      */
    408     public synchronized ArrayList<ResolveInfo> getGlobalSearchActivities() {
    409         return new ArrayList<ResolveInfo>(mGlobalSearchActivities);
    410     }
    411 
    412     /**
    413      * Gets the name of the global search activity.
    414      */
    415     public synchronized ComponentName getGlobalSearchActivity() {
    416         return mCurrentGlobalSearchActivity;
    417     }
    418 
    419     /**
    420      * Gets the name of the web search activity.
    421      */
    422     public synchronized ComponentName getWebSearchActivity() {
    423         return mWebSearchActivity;
    424     }
    425 }
    426