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 19 import android.Manifest; 20 import android.app.SearchManager; 21 import android.app.SearchableInfo; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ActivityInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ResolveInfo; 28 import android.os.Bundle; 29 import android.util.Log; 30 31 import java.util.ArrayList; 32 import java.util.HashMap; 33 import java.util.List; 34 35 /** 36 * This class maintains the information about all searchable activities. 37 */ 38 public class Searchables { 39 40 private static final String LOG_TAG = "Searchables"; 41 42 // static strings used for XML lookups, etc. 43 // TODO how should these be documented for the developer, in a more structured way than 44 // the current long wordy javadoc in SearchManager.java ? 45 private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable"; 46 private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*"; 47 48 private Context mContext; 49 50 private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null; 51 private ArrayList<SearchableInfo> mSearchablesList = null; 52 private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null; 53 private ComponentName mGlobalSearchActivity = null; 54 private ComponentName mWebSearchActivity = null; 55 56 public static String GOOGLE_SEARCH_COMPONENT_NAME = 57 "com.android.googlesearch/.GoogleSearch"; 58 public static String ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME = 59 "com.google.android.providers.enhancedgooglesearch/.Launcher"; 60 61 /** 62 * 63 * @param context Context to use for looking up activities etc. 64 */ 65 public Searchables (Context context) { 66 mContext = context; 67 } 68 69 /** 70 * Look up, or construct, based on the activity. 71 * 72 * The activities fall into three cases, based on meta-data found in 73 * the manifest entry: 74 * <ol> 75 * <li>The activity itself implements search. This is indicated by the 76 * presence of a "android.app.searchable" meta-data attribute. 77 * The value is a reference to an XML file containing search information.</li> 78 * <li>A related activity implements search. This is indicated by the 79 * presence of a "android.app.default_searchable" meta-data attribute. 80 * The value is a string naming the activity implementing search. In this 81 * case the factory will "redirect" and return the searchable data.</li> 82 * <li>No searchability data is provided. We return null here and other 83 * code will insert the "default" (e.g. contacts) search. 84 * 85 * TODO: cache the result in the map, and check the map first. 86 * TODO: it might make sense to implement the searchable reference as 87 * an application meta-data entry. This way we don't have to pepper each 88 * and every activity. 89 * TODO: can we skip the constructor step if it's a non-searchable? 90 * TODO: does it make sense to plug the default into a slot here for 91 * automatic return? Probably not, but it's one way to do it. 92 * 93 * @param activity The name of the current activity, or null if the 94 * activity does not define any explicit searchable metadata. 95 */ 96 public SearchableInfo getSearchableInfo(ComponentName activity) { 97 // Step 1. Is the result already hashed? (case 1) 98 SearchableInfo result; 99 synchronized (this) { 100 result = mSearchablesMap.get(activity); 101 if (result != null) return result; 102 } 103 104 // Step 2. See if the current activity references a searchable. 105 // Note: Conceptually, this could be a while(true) loop, but there's 106 // no point in implementing reference chaining here and risking a loop. 107 // References must point directly to searchable activities. 108 109 ActivityInfo ai = null; 110 try { 111 ai = mContext.getPackageManager(). 112 getActivityInfo(activity, PackageManager.GET_META_DATA ); 113 String refActivityName = null; 114 115 // First look for activity-specific reference 116 Bundle md = ai.metaData; 117 if (md != null) { 118 refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE); 119 } 120 // If not found, try for app-wide reference 121 if (refActivityName == null) { 122 md = ai.applicationInfo.metaData; 123 if (md != null) { 124 refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE); 125 } 126 } 127 128 // Irrespective of source, if a reference was found, follow it. 129 if (refActivityName != null) 130 { 131 // This value is deprecated, return null 132 if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) { 133 return null; 134 } 135 String pkg = activity.getPackageName(); 136 ComponentName referredActivity; 137 if (refActivityName.charAt(0) == '.') { 138 referredActivity = new ComponentName(pkg, pkg + refActivityName); 139 } else { 140 referredActivity = new ComponentName(pkg, refActivityName); 141 } 142 143 // Now try the referred activity, and if found, cache 144 // it against the original name so we can skip the check 145 synchronized (this) { 146 result = mSearchablesMap.get(referredActivity); 147 if (result != null) { 148 mSearchablesMap.put(activity, result); 149 return result; 150 } 151 } 152 } 153 } catch (PackageManager.NameNotFoundException e) { 154 // case 3: no metadata 155 } 156 157 // Step 3. None found. Return null. 158 return null; 159 160 } 161 162 /** 163 * Builds an entire list (suitable for display) of 164 * activities that are searchable, by iterating the entire set of 165 * ACTION_SEARCH & ACTION_WEB_SEARCH intents. 166 * 167 * Also clears the hash of all activities -> searches which will 168 * refill as the user clicks "search". 169 * 170 * This should only be done at startup and again if we know that the 171 * list has changed. 172 * 173 * TODO: every activity that provides a ACTION_SEARCH intent should 174 * also provide searchability meta-data. There are a bunch of checks here 175 * that, if data is not found, silently skip to the next activity. This 176 * won't help a developer trying to figure out why their activity isn't 177 * showing up in the list, but an exception here is too rough. I would 178 * like to find a better notification mechanism. 179 * 180 * TODO: sort the list somehow? UI choice. 181 */ 182 public void buildSearchableList() { 183 // These will become the new values at the end of the method 184 HashMap<ComponentName, SearchableInfo> newSearchablesMap 185 = new HashMap<ComponentName, SearchableInfo>(); 186 ArrayList<SearchableInfo> newSearchablesList 187 = new ArrayList<SearchableInfo>(); 188 ArrayList<SearchableInfo> newSearchablesInGlobalSearchList 189 = new ArrayList<SearchableInfo>(); 190 191 final PackageManager pm = mContext.getPackageManager(); 192 193 // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers. 194 List<ResolveInfo> searchList; 195 final Intent intent = new Intent(Intent.ACTION_SEARCH); 196 searchList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA); 197 198 List<ResolveInfo> webSearchInfoList; 199 final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH); 200 webSearchInfoList = pm.queryIntentActivities(webSearchIntent, PackageManager.GET_META_DATA); 201 202 // analyze each one, generate a Searchables record, and record 203 if (searchList != null || webSearchInfoList != null) { 204 int search_count = (searchList == null ? 0 : searchList.size()); 205 int web_search_count = (webSearchInfoList == null ? 0 : webSearchInfoList.size()); 206 int count = search_count + web_search_count; 207 for (int ii = 0; ii < count; ii++) { 208 // for each component, try to find metadata 209 ResolveInfo info = (ii < search_count) 210 ? searchList.get(ii) 211 : webSearchInfoList.get(ii - search_count); 212 ActivityInfo ai = info.activityInfo; 213 // Check first to avoid duplicate entries. 214 if (newSearchablesMap.get(new ComponentName(ai.packageName, ai.name)) == null) { 215 SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai); 216 if (searchable != null) { 217 newSearchablesList.add(searchable); 218 newSearchablesMap.put(searchable.getSearchActivity(), searchable); 219 if (searchable.shouldIncludeInGlobalSearch()) { 220 newSearchablesInGlobalSearchList.add(searchable); 221 } 222 } 223 } 224 } 225 } 226 227 // Find the global search activity 228 ComponentName newGlobalSearchActivity = findGlobalSearchActivity(); 229 230 // Find the web search activity 231 ComponentName newWebSearchActivity = findWebSearchActivity(newGlobalSearchActivity); 232 233 // Store a consistent set of new values 234 synchronized (this) { 235 mSearchablesMap = newSearchablesMap; 236 mSearchablesList = newSearchablesList; 237 mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList; 238 mGlobalSearchActivity = newGlobalSearchActivity; 239 mWebSearchActivity = newWebSearchActivity; 240 } 241 } 242 243 /** 244 * Finds the global search activity. 245 * 246 * This is currently implemented by returning the first activity that handles 247 * the GLOBAL_SEARCH intent and has the GLOBAL_SEARCH permission. If we allow 248 * more than one global search activity to be installed, this code must be changed. 249 */ 250 private ComponentName findGlobalSearchActivity() { 251 Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); 252 PackageManager pm = mContext.getPackageManager(); 253 List<ResolveInfo> activities = 254 pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 255 int count = activities == null ? 0 : activities.size(); 256 for (int i = 0; i < count; i++) { 257 ActivityInfo ai = activities.get(i).activityInfo; 258 if (pm.checkPermission(Manifest.permission.GLOBAL_SEARCH, 259 ai.packageName) == PackageManager.PERMISSION_GRANTED) { 260 return new ComponentName(ai.packageName, ai.name); 261 } else { 262 Log.w(LOG_TAG, "Package " + ai.packageName + " wants to handle GLOBAL_SEARCH, " 263 + "but does not have the GLOBAL_SEARCH permission."); 264 } 265 } 266 Log.w(LOG_TAG, "No global search activity found"); 267 return null; 268 } 269 270 /** 271 * Finds the web search activity. 272 * 273 * Only looks in the package of the global search activity. 274 */ 275 private ComponentName findWebSearchActivity(ComponentName globalSearchActivity) { 276 if (globalSearchActivity == null) { 277 return null; 278 } 279 Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); 280 intent.setPackage(globalSearchActivity.getPackageName()); 281 PackageManager pm = mContext.getPackageManager(); 282 List<ResolveInfo> activities = 283 pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 284 int count = activities == null ? 0 : activities.size(); 285 for (int i = 0; i < count; i++) { 286 ActivityInfo ai = activities.get(i).activityInfo; 287 // TODO: do some sanity checks here? 288 return new ComponentName(ai.packageName, ai.name); 289 } 290 Log.w(LOG_TAG, "No web search activity found"); 291 return null; 292 } 293 294 /** 295 * Returns the list of searchable activities. 296 */ 297 public synchronized ArrayList<SearchableInfo> getSearchablesList() { 298 ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList); 299 return result; 300 } 301 302 /** 303 * Returns a list of the searchable activities that can be included in global search. 304 */ 305 public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() { 306 return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList); 307 } 308 309 /** 310 * Gets the name of the global search activity. 311 */ 312 public synchronized ComponentName getGlobalSearchActivity() { 313 return mGlobalSearchActivity; 314 } 315 316 /** 317 * Gets the name of the web search activity. 318 */ 319 public synchronized ComponentName getWebSearchActivity() { 320 return mWebSearchActivity; 321 } 322 } 323