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