Searchables.java revision 9158825f9c41869689d6b1786d7c7aa8bdd524ce
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
17package com.android.server.search;
18
19import android.app.AppGlobals;
20import android.app.SearchManager;
21import android.app.SearchableInfo;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.ActivityInfo;
26import android.content.pm.ApplicationInfo;
27import android.content.pm.IPackageManager;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.os.Binder;
31import android.os.Bundle;
32import android.os.RemoteException;
33import android.provider.Settings;
34import android.text.TextUtils;
35import android.util.Log;
36
37import java.io.FileDescriptor;
38import java.io.PrintWriter;
39import java.util.ArrayList;
40import java.util.Collections;
41import java.util.Comparator;
42import java.util.HashMap;
43import java.util.List;
44
45/**
46 * This class maintains the information about all searchable activities.
47 * This is a hidden class.
48 */
49public 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