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