Searchables.java revision 6d72e029cb6e5a9cf26aa3314c3dca83614fc91b
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.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.pm.ActivityInfo;
24import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
26import android.os.Bundle;
27
28import java.util.ArrayList;
29import java.util.HashMap;
30import java.util.List;
31
32/**
33 * This class maintains the information about all searchable activities.
34 */
35public class Searchables {
36
37    // static strings used for XML lookups, etc.
38    // TODO how should these be documented for the developer, in a more structured way than
39    // the current long wordy javadoc in SearchManager.java ?
40    private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable";
41    private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*";
42
43    private Context mContext;
44
45    private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null;
46    private ArrayList<SearchableInfo> mSearchablesList = null;
47    private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null;
48    private SearchableInfo mDefaultSearchable = null;
49
50    /**
51     *
52     * @param context Context to use for looking up activities etc.
53     */
54    public Searchables (Context context) {
55        mContext = context;
56    }
57
58    /**
59     * Look up, or construct, based on the activity.
60     *
61     * The activities fall into three cases, based on meta-data found in
62     * the manifest entry:
63     * <ol>
64     * <li>The activity itself implements search.  This is indicated by the
65     * presence of a "android.app.searchable" meta-data attribute.
66     * The value is a reference to an XML file containing search information.</li>
67     * <li>A related activity implements search.  This is indicated by the
68     * presence of a "android.app.default_searchable" meta-data attribute.
69     * The value is a string naming the activity implementing search.  In this
70     * case the factory will "redirect" and return the searchable data.</li>
71     * <li>No searchability data is provided.  We return null here and other
72     * code will insert the "default" (e.g. contacts) search.
73     *
74     * TODO: cache the result in the map, and check the map first.
75     * TODO: it might make sense to implement the searchable reference as
76     * an application meta-data entry.  This way we don't have to pepper each
77     * and every activity.
78     * TODO: can we skip the constructor step if it's a non-searchable?
79     * TODO: does it make sense to plug the default into a slot here for
80     * automatic return?  Probably not, but it's one way to do it.
81     *
82     * @param activity The name of the current activity, or null if the
83     * activity does not define any explicit searchable metadata.
84     */
85    public SearchableInfo getSearchableInfo(ComponentName activity) {
86        // Step 1.  Is the result already hashed?  (case 1)
87        SearchableInfo result;
88        synchronized (this) {
89            result = mSearchablesMap.get(activity);
90            if (result != null) return result;
91        }
92
93        // Step 2.  See if the current activity references a searchable.
94        // Note:  Conceptually, this could be a while(true) loop, but there's
95        // no point in implementing reference chaining here and risking a loop.
96        // References must point directly to searchable activities.
97
98        ActivityInfo ai = null;
99        try {
100            ai = mContext.getPackageManager().
101                       getActivityInfo(activity, PackageManager.GET_META_DATA );
102            String refActivityName = null;
103
104            // First look for activity-specific reference
105            Bundle md = ai.metaData;
106            if (md != null) {
107                refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
108            }
109            // If not found, try for app-wide reference
110            if (refActivityName == null) {
111                md = ai.applicationInfo.metaData;
112                if (md != null) {
113                    refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
114                }
115            }
116
117            // Irrespective of source, if a reference was found, follow it.
118            if (refActivityName != null)
119            {
120                // An app or activity can declare that we should simply launch
121                // "system default search" if search is invoked.
122                if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
123                    return getDefaultSearchable();
124                }
125                String pkg = activity.getPackageName();
126                ComponentName referredActivity;
127                if (refActivityName.charAt(0) == '.') {
128                    referredActivity = new ComponentName(pkg, pkg + refActivityName);
129                } else {
130                    referredActivity = new ComponentName(pkg, refActivityName);
131                }
132
133                // Now try the referred activity, and if found, cache
134                // it against the original name so we can skip the check
135                synchronized (this) {
136                    result = mSearchablesMap.get(referredActivity);
137                    if (result != null) {
138                        mSearchablesMap.put(activity, result);
139                        return result;
140                    }
141                }
142            }
143        } catch (PackageManager.NameNotFoundException e) {
144            // case 3: no metadata
145        }
146
147        // Step 3.  None found. Return null.
148        return null;
149
150    }
151
152    /**
153     * Provides the system-default search activity, which you can use
154     * whenever getSearchableInfo() returns null;
155     *
156     * @return Returns the system-default search activity, null if never defined
157     */
158    public synchronized SearchableInfo getDefaultSearchable() {
159        return mDefaultSearchable;
160    }
161
162    public synchronized boolean isDefaultSearchable(SearchableInfo searchable) {
163        return searchable == mDefaultSearchable;
164    }
165
166    /**
167     * Builds an entire list (suitable for display) of
168     * activities that are searchable, by iterating the entire set of
169     * ACTION_SEARCH intents.
170     *
171     * Also clears the hash of all activities -> searches which will
172     * refill as the user clicks "search".
173     *
174     * This should only be done at startup and again if we know that the
175     * list has changed.
176     *
177     * TODO: every activity that provides a ACTION_SEARCH intent should
178     * also provide searchability meta-data.  There are a bunch of checks here
179     * that, if data is not found, silently skip to the next activity.  This
180     * won't help a developer trying to figure out why their activity isn't
181     * showing up in the list, but an exception here is too rough.  I would
182     * like to find a better notification mechanism.
183     *
184     * TODO: sort the list somehow?  UI choice.
185     */
186    public void buildSearchableList() {
187
188        // These will become the new values at the end of the method
189        HashMap<ComponentName, SearchableInfo> newSearchablesMap
190                                = new HashMap<ComponentName, SearchableInfo>();
191        ArrayList<SearchableInfo> newSearchablesList
192                                = new ArrayList<SearchableInfo>();
193        ArrayList<SearchableInfo> newSearchablesInGlobalSearchList
194                                = new ArrayList<SearchableInfo>();
195
196        final PackageManager pm = mContext.getPackageManager();
197
198        // use intent resolver to generate list of ACTION_SEARCH receivers
199        List<ResolveInfo> infoList;
200        final Intent intent = new Intent(Intent.ACTION_SEARCH);
201        infoList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
202
203        // analyze each one, generate a Searchables record, and record
204        if (infoList != null) {
205            int count = infoList.size();
206            for (int ii = 0; ii < count; ii++) {
207                // for each component, try to find metadata
208                ResolveInfo info = infoList.get(ii);
209                ActivityInfo ai = info.activityInfo;
210                SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai);
211                if (searchable != null) {
212                    newSearchablesList.add(searchable);
213                    newSearchablesMap.put(searchable.mSearchActivity, searchable);
214                    if (searchable.shouldIncludeInGlobalSearch()) {
215                        newSearchablesInGlobalSearchList.add(searchable);
216                    }
217                }
218            }
219        }
220
221        // Find the global search provider
222        Intent globalSearchIntent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
223        ComponentName globalSearchActivity = globalSearchIntent.resolveActivity(pm);
224        SearchableInfo newDefaultSearchable = newSearchablesMap.get(globalSearchActivity);
225
226        // Store a consistent set of new values
227        synchronized (this) {
228            mSearchablesMap = newSearchablesMap;
229            mSearchablesList = newSearchablesList;
230            mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList;
231            mDefaultSearchable = newDefaultSearchable;
232        }
233    }
234
235    /**
236     * Returns the list of searchable activities.
237     */
238    public synchronized ArrayList<SearchableInfo> getSearchablesList() {
239        ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList);
240        return result;
241    }
242
243    /**
244     * Returns a list of the searchable activities that can be included in global search.
245     */
246    public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() {
247        return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList);
248    }
249}
250