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