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.PackageManagerInternal;
30import android.content.pm.ResolveInfo;
31import android.os.Binder;
32import android.os.Bundle;
33import android.os.RemoteException;
34import android.os.UserHandle;
35import android.provider.Settings;
36import android.text.TextUtils;
37import android.util.Log;
38
39import com.android.server.LocalServices;
40
41import java.io.FileDescriptor;
42import java.io.PrintWriter;
43import java.util.ArrayList;
44import java.util.Collections;
45import java.util.Comparator;
46import java.util.HashMap;
47import java.util.List;
48
49/**
50 * This class maintains the information about all searchable activities.
51 * This is a hidden class.
52 */
53public 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