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