/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.search; import android.app.AppGlobals; import android.app.SearchManager; import android.app.SearchableInfo; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Binder; import android.os.Bundle; import android.os.RemoteException; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; /** * This class maintains the information about all searchable activities. * This is a hidden class. */ public class Searchables { private static final String LOG_TAG = "Searchables"; // static strings used for XML lookups, etc. // TODO how should these be documented for the developer, in a more structured way than // the current long wordy javadoc in SearchManager.java ? private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable"; private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*"; private Context mContext; private HashMap mSearchablesMap = null; private ArrayList mSearchablesList = null; private ArrayList mSearchablesInGlobalSearchList = null; // Contains all installed activities that handle the global search // intent. private List mGlobalSearchActivities; private ComponentName mCurrentGlobalSearchActivity = null; private ComponentName mWebSearchActivity = null; public static String GOOGLE_SEARCH_COMPONENT_NAME = "com.android.googlesearch/.GoogleSearch"; public static String ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME = "com.google.android.providers.enhancedgooglesearch/.Launcher"; // Cache the package manager instance final private IPackageManager mPm; // User for which this Searchables caches information private int mUserId; /** * * @param context Context to use for looking up activities etc. */ public Searchables (Context context, int userId) { mContext = context; mUserId = userId; mPm = AppGlobals.getPackageManager(); } /** * Look up, or construct, based on the activity. * * The activities fall into three cases, based on meta-data found in * the manifest entry: *
    *
  1. The activity itself implements search. This is indicated by the * presence of a "android.app.searchable" meta-data attribute. * The value is a reference to an XML file containing search information.
  2. *
  3. A related activity implements search. This is indicated by the * presence of a "android.app.default_searchable" meta-data attribute. * The value is a string naming the activity implementing search. In this * case the factory will "redirect" and return the searchable data.
  4. *
  5. No searchability data is provided. We return null here and other * code will insert the "default" (e.g. contacts) search. * * TODO: cache the result in the map, and check the map first. * TODO: it might make sense to implement the searchable reference as * an application meta-data entry. This way we don't have to pepper each * and every activity. * TODO: can we skip the constructor step if it's a non-searchable? * TODO: does it make sense to plug the default into a slot here for * automatic return? Probably not, but it's one way to do it. * * @param activity The name of the current activity, or null if the * activity does not define any explicit searchable metadata. */ public SearchableInfo getSearchableInfo(ComponentName activity) { // Step 1. Is the result already hashed? (case 1) SearchableInfo result; synchronized (this) { result = mSearchablesMap.get(activity); if (result != null) return result; } // Step 2. See if the current activity references a searchable. // Note: Conceptually, this could be a while(true) loop, but there's // no point in implementing reference chaining here and risking a loop. // References must point directly to searchable activities. ActivityInfo ai = null; try { ai = mPm.getActivityInfo(activity, PackageManager.GET_META_DATA, mUserId); } catch (RemoteException re) { Log.e(LOG_TAG, "Error getting activity info " + re); return null; } String refActivityName = null; // First look for activity-specific reference Bundle md = ai.metaData; if (md != null) { refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE); } // If not found, try for app-wide reference if (refActivityName == null) { md = ai.applicationInfo.metaData; if (md != null) { refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE); } } // Irrespective of source, if a reference was found, follow it. if (refActivityName != null) { // This value is deprecated, return null if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) { return null; } String pkg = activity.getPackageName(); ComponentName referredActivity; if (refActivityName.charAt(0) == '.') { referredActivity = new ComponentName(pkg, pkg + refActivityName); } else { referredActivity = new ComponentName(pkg, refActivityName); } // Now try the referred activity, and if found, cache // it against the original name so we can skip the check synchronized (this) { result = mSearchablesMap.get(referredActivity); if (result != null) { mSearchablesMap.put(activity, result); return result; } } } // Step 3. None found. Return null. return null; } /** * Builds an entire list (suitable for display) of * activities that are searchable, by iterating the entire set of * ACTION_SEARCH & ACTION_WEB_SEARCH intents. * * Also clears the hash of all activities -> searches which will * refill as the user clicks "search". * * This should only be done at startup and again if we know that the * list has changed. * * TODO: every activity that provides a ACTION_SEARCH intent should * also provide searchability meta-data. There are a bunch of checks here * that, if data is not found, silently skip to the next activity. This * won't help a developer trying to figure out why their activity isn't * showing up in the list, but an exception here is too rough. I would * like to find a better notification mechanism. * * TODO: sort the list somehow? UI choice. */ public void updateSearchableList() { // These will become the new values at the end of the method HashMap newSearchablesMap = new HashMap(); ArrayList newSearchablesList = new ArrayList(); ArrayList newSearchablesInGlobalSearchList = new ArrayList(); // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers. List searchList; final Intent intent = new Intent(Intent.ACTION_SEARCH); long ident = Binder.clearCallingIdentity(); try { searchList = queryIntentActivities(intent, PackageManager.GET_META_DATA | PackageManager.MATCH_DEBUG_TRIAGED_MISSING); List webSearchInfoList; final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH); webSearchInfoList = queryIntentActivities(webSearchIntent, PackageManager.GET_META_DATA | PackageManager.MATCH_DEBUG_TRIAGED_MISSING); // analyze each one, generate a Searchables record, and record if (searchList != null || webSearchInfoList != null) { int search_count = (searchList == null ? 0 : searchList.size()); int web_search_count = (webSearchInfoList == null ? 0 : webSearchInfoList.size()); int count = search_count + web_search_count; for (int ii = 0; ii < count; ii++) { // for each component, try to find metadata ResolveInfo info = (ii < search_count) ? searchList.get(ii) : webSearchInfoList.get(ii - search_count); ActivityInfo ai = info.activityInfo; // Check first to avoid duplicate entries. if (newSearchablesMap.get(new ComponentName(ai.packageName, ai.name)) == null) { SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai, mUserId); if (searchable != null) { newSearchablesList.add(searchable); newSearchablesMap.put(searchable.getSearchActivity(), searchable); if (searchable.shouldIncludeInGlobalSearch()) { newSearchablesInGlobalSearchList.add(searchable); } } } } } List newGlobalSearchActivities = findGlobalSearchActivities(); // Find the global search activity ComponentName newGlobalSearchActivity = findGlobalSearchActivity( newGlobalSearchActivities); // Find the web search activity ComponentName newWebSearchActivity = findWebSearchActivity(newGlobalSearchActivity); // Store a consistent set of new values synchronized (this) { mSearchablesMap = newSearchablesMap; mSearchablesList = newSearchablesList; mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList; mGlobalSearchActivities = newGlobalSearchActivities; mCurrentGlobalSearchActivity = newGlobalSearchActivity; mWebSearchActivity = newWebSearchActivity; } } finally { Binder.restoreCallingIdentity(ident); } } /** * Returns a sorted list of installed search providers as per * the following heuristics: * * (a) System apps are given priority over non system apps. * (b) Among system apps and non system apps, the relative ordering * is defined by their declared priority. */ private List findGlobalSearchActivities() { // Step 1 : Query the package manager for a list // of activities that can handle the GLOBAL_SEARCH intent. Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); List activities = queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_DEBUG_TRIAGED_MISSING); if (activities != null && !activities.isEmpty()) { // Step 2: Rank matching activities according to our heuristics. Collections.sort(activities, GLOBAL_SEARCH_RANKER); } return activities; } /** * Finds the global search activity. */ private ComponentName findGlobalSearchActivity(List installed) { // Fetch the global search provider from the system settings, // and if it's still installed, return it. final String searchProviderSetting = getGlobalSearchProviderSetting(); if (!TextUtils.isEmpty(searchProviderSetting)) { final ComponentName globalSearchComponent = ComponentName.unflattenFromString( searchProviderSetting); if (globalSearchComponent != null && isInstalled(globalSearchComponent)) { return globalSearchComponent; } } return getDefaultGlobalSearchProvider(installed); } /** * Checks whether the global search provider with a given * component name is installed on the system or not. This deals with * cases such as the removal of an installed provider. */ private boolean isInstalled(ComponentName globalSearch) { Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); intent.setComponent(globalSearch); List activities = queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); if (activities != null && !activities.isEmpty()) { return true; } return false; } private static final Comparator GLOBAL_SEARCH_RANKER = new Comparator() { @Override public int compare(ResolveInfo lhs, ResolveInfo rhs) { if (lhs == rhs) { return 0; } boolean lhsSystem = isSystemApp(lhs); boolean rhsSystem = isSystemApp(rhs); if (lhsSystem && !rhsSystem) { return -1; } else if (rhsSystem && !lhsSystem) { return 1; } else { // Either both system engines, or both non system // engines. // // Note, this isn't a typo. Higher priority numbers imply // higher priority, but are "lower" in the sort order. return rhs.priority - lhs.priority; } } }; /** * @return true iff. the resolve info corresponds to a system application. */ private static final boolean isSystemApp(ResolveInfo res) { return (res.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; } /** * Returns the highest ranked search provider as per the * ranking defined in {@link #getGlobalSearchActivities()}. */ private ComponentName getDefaultGlobalSearchProvider(List providerList) { if (providerList != null && !providerList.isEmpty()) { ActivityInfo ai = providerList.get(0).activityInfo; return new ComponentName(ai.packageName, ai.name); } Log.w(LOG_TAG, "No global search activity found"); return null; } private String getGlobalSearchProviderSetting() { return Settings.Secure.getString(mContext.getContentResolver(), Settings.Secure.SEARCH_GLOBAL_SEARCH_ACTIVITY); } /** * Finds the web search activity. * * Only looks in the package of the global search activity. */ private ComponentName findWebSearchActivity(ComponentName globalSearchActivity) { if (globalSearchActivity == null) { return null; } Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); intent.setPackage(globalSearchActivity.getPackageName()); List activities = queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); if (activities != null && !activities.isEmpty()) { ActivityInfo ai = activities.get(0).activityInfo; // TODO: do some sanity checks here? return new ComponentName(ai.packageName, ai.name); } Log.w(LOG_TAG, "No web search activity found"); return null; } private List queryIntentActivities(Intent intent, int flags) { List activities = null; try { activities = mPm.queryIntentActivities(intent, intent.resolveTypeIfNeeded(mContext.getContentResolver()), flags, mUserId).getList(); } catch (RemoteException re) { // Local call } return activities; } /** * Returns the list of searchable activities. */ public synchronized ArrayList getSearchablesList() { ArrayList result = new ArrayList(mSearchablesList); return result; } /** * Returns a list of the searchable activities that can be included in global search. */ public synchronized ArrayList getSearchablesInGlobalSearchList() { return new ArrayList(mSearchablesInGlobalSearchList); } /** * Returns a list of activities that handle the global search intent. */ public synchronized ArrayList getGlobalSearchActivities() { return new ArrayList(mGlobalSearchActivities); } /** * Gets the name of the global search activity. */ public synchronized ComponentName getGlobalSearchActivity() { return mCurrentGlobalSearchActivity; } /** * Gets the name of the web search activity. */ public synchronized ComponentName getWebSearchActivity() { return mWebSearchActivity; } void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("Searchable authorities:"); synchronized (this) { if (mSearchablesList != null) { for (SearchableInfo info: mSearchablesList) { pw.print(" "); pw.println(info.getSuggestAuthority()); } } } } }