diff options
Diffstat (limited to 'services/java/com/android/server/search/Searchables.java')
-rw-r--r-- | services/java/com/android/server/search/Searchables.java | 464 |
1 files changed, 464 insertions, 0 deletions
diff --git a/services/java/com/android/server/search/Searchables.java b/services/java/com/android/server/search/Searchables.java new file mode 100644 index 0000000..0ffbb7d --- /dev/null +++ b/services/java/com/android/server/search/Searchables.java @@ -0,0 +1,464 @@ +/* + * 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<ComponentName, SearchableInfo> mSearchablesMap = null; + private ArrayList<SearchableInfo> mSearchablesList = null; + private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null; + // Contains all installed activities that handle the global search + // intent. + private List<ResolveInfo> 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: + * <ol> + * <li>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.</li> + * <li>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.</li> + * <li>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 buildSearchableList() { + // These will become the new values at the end of the method + HashMap<ComponentName, SearchableInfo> newSearchablesMap + = new HashMap<ComponentName, SearchableInfo>(); + ArrayList<SearchableInfo> newSearchablesList + = new ArrayList<SearchableInfo>(); + ArrayList<SearchableInfo> newSearchablesInGlobalSearchList + = new ArrayList<SearchableInfo>(); + + // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers. + List<ResolveInfo> searchList; + final Intent intent = new Intent(Intent.ACTION_SEARCH); + + long ident = Binder.clearCallingIdentity(); + try { + searchList = queryIntentActivities(intent, PackageManager.GET_META_DATA); + + List<ResolveInfo> webSearchInfoList; + final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH); + webSearchInfoList = queryIntentActivities(webSearchIntent, PackageManager.GET_META_DATA); + + // 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<ResolveInfo> 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<ResolveInfo> 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<ResolveInfo> activities = + queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + 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<ResolveInfo> 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<ResolveInfo> activities = queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY); + if (activities != null && !activities.isEmpty()) { + return true; + } + + return false; + } + + private static final Comparator<ResolveInfo> GLOBAL_SEARCH_RANKER = + new Comparator<ResolveInfo>() { + @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<ResolveInfo> 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<ResolveInfo> 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<ResolveInfo> queryIntentActivities(Intent intent, int flags) { + List<ResolveInfo> activities = null; + try { + activities = + mPm.queryIntentActivities(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + flags, mUserId); + } catch (RemoteException re) { + // Local call + } + return activities; + } + + /** + * Returns the list of searchable activities. + */ + public synchronized ArrayList<SearchableInfo> getSearchablesList() { + ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList); + return result; + } + + /** + * Returns a list of the searchable activities that can be included in global search. + */ + public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() { + return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList); + } + + /** + * Returns a list of activities that handle the global search intent. + */ + public synchronized ArrayList<ResolveInfo> getGlobalSearchActivities() { + return new ArrayList<ResolveInfo>(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()); + } + } + } + } +} |