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 updateSearchableList() {
    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,
    219                     PackageManager.GET_META_DATA | PackageManager.MATCH_DEBUG_TRIAGED_MISSING);
    220 
    221             List<ResolveInfo> webSearchInfoList;
    222             final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH);
    223             webSearchInfoList = queryIntentActivities(webSearchIntent,
    224                     PackageManager.GET_META_DATA | PackageManager.MATCH_DEBUG_TRIAGED_MISSING);
    225 
    226             // analyze each one, generate a Searchables record, and record
    227             if (searchList != null || webSearchInfoList != null) {
    228                 int search_count = (searchList == null ? 0 : searchList.size());
    229                 int web_search_count = (webSearchInfoList == null ? 0 : webSearchInfoList.size());
    230                 int count = search_count + web_search_count;
    231                 for (int ii = 0; ii < count; ii++) {
    232                     // for each component, try to find metadata
    233                     ResolveInfo info = (ii < search_count)
    234                             ? searchList.get(ii)
    235                             : webSearchInfoList.get(ii - search_count);
    236                     ActivityInfo ai = info.activityInfo;
    237                     // Check first to avoid duplicate entries.
    238                     if (newSearchablesMap.get(new ComponentName(ai.packageName, ai.name)) == null) {
    239                         SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai,
    240                                 mUserId);
    241                         if (searchable != null) {
    242                             newSearchablesList.add(searchable);
    243                             newSearchablesMap.put(searchable.getSearchActivity(), searchable);
    244                             if (searchable.shouldIncludeInGlobalSearch()) {
    245                                 newSearchablesInGlobalSearchList.add(searchable);
    246                             }
    247                         }
    248                     }
    249                 }
    250             }
    251 
    252             List<ResolveInfo> newGlobalSearchActivities = findGlobalSearchActivities();
    253 
    254             // Find the global search activity
    255             ComponentName newGlobalSearchActivity = findGlobalSearchActivity(
    256                     newGlobalSearchActivities);
    257 
    258             // Find the web search activity
    259             ComponentName newWebSearchActivity = findWebSearchActivity(newGlobalSearchActivity);
    260 
    261             // Store a consistent set of new values
    262             synchronized (this) {
    263                 mSearchablesMap = newSearchablesMap;
    264                 mSearchablesList = newSearchablesList;
    265                 mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList;
    266                 mGlobalSearchActivities = newGlobalSearchActivities;
    267                 mCurrentGlobalSearchActivity = newGlobalSearchActivity;
    268                 mWebSearchActivity = newWebSearchActivity;
    269             }
    270         } finally {
    271             Binder.restoreCallingIdentity(ident);
    272         }
    273     }
    274 
    275     /**
    276      * Returns a sorted list of installed search providers as per
    277      * the following heuristics:
    278      *
    279      * (a) System apps are given priority over non system apps.
    280      * (b) Among system apps and non system apps, the relative ordering
    281      * is defined by their declared priority.
    282      */
    283     private List<ResolveInfo> findGlobalSearchActivities() {
    284         // Step 1 : Query the package manager for a list
    285         // of activities that can handle the GLOBAL_SEARCH intent.
    286         Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
    287         List<ResolveInfo> activities = queryIntentActivities(intent,
    288                 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_DEBUG_TRIAGED_MISSING);
    289         if (activities != null && !activities.isEmpty()) {
    290             // Step 2: Rank matching activities according to our heuristics.
    291             Collections.sort(activities, GLOBAL_SEARCH_RANKER);
    292         }
    293 
    294         return activities;
    295     }
    296 
    297     /**
    298      * Finds the global search activity.
    299      */
    300     private ComponentName findGlobalSearchActivity(List<ResolveInfo> installed) {
    301         // Fetch the global search provider from the system settings,
    302         // and if it's still installed, return it.
    303         final String searchProviderSetting = getGlobalSearchProviderSetting();
    304         if (!TextUtils.isEmpty(searchProviderSetting)) {
    305             final ComponentName globalSearchComponent = ComponentName.unflattenFromString(
    306                     searchProviderSetting);
    307             if (globalSearchComponent != null && isInstalled(globalSearchComponent)) {
    308                 return globalSearchComponent;
    309             }
    310         }
    311 
    312         return getDefaultGlobalSearchProvider(installed);
    313     }
    314 
    315     /**
    316      * Checks whether the global search provider with a given
    317      * component name is installed on the system or not. This deals with
    318      * cases such as the removal of an installed provider.
    319      */
    320     private boolean isInstalled(ComponentName globalSearch) {
    321         Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
    322         intent.setComponent(globalSearch);
    323 
    324         List<ResolveInfo> activities = queryIntentActivities(intent,
    325                 PackageManager.MATCH_DEFAULT_ONLY);
    326         if (activities != null && !activities.isEmpty()) {
    327             return true;
    328         }
    329 
    330         return false;
    331     }
    332 
    333     private static final Comparator<ResolveInfo> GLOBAL_SEARCH_RANKER =
    334             new Comparator<ResolveInfo>() {
    335         @Override
    336         public int compare(ResolveInfo lhs, ResolveInfo rhs) {
    337             if (lhs == rhs) {
    338                 return 0;
    339             }
    340             boolean lhsSystem = isSystemApp(lhs);
    341             boolean rhsSystem = isSystemApp(rhs);
    342 
    343             if (lhsSystem && !rhsSystem) {
    344                 return -1;
    345             } else if (rhsSystem && !lhsSystem) {
    346                 return 1;
    347             } else {
    348                 // Either both system engines, or both non system
    349                 // engines.
    350                 //
    351                 // Note, this isn't a typo. Higher priority numbers imply
    352                 // higher priority, but are "lower" in the sort order.
    353                 return rhs.priority - lhs.priority;
    354             }
    355         }
    356     };
    357 
    358     /**
    359      * @return true iff. the resolve info corresponds to a system application.
    360      */
    361     private static final boolean isSystemApp(ResolveInfo res) {
    362         return (res.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
    363     }
    364 
    365     /**
    366      * Returns the highest ranked search provider as per the
    367      * ranking defined in {@link #getGlobalSearchActivities()}.
    368      */
    369     private ComponentName getDefaultGlobalSearchProvider(List<ResolveInfo> providerList) {
    370         if (providerList != null && !providerList.isEmpty()) {
    371             ActivityInfo ai = providerList.get(0).activityInfo;
    372             return new ComponentName(ai.packageName, ai.name);
    373         }
    374 
    375         Log.w(LOG_TAG, "No global search activity found");
    376         return null;
    377     }
    378 
    379     private String getGlobalSearchProviderSetting() {
    380         return Settings.Secure.getString(mContext.getContentResolver(),
    381                 Settings.Secure.SEARCH_GLOBAL_SEARCH_ACTIVITY);
    382     }
    383 
    384     /**
    385      * Finds the web search activity.
    386      *
    387      * Only looks in the package of the global search activity.
    388      */
    389     private ComponentName findWebSearchActivity(ComponentName globalSearchActivity) {
    390         if (globalSearchActivity == null) {
    391             return null;
    392         }
    393         Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
    394         intent.setPackage(globalSearchActivity.getPackageName());
    395         List<ResolveInfo> activities =
    396                 queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    397 
    398         if (activities != null && !activities.isEmpty()) {
    399             ActivityInfo ai = activities.get(0).activityInfo;
    400             // TODO: do some sanity checks here?
    401             return new ComponentName(ai.packageName, ai.name);
    402         }
    403         Log.w(LOG_TAG, "No web search activity found");
    404         return null;
    405     }
    406 
    407     private List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
    408         List<ResolveInfo> activities = null;
    409         try {
    410             activities =
    411                     mPm.queryIntentActivities(intent,
    412                     intent.resolveTypeIfNeeded(mContext.getContentResolver()),
    413                     flags, mUserId).getList();
    414         } catch (RemoteException re) {
    415             // Local call
    416         }
    417         return activities;
    418     }
    419 
    420     /**
    421      * Returns the list of searchable activities.
    422      */
    423     public synchronized ArrayList<SearchableInfo> getSearchablesList() {
    424         ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList);
    425         return result;
    426     }
    427 
    428     /**
    429      * Returns a list of the searchable activities that can be included in global search.
    430      */
    431     public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() {
    432         return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList);
    433     }
    434 
    435     /**
    436      * Returns a list of activities that handle the global search intent.
    437      */
    438     public synchronized ArrayList<ResolveInfo> getGlobalSearchActivities() {
    439         return new ArrayList<ResolveInfo>(mGlobalSearchActivities);
    440     }
    441 
    442     /**
    443      * Gets the name of the global search activity.
    444      */
    445     public synchronized ComponentName getGlobalSearchActivity() {
    446         return mCurrentGlobalSearchActivity;
    447     }
    448 
    449     /**
    450      * Gets the name of the web search activity.
    451      */
    452     public synchronized ComponentName getWebSearchActivity() {
    453         return mWebSearchActivity;
    454     }
    455 
    456     void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    457         pw.println("Searchable authorities:");
    458         synchronized (this) {
    459             if (mSearchablesList != null) {
    460                 for (SearchableInfo info: mSearchablesList) {
    461                     pw.print("  "); pw.println(info.getSuggestAuthority());
    462                 }
    463             }
    464         }
    465     }
    466 }
    467