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