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