Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 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 android.server.search; |
| 18 | |
Bjorn Bringert | 74708bb | 2009-04-28 11:26:52 +0100 | [diff] [blame] | 19 | import android.app.SearchManager; |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 20 | import android.content.ComponentName; |
| 21 | import android.content.Context; |
| 22 | import android.content.Intent; |
| 23 | import android.content.pm.ActivityInfo; |
| 24 | import android.content.pm.PackageManager; |
| 25 | import android.content.pm.ResolveInfo; |
| 26 | import android.os.Bundle; |
| 27 | |
| 28 | import java.util.ArrayList; |
| 29 | import java.util.HashMap; |
| 30 | import java.util.List; |
| 31 | |
| 32 | /** |
| 33 | * This class maintains the information about all searchable activities. |
| 34 | */ |
| 35 | public 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; |
Bjorn Bringert | 6d72e02 | 2009-04-29 14:56:12 +0100 | [diff] [blame] | 47 | private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null; |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 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 | /** |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 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 | |
Bjorn Bringert | 74708bb | 2009-04-28 11:26:52 +0100 | [diff] [blame] | 188 | // These will become the new values at the end of the method |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 189 | HashMap<ComponentName, SearchableInfo> newSearchablesMap |
| 190 | = new HashMap<ComponentName, SearchableInfo>(); |
| 191 | ArrayList<SearchableInfo> newSearchablesList |
| 192 | = new ArrayList<SearchableInfo>(); |
Bjorn Bringert | 6d72e02 | 2009-04-29 14:56:12 +0100 | [diff] [blame] | 193 | ArrayList<SearchableInfo> newSearchablesInGlobalSearchList |
| 194 | = new ArrayList<SearchableInfo>(); |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 195 | |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 196 | final PackageManager pm = mContext.getPackageManager(); |
Bjorn Bringert | 74708bb | 2009-04-28 11:26:52 +0100 | [diff] [blame] | 197 | |
| 198 | // use intent resolver to generate list of ACTION_SEARCH receivers |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 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); |
Bjorn Bringert | a920413 | 2009-05-05 14:06:35 +0100 | [diff] [blame] | 213 | newSearchablesMap.put(searchable.getSearchActivity(), searchable); |
Bjorn Bringert | 6d72e02 | 2009-04-29 14:56:12 +0100 | [diff] [blame] | 214 | if (searchable.shouldIncludeInGlobalSearch()) { |
| 215 | newSearchablesInGlobalSearchList.add(searchable); |
| 216 | } |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 217 | } |
| 218 | } |
| 219 | } |
| 220 | |
Bjorn Bringert | 74708bb | 2009-04-28 11:26:52 +0100 | [diff] [blame] | 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 |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 227 | synchronized (this) { |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 228 | mSearchablesMap = newSearchablesMap; |
Bjorn Bringert | 6d72e02 | 2009-04-29 14:56:12 +0100 | [diff] [blame] | 229 | mSearchablesList = newSearchablesList; |
| 230 | mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList; |
Bjorn Bringert | 74708bb | 2009-04-28 11:26:52 +0100 | [diff] [blame] | 231 | mDefaultSearchable = newDefaultSearchable; |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 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 | } |
Bjorn Bringert | 6d72e02 | 2009-04-29 14:56:12 +0100 | [diff] [blame] | 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 | } |
Karl Rosaen | 875d50a | 2009-04-23 19:00:21 -0700 | [diff] [blame] | 249 | } |