Merge "Clean up global search and web search activity finding"

This commit is contained in:
Bjorn Bringert
2010-02-23 06:28:31 -08:00
committed by Android (Google) Code Review
7 changed files with 108 additions and 303 deletions

View File

@@ -24,9 +24,8 @@ import android.os.Bundle;
/** @hide */
interface ISearchManager {
SearchableInfo getSearchableInfo(in ComponentName launchActivity, boolean globalSearch);
SearchableInfo getSearchableInfo(in ComponentName launchActivity);
List<SearchableInfo> getSearchablesInGlobalSearch();
List<SearchableInfo> getSearchablesForWebSearch();
SearchableInfo getDefaultSearchableForWebSearch();
void setDefaultWebSearch(in ComponentName component);
ComponentName getGlobalSearchActivity();
ComponentName getWebSearchActivity();
}

View File

@@ -274,7 +274,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
SearchManager searchManager = (SearchManager)
mContext.getSystemService(Context.SEARCH_SERVICE);
// Try to get the searchable info for the provided component.
mSearchable = searchManager.getSearchableInfo(componentName, false);
mSearchable = searchManager.getSearchableInfo(componentName);
if (mSearchable == null) {
return false;

View File

@@ -137,21 +137,11 @@ import java.util.List;
* setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); // search within your activity
* setDefaultKeyMode(DEFAULT_KEYS_SEARCH_GLOBAL); // search using platform global search</pre>
*
* <p><b>How to enable global search with Quick Search Box.</b> In addition to searching within
* <p><b>How to start global search.</b> In addition to searching within
* your activity or application, you can also use the Search Manager to invoke a platform-global
* search, which uses Quick Search Box to search across the device and the web. There are two ways
* to do this:
* <ul><li>You can simply define "search" within your application or activity to mean global search.
* This is described in more detail in the
* <a href="#SearchabilityMetadata">Searchability Metadata</a> section. Briefly, you will
* add a single meta-data entry to your manifest, declaring that the default search
* for your application is "*". This indicates to the system that no application-specific
* search activity is provided, and that it should launch web-based search instead.</li>
* <li>Simply do nothing and the default implementation of
* {@link android.app.Activity#onSearchRequested} will cause global search to be triggered.
* (You can also always trigger search via a direct call to {@link android.app.Activity#startSearch}.
* This is most useful if you wish to provide local searchability <i>and</i> access to global
* search.)</li></ul>
* search, which uses Quick Search Box to search across the device and the web.
* Override {@link android.app.Activity#onSearchRequested} and call
* {@link android.app.Activity#startSearch} with {@code globalSearch} set to {@code true}.
*
* <p><b>How to disable search from your activity.</b> Search is a system-wide feature and users
* will expect it to be available in all contexts. If your UI design absolutely precludes
@@ -871,12 +861,8 @@ import java.util.List;
*
* <p>The simplest way to specify this is to add a <i>search reference</i> element to the
* application entry in the <a href="{@docRoot}guide/topics/manifest/manifest-intro.html">manifest</a> file.
* The value of this reference can be either of:
* <ul><li>The name of your searchable activity.
* It is typically prefixed by '.' to indicate that it's in the same package.</li>
* <li>A "*" indicates that the system may select a default searchable activity, in which
* case it will typically select web-based search.</li>
* </ul>
* The value of this reference should be the name of your searchable activity.
* It is typically prefixed by '.' to indicate that it's in the same package.
*
* <p>Here is a snippet showing the necessary addition to the manifest entry for your
* non-searchable activities.
@@ -1662,32 +1648,15 @@ public class SearchManager
/**
* Gets the name of the global search activity.
*
* This is currently implemented by returning the first activity that handles
* the GLOBAL_SEARCH intent and has the GLOBAL_SEARCH permission. If we allow
* more than one global search acitivity to be installed, this code must be changed.
*
* TODO: Doing this every time we start global search is inefficient. Will fix that once
* we have settled on the right mechanism for finding the global search activity.
*
* @hide
*/
public ComponentName getGlobalSearchActivity() {
Intent intent = new Intent(INTENT_ACTION_GLOBAL_SEARCH);
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> activities =
pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
int count = activities.size();
for (int i = 0; i < count; i++) {
ActivityInfo ai = activities.get(i).activityInfo;
if (pm.checkPermission(Manifest.permission.GLOBAL_SEARCH,
ai.packageName) == PackageManager.PERMISSION_GRANTED) {
return new ComponentName(ai.packageName, ai.name);
} else {
Log.w(TAG, "Package " + ai.packageName + " wants to handle GLOBAL_SEARCH, "
+ "but does not have the GLOBAL_SEARCH permission.");
}
try {
return mService.getGlobalSearchActivity();
} catch (RemoteException ex) {
Log.e(TAG, "getGlobalSearchActivity() failed: " + ex);
return null;
}
return null;
}
/**
@@ -1700,13 +1669,12 @@ public class SearchManager
* @hide
*/
public ComponentName getWebSearchActivity() {
ComponentName globalSearch = getGlobalSearchActivity();
if (globalSearch == null) {
try {
return mService.getWebSearchActivity();
} catch (RemoteException ex) {
Log.e(TAG, "getWebSearchActivity() failed: " + ex);
return null;
}
Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.setPackage(globalSearch.getPackageName());
return intent.resolveActivity(mContext.getPackageManager());
}
/**
@@ -1840,27 +1808,7 @@ public class SearchManager
*/
public SearchableInfo getSearchableInfo(ComponentName componentName) {
try {
return mService.getSearchableInfo(componentName, false);
} catch (RemoteException ex) {
Log.e(TAG, "getSearchableInfo() failed: " + ex);
return null;
}
}
/**
* Gets information about a searchable activity.
*
* @param componentName The activity to get searchable information for.
* @param globalSearch If <code>false</code>, return information about the given activity.
* If <code>true</code>, return information about the global search activity.
* @return Searchable information, or <code>null</code> if the activity is not searchable.
*
* @hide because SearchableInfo is not part of the API.
*/
public SearchableInfo getSearchableInfo(ComponentName componentName,
boolean globalSearch) {
try {
return mService.getSearchableInfo(componentName, globalSearch);
return mService.getSearchableInfo(componentName);
} catch (RemoteException ex) {
Log.e(TAG, "getSearchableInfo() failed: " + ex);
return null;

View File

@@ -123,24 +123,15 @@ public class SearchManagerService extends ISearchManager.Stub {
* Returns the SearchableInfo for a given activity.
*
* @param launchActivity The activity from which we're launching this search.
* @param globalSearch If false, this will only launch the search that has been specifically
* defined by the application (which is usually defined as a local search). If no default
* search is defined in the current application or activity, no search will be launched.
* If true, this will always launch a platform-global (e.g. web-based) search instead.
* @return Returns a SearchableInfo record describing the parameters of the search,
* or null if no searchable metadata was available.
*/
public SearchableInfo getSearchableInfo(final ComponentName launchActivity,
final boolean globalSearch) {
if (globalSearch) {
return getSearchables().getDefaultSearchable();
} else {
if (launchActivity == null) {
Log.e(TAG, "getSearchableInfo(), activity == null");
return null;
}
return getSearchables().getSearchableInfo(launchActivity);
public SearchableInfo getSearchableInfo(final ComponentName launchActivity) {
if (launchActivity == null) {
Log.e(TAG, "getSearchableInfo(), activity == null");
return null;
}
return getSearchables().getSearchableInfo(launchActivity);
}
/**
@@ -151,27 +142,17 @@ public class SearchManagerService extends ISearchManager.Stub {
}
/**
* Returns a list of the searchable activities that handle web searches.
* Can be called from any thread.
* Gets the name of the global search activity.
*/
public List<SearchableInfo> getSearchablesForWebSearch() {
return getSearchables().getSearchablesForWebSearchList();
public ComponentName getGlobalSearchActivity() {
return getSearchables().getGlobalSearchActivity();
}
/**
* Returns the default searchable activity for web searches.
* Can be called from any thread.
* Gets the name of the web search activity.
*/
public SearchableInfo getDefaultSearchableForWebSearch() {
return getSearchables().getDefaultSearchableForWebSearch();
public ComponentName getWebSearchActivity() {
return getSearchables().getWebSearchActivity();
}
/**
* Sets the default searchable activity for web searches.
* Can be called from any thread.
*/
public void setDefaultWebSearch(final ComponentName component) {
getSearchables().setDefaultWebSearch(component);
broadcastSearchablesChanged();
}
}

View File

@@ -16,14 +16,12 @@
package android.server.search;
import com.android.internal.app.ResolverActivity;
import android.Manifest;
import android.app.SearchManager;
import android.app.SearchableInfo;
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;
@@ -52,9 +50,8 @@ public class Searchables {
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;
private ComponentName mGlobalSearchActivity = null;
private ComponentName mWebSearchActivity = null;
public static String GOOGLE_SEARCH_COMPONENT_NAME =
"com.android.googlesearch/.GoogleSearch";
@@ -131,10 +128,9 @@ public class Searchables {
// 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.
// This value is deprecated, return null
if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
return getDefaultSearchable();
return null;
}
String pkg = activity.getPackageName();
ComponentName referredActivity;
@@ -163,20 +159,6 @@ public class Searchables {
}
/**
* 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
@@ -205,8 +187,6 @@ public class Searchables {
= new ArrayList<SearchableInfo>();
ArrayList<SearchableInfo> newSearchablesInGlobalSearchList
= new ArrayList<SearchableInfo>();
ArrayList<SearchableInfo> newSearchablesForWebSearchList
= new ArrayList<SearchableInfo>();
final PackageManager pm = mContext.getPackageManager();
@@ -244,127 +224,71 @@ public class Searchables {
}
}
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 activity
ComponentName newGlobalSearchActivity = findGlobalSearchActivity();
// 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);
}
// Find the web search activity
ComponentName newWebSearchActivity = findWebSearchActivity(newGlobalSearchActivity);
// Store a consistent set of new values
synchronized (this) {
mSearchablesMap = newSearchablesMap;
mSearchablesList = newSearchablesList;
mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList;
mSearchablesForWebSearchList = newSearchablesForWebSearchList;
mDefaultSearchable = newDefaultSearchable;
mDefaultSearchableForWebSearch = newDefaultSearchableForWebSearch;
mGlobalSearchActivity = newGlobalSearchActivity;
mWebSearchActivity = newWebSearchActivity;
}
}
/**
* 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.
* Finds the global search activity.
*
* This is currently implemented by returning the first activity that handles
* the GLOBAL_SEARCH intent and has the GLOBAL_SEARCH permission. If we allow
* more than one global search activity to be installed, this code must be changed.
*/
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;
private ComponentName findGlobalSearchActivity() {
Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> activities =
pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
int count = activities == null ? 0 : activities.size();
for (int i = 0; i < count; i++) {
ActivityInfo ai = activities.get(i).activityInfo;
if (pm.checkPermission(Manifest.permission.GLOBAL_SEARCH,
ai.packageName) == PackageManager.PERMISSION_GRANTED) {
return new ComponentName(ai.packageName, ai.name);
} else {
Log.w(LOG_TAG, "Package " + ai.packageName + " wants to handle GLOBAL_SEARCH, "
+ "but does not have the GLOBAL_SEARCH permission.");
}
}
// 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;
Log.w(LOG_TAG, "No global search activity found");
return null;
}
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;
}
}
/**
* 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;
}
if (ri == null) return null;
return new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name);
Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.setPackage(globalSearchActivity.getPackageName());
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> activities =
pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
int count = activities == null ? 0 : activities.size();
for (int i = 0; i < count; i++) {
ActivityInfo ai = activities.get(i).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;
}
/**
@@ -383,24 +307,16 @@ public class Searchables {
}
/**
* Returns a list of the searchable activities that handle web searches.
* Gets the name of the global search activity.
*/
public synchronized ArrayList<SearchableInfo> getSearchablesForWebSearchList() {
return new ArrayList<SearchableInfo>(mSearchablesForWebSearchList);
public synchronized ComponentName getGlobalSearchActivity() {
return mGlobalSearchActivity;
}
/**
* Returns the default searchable activity for web searches.
* Gets the name of the web search activity.
*/
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();
public synchronized ComponentName getWebSearchActivity() {
return mWebSearchActivity;
}
}

View File

@@ -127,15 +127,4 @@
<item><xliff:g id="id">ime</xliff:g></item>
</string-array>
<!-- Do not translate. Each string points to the component name of an ACTION_WEB_SEARCH
handling activity. On startup if there were no preferred ACTION_WEB_SEARCH handlers,
the first component from this list which is found to be installed is set as the
preferred activity. -->
<string-array name="default_web_search_providers">
<item>com.google.android.googlequicksearchbox/.google.GoogleSearch</item>
<item>com.android.quicksearchbox/.google.GoogleSearch</item>
<item>com.google.android.providers.enhancedgooglesearch/.Launcher</item>
<item>com.android.googlesearch/.GoogleSearch</item>
<item>com.android.websearch/.Search.1</item>
</string-array>
</resources>

View File

@@ -64,39 +64,7 @@ public class SearchablesTest extends AndroidTestCase {
* findActionKey works
* getIcon works
*/
/**
* The goal of this test is to confirm proper operation of the
* SearchableInfo helper class.
*
* TODO: The metadata source needs to be mocked out because adding
* searchability metadata via this test is causing it to leak into the
* real system. So for now I'm just going to test for existence of the
* GlobalSearch app (which is searchable).
*/
public void testSearchableGlobalSearch() {
// test basic array & hashmap
Searchables searchables = new Searchables(mContext);
searchables.buildSearchableList();
// test linkage from another activity
// TODO inject this via mocking into the package manager.
// TODO for now, just check for searchable GlobalSearch app (this isn't really a unit test)
ComponentName thisActivity = new ComponentName(
"com.android.globalsearch",
"com.android.globalsearch.GlobalSearch");
SearchableInfo si = searchables.getSearchableInfo(thisActivity);
assertNotNull(si);
assertEquals(thisActivity, si.getSearchActivity());
Context appContext = si.getActivityContext(mContext);
assertNotNull(appContext);
MoreAsserts.assertNotEqual(appContext, mContext);
assertEquals("Quick Search Box", appContext.getString(si.getHintId()));
assertEquals("Quick Search Box", appContext.getString(si.getLabelId()));
}
/**
* Test that non-searchable activities return no searchable info (this would typically
* trigger the use of the default searchable e.g. contacts)
@@ -113,18 +81,7 @@ public class SearchablesTest extends AndroidTestCase {
SearchableInfo si = searchables.getSearchableInfo(nonActivity);
assertNull(si);
}
/**
* Test that there is a default searchable (aka global search provider).
*/
public void testDefaultSearchable() {
Searchables searchables = new Searchables(mContext);
searchables.buildSearchableList();
SearchableInfo si = searchables.getDefaultSearchable();
checkSearchable(si);
assertTrue(searchables.isDefaultSearchable(si));
}
/**
* This is an attempt to run the searchable info list with a mocked context. Here are some
* things I'd like to test.
@@ -371,7 +328,8 @@ public class SearchablesTest extends AndroidTestCase {
public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
assertNotNull(intent);
assertTrue(intent.getAction().equals(Intent.ACTION_SEARCH)
|| intent.getAction().equals(Intent.ACTION_WEB_SEARCH));
|| intent.getAction().equals(Intent.ACTION_WEB_SEARCH)
|| intent.getAction().equals(SearchManager.INTENT_ACTION_GLOBAL_SEARCH));
switch (mSearchablesMode) {
case SEARCHABLES_PASSTHROUGH:
return mRealPackageManager.queryIntentActivities(intent, flags);
@@ -472,6 +430,20 @@ public class SearchablesTest extends AndroidTestCase {
throw new UnsupportedOperationException();
}
}
@Override
public int checkPermission(String permName, String pkgName) {
assertNotNull(permName);
assertNotNull(pkgName);
switch (mSearchablesMode) {
case SEARCHABLES_PASSTHROUGH:
return mRealPackageManager.checkPermission(permName, pkgName);
case SEARCHABLES_MOCK_ZERO:
return PackageManager.PERMISSION_DENIED;
default:
throw new UnsupportedOperationException();
}
}
}
}