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.app.SearchManager; 20 import android.app.SearchableInfo; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.ActivityInfo; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ResolveInfo; 28 import android.os.Bundle; 29 import android.provider.Settings; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.Comparator; 36 import java.util.HashMap; 37 import java.util.List; 38 39 /** 40 * This class maintains the information about all searchable activities. 41 */ 42 public 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