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