/* * 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 android.server.search; import com.android.internal.app.ResolverActivity; import android.app.SearchManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * This class maintains the information about all searchable activities. */ 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; private ArrayList<SearchableInfo> mSearchablesForWebSearchList = null; private SearchableInfo mDefaultSearchable = null; private SearchableInfo mDefaultSearchableForWebSearch = 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"; /** * * @param context Context to use for looking up activities etc. */ public Searchables (Context context) { mContext = context; } /** * 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 = mContext.getPackageManager(). getActivityInfo(activity, PackageManager.GET_META_DATA ); 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) { // An app or activity can declare that we should simply launch // "system default search" if search is invoked. if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) { return getDefaultSearchable(); } 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; } } } } catch (PackageManager.NameNotFoundException e) { // case 3: no metadata } // Step 3. None found. Return null. return null; } /** * Provides the system-default search activity, which you can use * whenever getSearchableInfo() returns null; * * @return Returns the system-default search activity, null if never defined */ public synchronized SearchableInfo getDefaultSearchable() { return mDefaultSearchable; } public synchronized boolean isDefaultSearchable(SearchableInfo searchable) { return searchable == mDefaultSearchable; } /** * 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>(); ArrayList<SearchableInfo> newSearchablesForWebSearchList = new ArrayList<SearchableInfo>(); final PackageManager pm = mContext.getPackageManager(); // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers. List<ResolveInfo> searchList; final Intent intent = new Intent(Intent.ACTION_SEARCH); searchList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA); List<ResolveInfo> webSearchInfoList; final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH); webSearchInfoList = pm.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); if (searchable != null) { newSearchablesList.add(searchable); newSearchablesMap.put(searchable.getSearchActivity(), searchable); if (searchable.shouldIncludeInGlobalSearch()) { newSearchablesInGlobalSearchList.add(searchable); } } } } } if (webSearchInfoList != null) { for (int i = 0; i < webSearchInfoList.size(); ++i) { ActivityInfo ai = webSearchInfoList.get(i).activityInfo; ComponentName component = new ComponentName(ai.packageName, ai.name); SearchableInfo searchable = newSearchablesMap.get(component); if (searchable == null) { Log.w(LOG_TAG, "did not find component in searchables: " + component); } else { newSearchablesForWebSearchList.add(searchable); } } } // Find the global search provider Intent globalSearchIntent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); ComponentName globalSearchActivity = globalSearchIntent.resolveActivity(pm); SearchableInfo newDefaultSearchable = newSearchablesMap.get(globalSearchActivity); if (newDefaultSearchable == null) { Log.w(LOG_TAG, "No searchable info found for new default searchable activity " + globalSearchActivity); } // Find the default web search provider. ComponentName webSearchActivity = getPreferredWebSearchActivity(mContext); SearchableInfo newDefaultSearchableForWebSearch = null; if (webSearchActivity != null) { newDefaultSearchableForWebSearch = newSearchablesMap.get(webSearchActivity); } if (newDefaultSearchableForWebSearch == null) { Log.w(LOG_TAG, "No searchable info found for new default web search activity " + webSearchActivity); } // Store a consistent set of new values synchronized (this) { mSearchablesMap = newSearchablesMap; mSearchablesList = newSearchablesList; mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList; mSearchablesForWebSearchList = newSearchablesForWebSearchList; mDefaultSearchable = newDefaultSearchable; mDefaultSearchableForWebSearch = newDefaultSearchableForWebSearch; } } /** * Checks if the given activity component is present in the system and if so makes it the * preferred activity for handling ACTION_WEB_SEARCH. * @param component Name of the component to check and set as preferred. * @param action Intent action for which this activity is to be set as preferred. * @return true if component was detected and set as preferred activity, false if not. */ private static boolean setPreferredActivity(Context context, ComponentName component, String action) { Log.d(LOG_TAG, "Checking component " + component); PackageManager pm = context.getPackageManager(); ActivityInfo ai; try { ai = pm.getActivityInfo(component, 0); } catch (PackageManager.NameNotFoundException e) { return false; } // The code here to find the value for bestMatch is heavily inspired by the code // in ResolverActivity where the preferred activity is set. Intent intent = new Intent(action); intent.addCategory(Intent.CATEGORY_DEFAULT); List<ResolveInfo> webSearchActivities = pm.queryIntentActivities(intent, 0); ComponentName set[] = new ComponentName[webSearchActivities.size()]; int bestMatch = 0; for (int i = 0; i < webSearchActivities.size(); ++i) { ResolveInfo ri = webSearchActivities.get(i); set[i] = new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name); if (ri.match > bestMatch) bestMatch = ri.match; } Log.d(LOG_TAG, "Setting preferred web search activity to " + component); IntentFilter filter = new IntentFilter(action); filter.addCategory(Intent.CATEGORY_DEFAULT); pm.replacePreferredActivity(filter, bestMatch, set, component); return true; } private static ComponentName getPreferredWebSearchActivity(Context context) { // Check if we have a preferred web search activity. Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); PackageManager pm = context.getPackageManager(); ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); if (ri == null || ri.activityInfo.name.equals(ResolverActivity.class.getName())) { Log.d(LOG_TAG, "No preferred activity set for action web search."); // The components in the providers array are checked in the order of declaration so the // first one has the highest priority. If the component exists in the system it is set // as the preferred activity to handle intent action web search. String[] preferredActivities = context.getResources().getStringArray( com.android.internal.R.array.default_web_search_providers); for (String componentName : preferredActivities) { ComponentName component = ComponentName.unflattenFromString(componentName); if (setPreferredActivity(context, component, Intent.ACTION_WEB_SEARCH)) { return component; } } } else { // If the current preferred activity is GoogleSearch, and we detect // EnhancedGoogleSearch installed as well, set the latter as preferred since that // is a superset and provides more functionality. ComponentName cn = new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name); if (cn.flattenToShortString().equals(GOOGLE_SEARCH_COMPONENT_NAME)) { ComponentName enhancedGoogleSearch = ComponentName.unflattenFromString( ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME); if (setPreferredActivity(context, enhancedGoogleSearch, Intent.ACTION_WEB_SEARCH)) { return enhancedGoogleSearch; } } } if (ri == null) return null; return new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name); } /** * 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 the searchable activities that handle web searches. */ public synchronized ArrayList<SearchableInfo> getSearchablesForWebSearchList() { return new ArrayList<SearchableInfo>(mSearchablesForWebSearchList); } /** * Returns the default searchable activity for web searches. */ public synchronized SearchableInfo getDefaultSearchableForWebSearch() { return mDefaultSearchableForWebSearch; } /** * Sets the default searchable activity for web searches. */ public synchronized void setDefaultWebSearch(ComponentName component) { setPreferredActivity(mContext, component, Intent.ACTION_WEB_SEARCH); buildSearchableList(); } }