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 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