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.Manifest;
     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.PackageManager;
     27 import android.content.pm.ResolveInfo;
     28 import android.os.Bundle;
     29 import android.util.Log;
     30 
     31 import java.util.ArrayList;
     32 import java.util.HashMap;
     33 import java.util.List;
     34 
     35 /**
     36  * This class maintains the information about all searchable activities.
     37  */
     38 public class Searchables {
     39 
     40     private static final String LOG_TAG = "Searchables";
     41 
     42     // static strings used for XML lookups, etc.
     43     // TODO how should these be documented for the developer, in a more structured way than
     44     // the current long wordy javadoc in SearchManager.java ?
     45     private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable";
     46     private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*";
     47 
     48     private Context mContext;
     49 
     50     private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null;
     51     private ArrayList<SearchableInfo> mSearchablesList = null;
     52     private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null;
     53     private ComponentName mGlobalSearchActivity = null;
     54     private ComponentName mWebSearchActivity = null;
     55 
     56     public static String GOOGLE_SEARCH_COMPONENT_NAME =
     57             "com.android.googlesearch/.GoogleSearch";
     58     public static String ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME =
     59             "com.google.android.providers.enhancedgooglesearch/.Launcher";
     60 
     61     /**
     62      *
     63      * @param context Context to use for looking up activities etc.
     64      */
     65     public Searchables (Context context) {
     66         mContext = context;
     67     }
     68 
     69     /**
     70      * Look up, or construct, based on the activity.
     71      *
     72      * The activities fall into three cases, based on meta-data found in
     73      * the manifest entry:
     74      * <ol>
     75      * <li>The activity itself implements search.  This is indicated by the
     76      * presence of a "android.app.searchable" meta-data attribute.
     77      * The value is a reference to an XML file containing search information.</li>
     78      * <li>A related activity implements search.  This is indicated by the
     79      * presence of a "android.app.default_searchable" meta-data attribute.
     80      * The value is a string naming the activity implementing search.  In this
     81      * case the factory will "redirect" and return the searchable data.</li>
     82      * <li>No searchability data is provided.  We return null here and other
     83      * code will insert the "default" (e.g. contacts) search.
     84      *
     85      * TODO: cache the result in the map, and check the map first.
     86      * TODO: it might make sense to implement the searchable reference as
     87      * an application meta-data entry.  This way we don't have to pepper each
     88      * and every activity.
     89      * TODO: can we skip the constructor step if it's a non-searchable?
     90      * TODO: does it make sense to plug the default into a slot here for
     91      * automatic return?  Probably not, but it's one way to do it.
     92      *
     93      * @param activity The name of the current activity, or null if the
     94      * activity does not define any explicit searchable metadata.
     95      */
     96     public SearchableInfo getSearchableInfo(ComponentName activity) {
     97         // Step 1.  Is the result already hashed?  (case 1)
     98         SearchableInfo result;
     99         synchronized (this) {
    100             result = mSearchablesMap.get(activity);
    101             if (result != null) return result;
    102         }
    103 
    104         // Step 2.  See if the current activity references a searchable.
    105         // Note:  Conceptually, this could be a while(true) loop, but there's
    106         // no point in implementing reference chaining here and risking a loop.
    107         // References must point directly to searchable activities.
    108 
    109         ActivityInfo ai = null;
    110         try {
    111             ai = mContext.getPackageManager().
    112                        getActivityInfo(activity, PackageManager.GET_META_DATA );
    113             String refActivityName = null;
    114 
    115             // First look for activity-specific reference
    116             Bundle md = ai.metaData;
    117             if (md != null) {
    118                 refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
    119             }
    120             // If not found, try for app-wide reference
    121             if (refActivityName == null) {
    122                 md = ai.applicationInfo.metaData;
    123                 if (md != null) {
    124                     refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
    125                 }
    126             }
    127 
    128             // Irrespective of source, if a reference was found, follow it.
    129             if (refActivityName != null)
    130             {
    131                 // This value is deprecated, return null
    132                 if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
    133                     return null;
    134                 }
    135                 String pkg = activity.getPackageName();
    136                 ComponentName referredActivity;
    137                 if (refActivityName.charAt(0) == '.') {
    138                     referredActivity = new ComponentName(pkg, pkg + refActivityName);
    139                 } else {
    140                     referredActivity = new ComponentName(pkg, refActivityName);
    141                 }
    142 
    143                 // Now try the referred activity, and if found, cache
    144                 // it against the original name so we can skip the check
    145                 synchronized (this) {
    146                     result = mSearchablesMap.get(referredActivity);
    147                     if (result != null) {
    148                         mSearchablesMap.put(activity, result);
    149                         return result;
    150                     }
    151                 }
    152             }
    153         } catch (PackageManager.NameNotFoundException e) {
    154             // case 3: no metadata
    155         }
    156 
    157         // Step 3.  None found. Return null.
    158         return null;
    159 
    160     }
    161 
    162     /**
    163      * Builds an entire list (suitable for display) of
    164      * activities that are searchable, by iterating the entire set of
    165      * ACTION_SEARCH & ACTION_WEB_SEARCH intents.
    166      *
    167      * Also clears the hash of all activities -> searches which will
    168      * refill as the user clicks "search".
    169      *
    170      * This should only be done at startup and again if we know that the
    171      * list has changed.
    172      *
    173      * TODO: every activity that provides a ACTION_SEARCH intent should
    174      * also provide searchability meta-data.  There are a bunch of checks here
    175      * that, if data is not found, silently skip to the next activity.  This
    176      * won't help a developer trying to figure out why their activity isn't
    177      * showing up in the list, but an exception here is too rough.  I would
    178      * like to find a better notification mechanism.
    179      *
    180      * TODO: sort the list somehow?  UI choice.
    181      */
    182     public void buildSearchableList() {
    183         // These will become the new values at the end of the method
    184         HashMap<ComponentName, SearchableInfo> newSearchablesMap
    185                                 = new HashMap<ComponentName, SearchableInfo>();
    186         ArrayList<SearchableInfo> newSearchablesList
    187                                 = new ArrayList<SearchableInfo>();
    188         ArrayList<SearchableInfo> newSearchablesInGlobalSearchList
    189                                 = new ArrayList<SearchableInfo>();
    190 
    191         final PackageManager pm = mContext.getPackageManager();
    192 
    193         // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers.
    194         List<ResolveInfo> searchList;
    195         final Intent intent = new Intent(Intent.ACTION_SEARCH);
    196         searchList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
    197 
    198         List<ResolveInfo> webSearchInfoList;
    199         final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH);
    200         webSearchInfoList = pm.queryIntentActivities(webSearchIntent, PackageManager.GET_META_DATA);
    201 
    202         // analyze each one, generate a Searchables record, and record
    203         if (searchList != null || webSearchInfoList != null) {
    204             int search_count = (searchList == null ? 0 : searchList.size());
    205             int web_search_count = (webSearchInfoList == null ? 0 : webSearchInfoList.size());
    206             int count = search_count + web_search_count;
    207             for (int ii = 0; ii < count; ii++) {
    208                 // for each component, try to find metadata
    209                 ResolveInfo info = (ii < search_count)
    210                         ? searchList.get(ii)
    211                         : webSearchInfoList.get(ii - search_count);
    212                 ActivityInfo ai = info.activityInfo;
    213                 // Check first to avoid duplicate entries.
    214                 if (newSearchablesMap.get(new ComponentName(ai.packageName, ai.name)) == null) {
    215                     SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai);
    216                     if (searchable != null) {
    217                         newSearchablesList.add(searchable);
    218                         newSearchablesMap.put(searchable.getSearchActivity(), searchable);
    219                         if (searchable.shouldIncludeInGlobalSearch()) {
    220                             newSearchablesInGlobalSearchList.add(searchable);
    221                         }
    222                     }
    223                 }
    224             }
    225         }
    226 
    227         // Find the global search activity
    228         ComponentName newGlobalSearchActivity = findGlobalSearchActivity();
    229 
    230         // Find the web search activity
    231         ComponentName newWebSearchActivity = findWebSearchActivity(newGlobalSearchActivity);
    232 
    233         // Store a consistent set of new values
    234         synchronized (this) {
    235             mSearchablesMap = newSearchablesMap;
    236             mSearchablesList = newSearchablesList;
    237             mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList;
    238             mGlobalSearchActivity = newGlobalSearchActivity;
    239             mWebSearchActivity = newWebSearchActivity;
    240         }
    241     }
    242 
    243     /**
    244      * Finds the global search activity.
    245      *
    246      * This is currently implemented by returning the first activity that handles
    247      * the GLOBAL_SEARCH intent and has the GLOBAL_SEARCH permission. If we allow
    248      * more than one global search activity to be installed, this code must be changed.
    249      */
    250     private ComponentName findGlobalSearchActivity() {
    251         Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
    252         PackageManager pm = mContext.getPackageManager();
    253         List<ResolveInfo> activities =
    254                 pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    255         int count = activities == null ? 0 : activities.size();
    256         for (int i = 0; i < count; i++) {
    257             ActivityInfo ai = activities.get(i).activityInfo;
    258             if (pm.checkPermission(Manifest.permission.GLOBAL_SEARCH,
    259                     ai.packageName) == PackageManager.PERMISSION_GRANTED) {
    260                 return new ComponentName(ai.packageName, ai.name);
    261             } else {
    262                 Log.w(LOG_TAG, "Package " + ai.packageName + " wants to handle GLOBAL_SEARCH, "
    263                         + "but does not have the GLOBAL_SEARCH permission.");
    264             }
    265         }
    266         Log.w(LOG_TAG, "No global search activity found");
    267         return null;
    268     }
    269 
    270     /**
    271      * Finds the web search activity.
    272      *
    273      * Only looks in the package of the global search activity.
    274      */
    275     private ComponentName findWebSearchActivity(ComponentName globalSearchActivity) {
    276         if (globalSearchActivity == null) {
    277             return null;
    278         }
    279         Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
    280         intent.setPackage(globalSearchActivity.getPackageName());
    281         PackageManager pm = mContext.getPackageManager();
    282         List<ResolveInfo> activities =
    283                 pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    284         int count = activities == null ? 0 : activities.size();
    285         for (int i = 0; i < count; i++) {
    286             ActivityInfo ai = activities.get(i).activityInfo;
    287             // TODO: do some sanity checks here?
    288             return new ComponentName(ai.packageName, ai.name);
    289         }
    290         Log.w(LOG_TAG, "No web search activity found");
    291         return null;
    292     }
    293 
    294     /**
    295      * Returns the list of searchable activities.
    296      */
    297     public synchronized ArrayList<SearchableInfo> getSearchablesList() {
    298         ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList);
    299         return result;
    300     }
    301 
    302     /**
    303      * Returns a list of the searchable activities that can be included in global search.
    304      */
    305     public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() {
    306         return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList);
    307     }
    308 
    309     /**
    310      * Gets the name of the global search activity.
    311      */
    312     public synchronized ComponentName getGlobalSearchActivity() {
    313         return mGlobalSearchActivity;
    314     }
    315 
    316     /**
    317      * Gets the name of the web search activity.
    318      */
    319     public synchronized ComponentName getWebSearchActivity() {
    320         return mWebSearchActivity;
    321     }
    322 }
    323