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 buildSearchableList() { 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, PackageManager.GET_META_DATA); 219 220 List<ResolveInfo> webSearchInfoList; 221 final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH); 222 webSearchInfoList = queryIntentActivities(webSearchIntent, PackageManager.GET_META_DATA); 223 224 // analyze each one, generate a Searchables record, and record 225 if (searchList != null || webSearchInfoList != null) { 226 int search_count = (searchList == null ? 0 : searchList.size()); 227 int web_search_count = (webSearchInfoList == null ? 0 : webSearchInfoList.size()); 228 int count = search_count + web_search_count; 229 for (int ii = 0; ii < count; ii++) { 230 // for each component, try to find metadata 231 ResolveInfo info = (ii < search_count) 232 ? searchList.get(ii) 233 : webSearchInfoList.get(ii - search_count); 234 ActivityInfo ai = info.activityInfo; 235 // Check first to avoid duplicate entries. 236 if (newSearchablesMap.get(new ComponentName(ai.packageName, ai.name)) == null) { 237 SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai, 238 mUserId); 239 if (searchable != null) { 240 newSearchablesList.add(searchable); 241 newSearchablesMap.put(searchable.getSearchActivity(), searchable); 242 if (searchable.shouldIncludeInGlobalSearch()) { 243 newSearchablesInGlobalSearchList.add(searchable); 244 } 245 } 246 } 247 } 248 } 249 250 List<ResolveInfo> newGlobalSearchActivities = findGlobalSearchActivities(); 251 252 // Find the global search activity 253 ComponentName newGlobalSearchActivity = findGlobalSearchActivity( 254 newGlobalSearchActivities); 255 256 // Find the web search activity 257 ComponentName newWebSearchActivity = findWebSearchActivity(newGlobalSearchActivity); 258 259 // Store a consistent set of new values 260 synchronized (this) { 261 mSearchablesMap = newSearchablesMap; 262 mSearchablesList = newSearchablesList; 263 mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList; 264 mGlobalSearchActivities = newGlobalSearchActivities; 265 mCurrentGlobalSearchActivity = newGlobalSearchActivity; 266 mWebSearchActivity = newWebSearchActivity; 267 } 268 } finally { 269 Binder.restoreCallingIdentity(ident); 270 } 271 } 272 273 /** 274 * Returns a sorted list of installed search providers as per 275 * the following heuristics: 276 * 277 * (a) System apps are given priority over non system apps. 278 * (b) Among system apps and non system apps, the relative ordering 279 * is defined by their declared priority. 280 */ 281 private List<ResolveInfo> findGlobalSearchActivities() { 282 // Step 1 : Query the package manager for a list 283 // of activities that can handle the GLOBAL_SEARCH intent. 284 Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); 285 List<ResolveInfo> activities = 286 queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 287 if (activities != null && !activities.isEmpty()) { 288 // Step 2: Rank matching activities according to our heuristics. 289 Collections.sort(activities, GLOBAL_SEARCH_RANKER); 290 } 291 292 return activities; 293 } 294 295 /** 296 * Finds the global search activity. 297 */ 298 private ComponentName findGlobalSearchActivity(List<ResolveInfo> installed) { 299 // Fetch the global search provider from the system settings, 300 // and if it's still installed, return it. 301 final String searchProviderSetting = getGlobalSearchProviderSetting(); 302 if (!TextUtils.isEmpty(searchProviderSetting)) { 303 final ComponentName globalSearchComponent = ComponentName.unflattenFromString( 304 searchProviderSetting); 305 if (globalSearchComponent != null && isInstalled(globalSearchComponent)) { 306 return globalSearchComponent; 307 } 308 } 309 310 return getDefaultGlobalSearchProvider(installed); 311 } 312 313 /** 314 * Checks whether the global search provider with a given 315 * component name is installed on the system or not. This deals with 316 * cases such as the removal of an installed provider. 317 */ 318 private boolean isInstalled(ComponentName globalSearch) { 319 Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); 320 intent.setComponent(globalSearch); 321 322 List<ResolveInfo> activities = queryIntentActivities(intent, 323 PackageManager.MATCH_DEFAULT_ONLY); 324 if (activities != null && !activities.isEmpty()) { 325 return true; 326 } 327 328 return false; 329 } 330 331 private static final Comparator<ResolveInfo> GLOBAL_SEARCH_RANKER = 332 new Comparator<ResolveInfo>() { 333 @Override 334 public int compare(ResolveInfo lhs, ResolveInfo rhs) { 335 if (lhs == rhs) { 336 return 0; 337 } 338 boolean lhsSystem = isSystemApp(lhs); 339 boolean rhsSystem = isSystemApp(rhs); 340 341 if (lhsSystem && !rhsSystem) { 342 return -1; 343 } else if (rhsSystem && !lhsSystem) { 344 return 1; 345 } else { 346 // Either both system engines, or both non system 347 // engines. 348 // 349 // Note, this isn't a typo. Higher priority numbers imply 350 // higher priority, but are "lower" in the sort order. 351 return rhs.priority - lhs.priority; 352 } 353 } 354 }; 355 356 /** 357 * @return true iff. the resolve info corresponds to a system application. 358 */ 359 private static final boolean isSystemApp(ResolveInfo res) { 360 return (res.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 361 } 362 363 /** 364 * Returns the highest ranked search provider as per the 365 * ranking defined in {@link #getGlobalSearchActivities()}. 366 */ 367 private ComponentName getDefaultGlobalSearchProvider(List<ResolveInfo> providerList) { 368 if (providerList != null && !providerList.isEmpty()) { 369 ActivityInfo ai = providerList.get(0).activityInfo; 370 return new ComponentName(ai.packageName, ai.name); 371 } 372 373 Log.w(LOG_TAG, "No global search activity found"); 374 return null; 375 } 376 377 private String getGlobalSearchProviderSetting() { 378 return Settings.Secure.getString(mContext.getContentResolver(), 379 Settings.Secure.SEARCH_GLOBAL_SEARCH_ACTIVITY); 380 } 381 382 /** 383 * Finds the web search activity. 384 * 385 * Only looks in the package of the global search activity. 386 */ 387 private ComponentName findWebSearchActivity(ComponentName globalSearchActivity) { 388 if (globalSearchActivity == null) { 389 return null; 390 } 391 Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); 392 intent.setPackage(globalSearchActivity.getPackageName()); 393 List<ResolveInfo> activities = 394 queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 395 396 if (activities != null && !activities.isEmpty()) { 397 ActivityInfo ai = activities.get(0).activityInfo; 398 // TODO: do some sanity checks here? 399 return new ComponentName(ai.packageName, ai.name); 400 } 401 Log.w(LOG_TAG, "No web search activity found"); 402 return null; 403 } 404 405 private List<ResolveInfo> queryIntentActivities(Intent intent, int flags) { 406 List<ResolveInfo> activities = null; 407 try { 408 activities = 409 mPm.queryIntentActivities(intent, 410 intent.resolveTypeIfNeeded(mContext.getContentResolver()), 411 flags, mUserId); 412 } catch (RemoteException re) { 413 // Local call 414 } 415 return activities; 416 } 417 418 /** 419 * Returns the list of searchable activities. 420 */ 421 public synchronized ArrayList<SearchableInfo> getSearchablesList() { 422 ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList); 423 return result; 424 } 425 426 /** 427 * Returns a list of the searchable activities that can be included in global search. 428 */ 429 public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() { 430 return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList); 431 } 432 433 /** 434 * Returns a list of activities that handle the global search intent. 435 */ 436 public synchronized ArrayList<ResolveInfo> getGlobalSearchActivities() { 437 return new ArrayList<ResolveInfo>(mGlobalSearchActivities); 438 } 439 440 /** 441 * Gets the name of the global search activity. 442 */ 443 public synchronized ComponentName getGlobalSearchActivity() { 444 return mCurrentGlobalSearchActivity; 445 } 446 447 /** 448 * Gets the name of the web search activity. 449 */ 450 public synchronized ComponentName getWebSearchActivity() { 451 return mWebSearchActivity; 452 } 453 454 void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 455 pw.println("Searchable authorities:"); 456 synchronized (this) { 457 if (mSearchablesList != null) { 458 for (SearchableInfo info: mSearchablesList) { 459 pw.print(" "); pw.println(info.getSuggestAuthority()); 460 } 461 } 462 } 463 } 464 } 465