/* * Copyright (C) 2007 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.app; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.text.TextUtils; import android.util.Log; import android.util.Slog; import android.view.KeyEvent; import java.util.List; /** * This class provides access to the system search services. * *
In practice, you won't interact with this class directly, as search * services are provided through methods in {@link android.app.Activity Activity} * and the {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} * {@link android.content.Intent Intent}. * If you do require direct access to the SearchManager, do not instantiate * this class directly. Instead, retrieve it through * {@link android.content.Context#getSystemService * context.getSystemService(Context.SEARCH_SERVICE)}. * *
For more information about using the search dialog and adding search * suggestions in your application, read the * Search developer guide.
*Use this flag as a bit-field for {@link #SUGGEST_COLUMN_FLAGS}. */ public final static int FLAG_QUERY_REFINEMENT = 1 << 0; /** * Uri path for queried suggestions data. This is the path that the search manager * will use when querying your content provider for suggestions data based on user input * (e.g. looking for partial matches). * Typically you'll use this with a URI matcher. */ public final static String SUGGEST_URI_PATH_QUERY = "search_suggest_query"; /** * MIME type for suggestions data. You'll use this in your suggestions content provider * in the getType() function. */ public final static String SUGGEST_MIME_TYPE = "vnd.android.cursor.dir/vnd.android.search.suggest"; /** * Uri path for shortcut validation. This is the path that the search manager will use when * querying your content provider to refresh a shortcutted suggestion result and to check if it * is still valid. When asked, a source may return an up to date result, or no result. No * result indicates the shortcut refers to a no longer valid sugggestion. * * @see #SUGGEST_COLUMN_SHORTCUT_ID */ public final static String SUGGEST_URI_PATH_SHORTCUT = "search_suggest_shortcut"; /** * MIME type for shortcut validation. You'll use this in your suggestions content provider * in the getType() function. */ public final static String SHORTCUT_MIME_TYPE = "vnd.android.cursor.item/vnd.android.search.suggest"; /** * Column name for suggestions cursor. Unused - can be null or column can be omitted. */ public final static String SUGGEST_COLUMN_FORMAT = "suggest_format"; /** * Column name for suggestions cursor. Required. This is the primary line of text that * will be presented to the user as the suggestion. */ public final static String SUGGEST_COLUMN_TEXT_1 = "suggest_text_1"; /** * Column name for suggestions cursor. Optional. If your cursor includes this column, * then all suggestions will be provided in a two-line format. The second line of text is in * a much smaller appearance. */ public final static String SUGGEST_COLUMN_TEXT_2 = "suggest_text_2"; /** * Column name for suggestions cursor. Optional. This is a URL that will be shown * as the second line of text instead of {@link #SUGGEST_COLUMN_TEXT_2}. This is a separate * column so that the search UI knows to display the text as a URL, e.g. by using a different * color. If this column is absent, or has the value {@code null}, * {@link #SUGGEST_COLUMN_TEXT_2} will be used instead. */ public final static String SUGGEST_COLUMN_TEXT_2_URL = "suggest_text_2_url"; /** * Column name for suggestions cursor. Optional. If your cursor includes this column, * then all suggestions will be provided in a format that includes space for two small icons, * one at the left and one at the right of each suggestion. The data in the column must * be a resource ID of a drawable, or a URI in one of the following formats: * *
* Must be one of {@link #FLAG_QUERY_REFINEMENT} or 0 to indicate no flags. *
*/ public final static String SUGGEST_COLUMN_FLAGS = "suggest_flags"; /** * Column name for suggestions cursor. Optional. This column may be * used to specify the time in {@link System#currentTimeMillis * System.currentTImeMillis()} (wall time in UTC) when an item was last * accessed within the results-providing application. If set, this may be * used to show more-recently-used items first. */ public final static String SUGGEST_COLUMN_LAST_ACCESS_HINT = "suggest_last_access_hint"; /** * Column value for suggestion column {@link #SUGGEST_COLUMN_SHORTCUT_ID} when a suggestion * should not be stored as a shortcut in global search. */ public final static String SUGGEST_NEVER_MAKE_SHORTCUT = "_-1"; /** * Query parameter added to suggestion queries to limit the number of suggestions returned. * This limit is only advisory and suggestion providers may chose to ignore it. */ public final static String SUGGEST_PARAMETER_LIMIT = "limit"; /** * Intent action for starting the global search activity. * The global search provider should handle this intent. * * Supported extra data keys: {@link #QUERY}, * {@link #EXTRA_SELECT_QUERY}, * {@link #APP_DATA}. */ public final static String INTENT_ACTION_GLOBAL_SEARCH = "android.search.action.GLOBAL_SEARCH"; /** * Intent action for starting the global search settings activity. * The global search provider should handle this intent. */ public final static String INTENT_ACTION_SEARCH_SETTINGS = "android.search.action.SEARCH_SETTINGS"; /** * Intent action for starting a web search provider's settings activity. * Web search providers should handle this intent if they have provider-specific * settings to implement. */ public final static String INTENT_ACTION_WEB_SEARCH_SETTINGS = "android.search.action.WEB_SEARCH_SETTINGS"; /** * Intent action broadcasted to inform that the searchables list or default have changed. * Components should handle this intent if they cache any searchable data and wish to stay * up to date on changes. */ public final static String INTENT_ACTION_SEARCHABLES_CHANGED = "android.search.action.SEARCHABLES_CHANGED"; /** * Intent action to be broadcast to inform that the global search provider * has changed. */ public final static String INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED = "android.search.action.GLOBAL_SEARCH_ACTIVITY_CHANGED"; /** * Intent action broadcasted to inform that the search settings have changed in some way. * Either searchables have been enabled or disabled, or a different web search provider * has been chosen. */ public final static String INTENT_ACTION_SEARCH_SETTINGS_CHANGED = "android.search.action.SETTINGS_CHANGED"; /** * This means that context is voice, and therefore the SearchDialog should * continue showing the microphone until the user indicates that he/she does * not want to re-speak (e.g. by typing). * * @hide */ public final static String CONTEXT_IS_VOICE = "android.search.CONTEXT_IS_VOICE"; /** * This means that the voice icon should not be shown at all, because the * current search engine does not support voice search. * @hide */ public final static String DISABLE_VOICE_SEARCH = "android.search.DISABLE_VOICE_SEARCH"; /** * Reference to the shared system search service. */ private static ISearchManager mService; private final Context mContext; /** * The package associated with this seach manager. */ private String mAssociatedPackage; // package private since they are used by the inner class SearchManagerCallback /* package */ final Handler mHandler; /* package */ OnDismissListener mDismissListener = null; /* package */ OnCancelListener mCancelListener = null; private SearchDialog mSearchDialog; /*package*/ SearchManager(Context context, Handler handler) { mContext = context; mHandler = handler; mService = ISearchManager.Stub.asInterface( ServiceManager.getService(Context.SEARCH_SERVICE)); } /** * Launch search UI. * *The search manager will open a search widget in an overlapping * window, and the underlying activity may be obscured. The search * entry state will remain in effect until one of the following events: *
Most applications will not use this interface to invoke search.
* The primary method for invoking search is to call
* {@link android.app.Activity#onSearchRequested Activity.onSearchRequested()} or
* {@link android.app.Activity#startSearch Activity.startSearch()}.
*
* @param initialQuery A search string can be pre-entered here, but this
* is typically null or empty.
* @param selectInitialQuery If true, the intial query will be preselected, which means that
* any further typing will replace it. This is useful for cases where an entire pre-formed
* query is being inserted. If false, the selection point will be placed at the end of the
* inserted query. This is useful when the inserted query is text that the user entered,
* and the user would expect to be able to keep typing. This parameter is only meaningful
* if initialQuery is a non-empty string.
* @param launchActivity The ComponentName of the activity that has launched this search.
* @param appSearchData An application can insert application-specific
* context here, in order to improve quality or specificity of its own
* searches. This data will be returned with SEARCH intent(s). Null if
* no extra data is required.
* @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, global search will be launched.
* If true, this will always launch a platform-global (e.g. web-based) search instead.
*
* @see android.app.Activity#onSearchRequested
* @see #stopSearch
*/
public void startSearch(String initialQuery,
boolean selectInitialQuery,
ComponentName launchActivity,
Bundle appSearchData,
boolean globalSearch) {
startSearch(initialQuery, selectInitialQuery, launchActivity,
appSearchData, globalSearch, null);
}
/**
* As {@link #startSearch(String, boolean, ComponentName, Bundle, boolean)} but including
* source bounds for the global search intent.
*
* @hide
*/
public void startSearch(String initialQuery,
boolean selectInitialQuery,
ComponentName launchActivity,
Bundle appSearchData,
boolean globalSearch,
Rect sourceBounds) {
if (globalSearch) {
startGlobalSearch(initialQuery, selectInitialQuery, appSearchData, sourceBounds);
return;
}
ensureSearchDialog();
mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData);
}
private void ensureSearchDialog() {
if (mSearchDialog == null) {
mSearchDialog = new SearchDialog(mContext, this);
mSearchDialog.setOnCancelListener(this);
mSearchDialog.setOnDismissListener(this);
}
}
/**
* Starts the global search activity.
*/
/* package */ void startGlobalSearch(String initialQuery, boolean selectInitialQuery,
Bundle appSearchData, Rect sourceBounds) {
ComponentName globalSearchActivity = getGlobalSearchActivity();
if (globalSearchActivity == null) {
Log.w(TAG, "No global search activity found.");
return;
}
Intent intent = new Intent(INTENT_ACTION_GLOBAL_SEARCH);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setComponent(globalSearchActivity);
// Make sure that we have a Bundle to put source in
if (appSearchData == null) {
appSearchData = new Bundle();
} else {
appSearchData = new Bundle(appSearchData);
}
// Set source to package name of app that starts global search, if not set already.
if (!appSearchData.containsKey("source")) {
appSearchData.putString("source", mContext.getPackageName());
}
intent.putExtra(APP_DATA, appSearchData);
if (!TextUtils.isEmpty(initialQuery)) {
intent.putExtra(QUERY, initialQuery);
}
if (selectInitialQuery) {
intent.putExtra(EXTRA_SELECT_QUERY, selectInitialQuery);
}
intent.setSourceBounds(sourceBounds);
try {
if (DBG) Log.d(TAG, "Starting global search: " + intent.toUri(0));
mContext.startActivity(intent);
} catch (ActivityNotFoundException ex) {
Log.e(TAG, "Global search activity not found: " + globalSearchActivity);
}
}
/**
* Returns a list of installed apps that handle the global search
* intent.
*
* @hide
*/
public List Typically the user will terminate the search UI by launching a
* search or by canceling. This function allows the underlying application
* or activity to cancel the search prematurely (for any reason).
*
* This function can be safely called at any time (even if no search is active.)
*
* @see #startSearch
*/
public void stopSearch() {
if (mSearchDialog != null) {
mSearchDialog.cancel();
}
}
/**
* Determine if the Search UI is currently displayed.
*
* This is provided primarily for application test purposes.
*
* @return Returns true if the search UI is currently displayed.
*
* @hide
*/
public boolean isVisible() {
return mSearchDialog == null? false : mSearchDialog.isShowing();
}
/**
* See {@link SearchManager#setOnDismissListener} for configuring your activity to monitor
* search UI state.
*/
public interface OnDismissListener {
/**
* This method will be called when the search UI is dismissed. To make use of it, you must
* implement this method in your activity, and call
* {@link SearchManager#setOnDismissListener} to register it.
*/
public void onDismiss();
}
/**
* See {@link SearchManager#setOnCancelListener} for configuring your activity to monitor
* search UI state.
*/
public interface OnCancelListener {
/**
* This method will be called when the search UI is canceled. To make use if it, you must
* implement this method in your activity, and call
* {@link SearchManager#setOnCancelListener} to register it.
*/
public void onCancel();
}
/**
* Set or clear the callback that will be invoked whenever the search UI is dismissed.
*
* @param listener The {@link OnDismissListener} to use, or null.
*/
public void setOnDismissListener(final OnDismissListener listener) {
mDismissListener = listener;
}
/**
* Set or clear the callback that will be invoked whenever the search UI is canceled.
*
* @param listener The {@link OnCancelListener} to use, or null.
*/
public void setOnCancelListener(OnCancelListener listener) {
mCancelListener = listener;
}
/**
* @deprecated This method is an obsolete internal implementation detail. Do not use.
*/
@Deprecated
public void onCancel(DialogInterface dialog) {
if (mCancelListener != null) {
mCancelListener.onCancel();
}
}
/**
* @deprecated This method is an obsolete internal implementation detail. Do not use.
*/
@Deprecated
public void onDismiss(DialogInterface dialog) {
if (mDismissListener != null) {
mDismissListener.onDismiss();
}
}
/**
* Gets information about a searchable activity.
*
* @param componentName The activity to get searchable information for.
* @return Searchable information, or null
if the activity does not
* exist, or is not searchable.
*/
public SearchableInfo getSearchableInfo(ComponentName componentName) {
try {
return mService.getSearchableInfo(componentName);
} catch (RemoteException ex) {
Log.e(TAG, "getSearchableInfo() failed: " + ex);
return null;
}
}
/**
* Gets a cursor with search suggestions.
*
* @param searchable Information about how to get the suggestions.
* @param query The search text entered (so far).
* @return a cursor with suggestions, or null the suggestion query failed.
*
* @hide because SearchableInfo is not part of the API.
*/
public Cursor getSuggestions(SearchableInfo searchable, String query) {
return getSuggestions(searchable, query, -1);
}
/**
* Gets a cursor with search suggestions.
*
* @param searchable Information about how to get the suggestions.
* @param query The search text entered (so far).
* @param limit The query limit to pass to the suggestion provider. This is advisory,
* the returned cursor may contain more rows. Pass {@code -1} for no limit.
* @return a cursor with suggestions, or
null the suggestion query failed.
*
* @hide because SearchableInfo is not part of the API.
*/
public Cursor getSuggestions(SearchableInfo searchable, String query, int limit) {
if (searchable == null) {
return null;
}
String authority = searchable.getSuggestAuthority();
if (authority == null) {
return null;
}
Uri.Builder uriBuilder = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority)
.query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
.fragment(""); // TODO: Remove, workaround for a bug in Uri.writeToParcel()
// if content path provided, insert it now
final String contentPath = searchable.getSuggestPath();
if (contentPath != null) {
uriBuilder.appendEncodedPath(contentPath);
}
// append standard suggestion query path
uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
// get the query selection, may be null
String selection = searchable.getSuggestSelection();
// inject query, either as selection args or inline
String[] selArgs = null;
if (selection != null) { // use selection if provided
selArgs = new String[] { query };
} else { // no selection, use REST pattern
uriBuilder.appendPath(query);
}
if (limit > 0) {
uriBuilder.appendQueryParameter(SUGGEST_PARAMETER_LIMIT, String.valueOf(limit));
}
Uri uri = uriBuilder.build();
// finally, make the query
return mContext.getContentResolver().query(uri, null, selection, selArgs, null);
}
/**
* Returns a list of the searchable activities that can be included in global search.
*
* @return a list containing searchable information for all searchable activities
* that have the
android:includeInGlobalSearch
attribute set
* in their searchable meta-data.
*/
public List