Merge change 3880 into donut

* changes:
  Run search dialog in the system process.
This commit is contained in:
Android (Google) Code Review
2009-06-15 02:06:39 -07:00
14 changed files with 739 additions and 179 deletions

View File

@@ -76,6 +76,7 @@ LOCAL_SRC_FILES += \
core/java/android/app/IIntentSender.aidl \
core/java/android/app/INotificationManager.aidl \
core/java/android/app/ISearchManager.aidl \
core/java/android/app/ISearchManagerCallback.aidl \
core/java/android/app/IServiceConnection.aidl \
core/java/android/app/IStatusBar.aidl \
core/java/android/app/IThumbnailReceiver.aidl \

View File

@@ -628,6 +628,8 @@ public class Activity extends ContextThemeWrapper
boolean mStartedActivity;
/*package*/ int mConfigChangeFlags;
/*package*/ Configuration mCurrentConfig;
private SearchManager mSearchManager;
private Bundle mSearchDialogState = null;
private Window mWindow;
@@ -788,6 +790,9 @@ public class Activity extends ContextThemeWrapper
protected void onCreate(Bundle savedInstanceState) {
mVisibleFromClient = mWindow.getWindowStyle().getBoolean(
com.android.internal.R.styleable.Window_windowNoDisplay, true);
// uses super.getSystemService() since this.getSystemService() looks at the
// mSearchManager field.
mSearchManager = (SearchManager) super.getSystemService(Context.SEARCH_SERVICE);
mCalled = true;
}
@@ -805,9 +810,10 @@ public class Activity extends ContextThemeWrapper
// Also restore the state of a search dialog (if any)
// TODO more generic than just this manager
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchManager.restoreSearchDialog(savedInstanceState, SAVED_SEARCH_DIALOG_KEY);
Bundle searchState = savedInstanceState.getBundle(SAVED_SEARCH_DIALOG_KEY);
if (searchState != null) {
mSearchManager.restoreSearchDialog(searchState);
}
}
/**
@@ -1013,9 +1019,11 @@ public class Activity extends ContextThemeWrapper
// Also save the state of a search dialog (if any)
// TODO more generic than just this manager
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchManager.saveSearchDialog(outState, SAVED_SEARCH_DIALOG_KEY);
// onPause() should always be called before this method, so mSearchManagerState
// should be up to date.
if (mSearchDialogState != null) {
outState.putBundle(SAVED_SEARCH_DIALOG_KEY, mSearchDialogState);
}
}
/**
@@ -1286,12 +1294,6 @@ public class Activity extends ContextThemeWrapper
}
}
}
// also dismiss search dialog if showing
// TODO more generic than just this manager
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchManager.stopSearch();
// close any cursors we are managing.
int numCursors = mManagedCursors.size();
@@ -1301,6 +1303,10 @@ public class Activity extends ContextThemeWrapper
c.mCursor.close();
}
}
// Clear any search state saved in performPause(). If the state may be needed in the
// future, it will have been saved by performSaveInstanceState()
mSearchDialogState = null;
}
/**
@@ -1324,9 +1330,7 @@ public class Activity extends ContextThemeWrapper
// also update search dialog if showing
// TODO more generic than just this manager
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchManager.onConfigurationChanged(newConfig);
mSearchManager.onConfigurationChanged(newConfig);
if (mWindow != null) {
// Pass the configuration changed event to the window
@@ -2543,10 +2547,7 @@ public class Activity extends ContextThemeWrapper
*/
public void startSearch(String initialQuery, boolean selectInitialQuery,
Bundle appSearchData, boolean globalSearch) {
// activate the search manager and start it up!
SearchManager searchManager = (SearchManager)
getSystemService(Context.SEARCH_SERVICE);
searchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(),
mSearchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(),
appSearchData, globalSearch);
}
@@ -3265,6 +3266,8 @@ public class Activity extends ContextThemeWrapper
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
return mSearchManager;
}
return super.getSystemService(name);
}
@@ -3563,10 +3566,21 @@ public class Activity extends ContextThemeWrapper
"Activity " + mComponent.toShortString() +
" did not call through to super.onPostResume()");
}
// restore search dialog, if any
if (mSearchDialogState != null) {
mSearchManager.restoreSearchDialog(mSearchDialogState);
}
mSearchDialogState = null;
}
final void performPause() {
onPause();
// save search dialog state if the search dialog is open,
// and then dismiss the search dialog
mSearchDialogState = mSearchManager.saveSearchDialog();
mSearchManager.stopSearch();
}
final void performUserLeaving() {

View File

@@ -16,7 +16,10 @@
package android.app;
import android.app.ISearchManagerCallback;
import android.content.ComponentName;
import android.content.res.Configuration;
import android.os.Bundle;
import android.server.search.SearchableInfo;
/** @hide */
@@ -26,4 +29,15 @@ interface ISearchManager {
List<SearchableInfo> getSearchablesForWebSearch();
SearchableInfo getDefaultSearchableForWebSearch();
void setDefaultWebSearch(in ComponentName component);
void startSearch(in String initialQuery,
boolean selectInitialQuery,
in ComponentName launchActivity,
in Bundle appSearchData,
boolean globalSearch,
ISearchManagerCallback searchManagerCallback);
void stopSearch();
boolean isVisible();
Bundle onSaveInstanceState();
void onRestoreInstanceState(in Bundle savedInstanceState);
void onConfigurationChanged(in Configuration newConfig);
}

View File

@@ -0,0 +1,23 @@
/**
* 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.app;
/** @hide */
oneway interface ISearchManagerCallback {
void onDismiss();
void onCancel();
}

View File

@@ -76,8 +76,8 @@ import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* In-application-process implementation of Search Bar. This is still controlled by the
* SearchManager, but it runs in the current activity's process to keep things lighter weight.
* System search dialog. This is controlled by the
* SearchManagerService and runs in the system process.
*
* @hide
*/
@@ -179,17 +179,17 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Window theWindow = getWindow();
theWindow.setGravity(Gravity.TOP|Gravity.FILL_HORIZONTAL);
setContentView(com.android.internal.R.layout.search_bar);
theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT,
// taking up the whole window (even when transparent) is less than ideal,
// but necessary to show the popup window until the window manager supports
// having windows anchored by their parent but not clipped by them.
ViewGroup.LayoutParams.FILL_PARENT);
Window theWindow = getWindow();
WindowManager.LayoutParams lp = theWindow.getAttributes();
lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR;
lp.width = ViewGroup.LayoutParams.FILL_PARENT;
// taking up the whole window (even when transparent) is less than ideal,
// but necessary to show the popup window until the window manager supports
// having windows anchored by their parent but not clipped by them.
lp.height = ViewGroup.LayoutParams.FILL_PARENT;
lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
theWindow.setAttributes(lp);
@@ -234,10 +234,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
// Save voice intent for later queries/launching
mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mLocationManager =
(LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
@@ -278,12 +280,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*/
public boolean show(String initialQuery, boolean selectInitialQuery,
ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
if (isShowing()) {
// race condition - already showing but not handling events yet.
// in this case, just discard the "show" request
return true;
}
// Reset any stored values from last time dialog was shown.
mStoredComponentName = null;
mStoredAppSearchData = null;
@@ -442,11 +439,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
stopLocationUpdates();
// TODO: Removing the listeners means that they never get called, since
// Dialog.dismissDialog() calls onStop() before sendDismissMessage().
setOnCancelListener(null);
setOnDismissListener(null);
// stop receiving broadcasts (throws exception if none registered)
try {
getContext().unregisterReceiver(mBroadcastReceiver);
@@ -654,15 +646,15 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
// we dismiss the entire dialog instead
mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
if (mGlobalSearchMode) {
mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in
mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
mSearchAutoComplete.setDropDownBackgroundResource(
com.android.internal.R.drawable.search_dropdown_background);
} else {
mSearchAutoComplete.setDropDownAlwaysVisible(false);
mSearchAutoComplete.setDropDownDismissedOnCompletion(true);
mSearchAutoComplete.setDropDownBackgroundResource(
com.android.internal.R.drawable.search_dropdown_background_apps);
}
@@ -1317,7 +1309,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
}
/**
* Launches an intent. Also dismisses the search dialog if not in global search mode.
* Launches an intent and dismisses the search dialog (unless the intent
* is one of the special intents that modifies the state of the search dialog).
*/
private void launchIntent(Intent intent) {
if (intent == null) {
@@ -1326,9 +1319,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
if (handleSpecialIntent(intent)){
return;
}
if (!mGlobalSearchMode) {
dismiss();
}
dismiss();
getContext().startActivity(intent);
}
@@ -1511,6 +1502,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
int actionKey, String actionMsg) {
// Now build the Intent
Intent intent = new Intent(action);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (data != null) {
intent.setData(data);
}
@@ -1595,14 +1587,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
private boolean isEmpty() {
return TextUtils.getTrimmedLength(getText()) == 0;
}
/**
* Clears the entered text.
*/
private void clear() {
setText("");
}
/**
* We override this method to avoid replacing the query box text
* when a suggestion is clicked.

View File

@@ -28,6 +28,7 @@ import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.server.search.SearchableInfo;
import android.util.Log;
import android.view.KeyEvent;
import java.util.List;
@@ -1108,6 +1109,10 @@ import java.util.List;
public class SearchManager
implements DialogInterface.OnDismissListener, DialogInterface.OnCancelListener
{
private static final boolean DBG = false;
private static final String TAG = "SearchManager";
/**
* This is a shortcut definition for the default menu key to use for invoking search.
*
@@ -1494,12 +1499,14 @@ public class SearchManager
private static ISearchManager sService = getSearchManagerService();
private final Context mContext;
private final Handler mHandler;
private SearchDialog mSearchDialog;
private OnDismissListener mDismissListener = null;
private OnCancelListener mCancelListener = null;
// package private since they are used by the inner class SearchManagerCallback
/* package */ boolean mIsShowing = false;
/* package */ final Handler mHandler;
/* package */ OnDismissListener mDismissListener = null;
/* package */ OnCancelListener mCancelListener = null;
private final SearchManagerCallback mSearchManagerCallback = new SearchManagerCallback();
/*package*/ SearchManager(Context context, Handler handler) {
mContext = context;
@@ -1551,17 +1558,16 @@ public class SearchManager
ComponentName launchActivity,
Bundle appSearchData,
boolean globalSearch) {
if (mSearchDialog == null) {
mSearchDialog = new SearchDialog(mContext);
if (DBG) debug("startSearch(), mIsShowing=" + mIsShowing);
if (mIsShowing) return;
try {
mIsShowing = true;
// activate the search manager and start it up!
sService.startSearch(initialQuery, selectInitialQuery, launchActivity, appSearchData,
globalSearch, mSearchManagerCallback);
} catch (RemoteException ex) {
Log.e(TAG, "startSearch() failed: " + ex);
}
// activate the search manager and start it up!
mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData,
globalSearch);
mSearchDialog.setOnCancelListener(this);
mSearchDialog.setOnDismissListener(this);
}
/**
@@ -1575,9 +1581,16 @@ public class SearchManager
*
* @see #startSearch
*/
public void stopSearch() {
if (mSearchDialog != null) {
mSearchDialog.cancel();
public void stopSearch() {
if (DBG) debug("stopSearch(), mIsShowing=" + mIsShowing);
if (!mIsShowing) return;
try {
sService.stopSearch();
// onDismiss will also clear this, but we do it here too since onDismiss() is
// called asynchronously.
mIsShowing = false;
} catch (RemoteException ex) {
Log.e(TAG, "stopSearch() failed: " + ex);
}
}
@@ -1590,13 +1603,11 @@ public class SearchManager
*
* @hide
*/
public boolean isVisible() {
if (mSearchDialog != null) {
return mSearchDialog.isShowing();
}
return false;
public boolean isVisible() {
if (DBG) debug("isVisible(), mIsShowing=" + mIsShowing);
return mIsShowing;
}
/**
* See {@link SearchManager#setOnDismissListener} for configuring your activity to monitor
* search UI state.
@@ -1631,79 +1642,112 @@ public class SearchManager
public void setOnDismissListener(final OnDismissListener listener) {
mDismissListener = listener;
}
/**
* The callback from the search dialog when dismissed
* @hide
*/
public void onDismiss(DialogInterface dialog) {
if (dialog == mSearchDialog) {
if (mDismissListener != null) {
mDismissListener.onDismiss();
}
}
}
/**
* 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(final OnCancelListener listener) {
public void setOnCancelListener(OnCancelListener listener) {
mCancelListener = listener;
}
/**
* The callback from the search dialog when canceled
* @hide
*/
private class SearchManagerCallback extends ISearchManagerCallback.Stub {
private final Runnable mFireOnDismiss = new Runnable() {
public void run() {
if (DBG) debug("mFireOnDismiss");
mIsShowing = false;
if (mDismissListener != null) {
mDismissListener.onDismiss();
}
}
};
private final Runnable mFireOnCancel = new Runnable() {
public void run() {
if (DBG) debug("mFireOnCancel");
// doesn't need to clear mIsShowing since onDismiss() always gets called too
if (mCancelListener != null) {
mCancelListener.onCancel();
}
}
};
public void onDismiss() {
if (DBG) debug("onDismiss()");
mHandler.post(mFireOnDismiss);
}
public void onCancel() {
if (DBG) debug("onCancel()");
mHandler.post(mFireOnCancel);
}
}
// TODO: remove the DialogInterface interfaces from SearchManager.
// This changes the public API, so I'll do it in a separate change.
public void onCancel(DialogInterface dialog) {
if (dialog == mSearchDialog) {
if (mCancelListener != null) {
mCancelListener.onCancel();
}
throw new UnsupportedOperationException();
}
public void onDismiss(DialogInterface dialog) {
throw new UnsupportedOperationException();
}
/**
* Saves the state of the search UI.
*
* @return A Bundle containing the state of the search dialog, or {@code null}
* if the search UI is not visible.
*
* @hide
*/
public Bundle saveSearchDialog() {
if (DBG) debug("saveSearchDialog(), mIsShowing=" + mIsShowing);
if (!mIsShowing) return null;
try {
return sService.onSaveInstanceState();
} catch (RemoteException ex) {
Log.e(TAG, "onSaveInstanceState() failed: " + ex);
return null;
}
}
/**
* Save instance state so we can recreate after a rotation.
*
* Restores the state of the search dialog.
*
* @param searchDialogState Bundle to read the state from.
*
* @hide
*/
void saveSearchDialog(Bundle outState, String key) {
if (mSearchDialog != null && mSearchDialog.isShowing()) {
Bundle searchDialogState = mSearchDialog.onSaveInstanceState();
outState.putBundle(key, searchDialogState);
public void restoreSearchDialog(Bundle searchDialogState) {
if (DBG) debug("restoreSearchDialog(" + searchDialogState + ")");
if (searchDialogState == null) return;
try {
sService.onRestoreInstanceState(searchDialogState);
} catch (RemoteException ex) {
Log.e(TAG, "onRestoreInstanceState() failed: " + ex);
}
}
/**
* Restore instance state after a rotation.
*
* Update the search dialog after a configuration change.
*
* @param newConfig The new configuration.
*
* @hide
*/
void restoreSearchDialog(Bundle inState, String key) {
Bundle searchDialogState = inState.getBundle(key);
if (searchDialogState != null) {
if (mSearchDialog == null) {
mSearchDialog = new SearchDialog(mContext);
}
mSearchDialog.onRestoreInstanceState(searchDialogState);
public void onConfigurationChanged(Configuration newConfig) {
if (DBG) debug("onConfigurationChanged(" + newConfig + "), mIsShowing=" + mIsShowing);
if (!mIsShowing) return;
try {
sService.onConfigurationChanged(newConfig);
} catch (RemoteException ex) {
Log.e(TAG, "onConfigurationChanged() failed:" + ex);
}
}
/**
* Hook for updating layout on a rotation
*
* @hide
*/
void onConfigurationChanged(Configuration newConfig) {
if (mSearchDialog != null && mSearchDialog.isShowing()) {
mSearchDialog.onConfigurationChanged(newConfig);
}
}
private static ISearchManager getSearchManagerService() {
return ISearchManager.Stub.asInterface(
ServiceManager.getService(Context.SEARCH_SERVICE));
@@ -1724,7 +1768,8 @@ public class SearchManager
boolean globalSearch) {
try {
return sService.getSearchableInfo(componentName, globalSearch);
} catch (RemoteException e) {
} catch (RemoteException ex) {
Log.e(TAG, "getSearchableInfo() failed: " + ex);
return null;
}
}
@@ -1805,6 +1850,7 @@ public class SearchManager
try {
return sService.getSearchablesInGlobalSearch();
} catch (RemoteException e) {
Log.e(TAG, "getSearchablesInGlobalSearch() failed: " + e);
return null;
}
}
@@ -1812,7 +1858,8 @@ public class SearchManager
/**
* Returns a list of the searchable activities that handle web searches.
*
* @return a a list of all searchable activities that handle {@link SearchManager#ACTION_WEB_SEARCH}.
* @return a list of all searchable activities that handle
* {@link android.content.Intent#ACTION_WEB_SEARCH}.
*
* @hide because SearchableInfo is not part of the API.
*/
@@ -1820,6 +1867,7 @@ public class SearchManager
try {
return sService.getSearchablesForWebSearch();
} catch (RemoteException e) {
Log.e(TAG, "getSearchablesForWebSearch() failed: " + e);
return null;
}
}
@@ -1835,6 +1883,7 @@ public class SearchManager
try {
return sService.getDefaultSearchableForWebSearch();
} catch (RemoteException e) {
Log.e(TAG, "getDefaultSearchableForWebSearch() failed: " + e);
return null;
}
}
@@ -1850,6 +1899,12 @@ public class SearchManager
try {
sService.setDefaultWebSearch(component);
} catch (RemoteException e) {
Log.e(TAG, "setDefaultWebSearch() failed: " + e);
}
}
private static void debug(String msg) {
Thread thread = Thread.currentThread();
Log.d(TAG, msg + " (" + thread.getName() + "-" + thread.getId() + ")");
}
}

View File

@@ -17,15 +17,25 @@
package android.server.search;
import android.app.ISearchManager;
import android.app.ISearchManagerCallback;
import android.app.SearchDialog;
import android.app.SearchManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* This is a simplified version of the Search Manager service. It no longer handles
@@ -34,16 +44,20 @@ import java.util.List;
* invoked search) to specific searchable activities (where the search will be dispatched).
*/
public class SearchManagerService extends ISearchManager.Stub
implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener
{
// general debugging support
private static final String TAG = "SearchManagerService";
private static final boolean DEBUG = false;
private static final boolean DBG = false;
// class maintenance and general shared data
private final Context mContext;
private final Handler mHandler;
private boolean mSearchablesDirty;
private Searchables mSearchables;
private final Searchables mSearchables;
final SearchDialog mSearchDialog;
ISearchManagerCallback mCallback = null;
/**
* Initializes the Search Manager service in the provided system context.
@@ -56,6 +70,9 @@ public class SearchManagerService extends ISearchManager.Stub
mHandler = new Handler();
mSearchablesDirty = true;
mSearchables = new Searchables(context);
mSearchDialog = new SearchDialog(context);
mSearchDialog.setOnCancelListener(this);
mSearchDialog.setOnDismissListener(this);
// Setup the infrastructure for updating and maintaining the list
// of searchable activities.
@@ -107,6 +124,7 @@ public class SearchManagerService extends ISearchManager.Stub
* a package add/remove broadcast message.
*/
private void updateSearchables() {
if (DBG) debug("updateSearchables()");
mSearchables.buildSearchableList();
mSearchablesDirty = false;
}
@@ -137,6 +155,10 @@ public class SearchManagerService extends ISearchManager.Stub
if (globalSearch) {
si = mSearchables.getDefaultSearchable();
} else {
if (launchActivity == null) {
Log.e(TAG, "getSearchableInfo(), activity == null");
return null;
}
si = mSearchables.getSearchableInfo(launchActivity);
}
@@ -150,6 +172,145 @@ public class SearchManagerService extends ISearchManager.Stub
updateSearchablesIfDirty();
return mSearchables.getSearchablesInGlobalSearchList();
}
/**
* Launches the search UI on the main thread of the service.
*
* @see SearchManager#startSearch(String, boolean, ComponentName, Bundle, boolean)
*/
public void startSearch(final String initialQuery,
final boolean selectInitialQuery,
final ComponentName launchActivity,
final Bundle appSearchData,
final boolean globalSearch,
final ISearchManagerCallback searchManagerCallback) {
if (DBG) debug("startSearch()");
Runnable task = new Runnable() {
public void run() {
performStartSearch(initialQuery,
selectInitialQuery,
launchActivity,
appSearchData,
globalSearch,
searchManagerCallback);
}
};
mHandler.post(task);
}
/**
* Actually launches the search. This must be called on the service UI thread.
*/
/*package*/ void performStartSearch(String initialQuery,
boolean selectInitialQuery,
ComponentName launchActivity,
Bundle appSearchData,
boolean globalSearch,
ISearchManagerCallback searchManagerCallback) {
if (DBG) debug("performStartSearch()");
mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData,
globalSearch);
if (searchManagerCallback != null) {
mCallback = searchManagerCallback;
}
}
/**
* Cancels the search dialog. Can be called from any thread.
*/
public void stopSearch() {
if (DBG) debug("stopSearch()");
mHandler.post(new Runnable() {
public void run() {
performStopSearch();
}
});
}
/**
* Cancels the search dialog. Must be called from the service UI thread.
*/
/*package*/ void performStopSearch() {
if (DBG) debug("performStopSearch()");
mSearchDialog.cancel();
}
/**
* Determines if the Search UI is currently displayed.
*
* @see SearchManager#isVisible()
*/
public boolean isVisible() {
return postAndWait(mIsShowing, false, "isShowing()");
}
private final Callable<Boolean> mIsShowing = new Callable<Boolean>() {
public Boolean call() {
return mSearchDialog.isShowing();
}
};
public Bundle onSaveInstanceState() {
return postAndWait(mOnSaveInstanceState, null, "onSaveInstanceState()");
}
private final Callable<Bundle> mOnSaveInstanceState = new Callable<Bundle>() {
public Bundle call() {
if (mSearchDialog.isShowing()) {
return mSearchDialog.onSaveInstanceState();
} else {
return null;
}
}
};
public void onRestoreInstanceState(final Bundle searchDialogState) {
if (searchDialogState != null) {
mHandler.post(new Runnable() {
public void run() {
mSearchDialog.onRestoreInstanceState(searchDialogState);
}
});
}
}
public void onConfigurationChanged(final Configuration newConfig) {
mHandler.post(new Runnable() {
public void run() {
if (mSearchDialog.isShowing()) {
mSearchDialog.onConfigurationChanged(newConfig);
}
}
});
}
/**
* Called by {@link SearchDialog} when it goes away.
*/
public void onDismiss(DialogInterface dialog) {
if (DBG) debug("onDismiss()");
if (mCallback != null) {
try {
mCallback.onDismiss();
} catch (RemoteException ex) {
Log.e(TAG, "onDismiss() failed: " + ex);
}
}
}
/**
* Called by {@link SearchDialog} when the user or activity cancels search.
* When this is called, {@link #onDismiss} is called too.
*/
public void onCancel(DialogInterface dialog) {
if (DBG) debug("onCancel()");
if (mCallback != null) {
try {
mCallback.onCancel();
} catch (RemoteException ex) {
Log.e(TAG, "onCancel() failed: " + ex);
}
}
}
/**
* Returns a list of the searchable activities that handle web searches.
@@ -173,4 +334,34 @@ public class SearchManagerService extends ISearchManager.Stub
public void setDefaultWebSearch(ComponentName component) {
mSearchables.setDefaultWebSearch(component);
}
/**
* Runs an operation on the handler for the service, blocks until it returns,
* and returns the value returned by the operation.
*
* @param <V> Return value type.
* @param callable Operation to run.
* @param errorResult Value to return if the operations throws an exception.
* @param name Operation name to include in error log messages.
* @return The value returned by the operation.
*/
private <V> V postAndWait(Callable<V> callable, V errorResult, String name) {
FutureTask<V> task = new FutureTask<V>(callable);
mHandler.post(task);
try {
return task.get();
} catch (InterruptedException ex) {
Log.e(TAG, "Error calling " + name + ": " + ex);
return errorResult;
} catch (ExecutionException ex) {
Log.e(TAG, "Error calling " + name + ": " + ex);
return errorResult;
}
}
private static void debug(String msg) {
Thread thread = Thread.currentThread();
Log.d(TAG, msg + " (" + thread.getName() + "-" + thread.getId() + ")");
}
}

View File

@@ -320,7 +320,7 @@ public final class SearchableInfo implements Parcelable {
// for now, implement some form of rules - minimal data
if (mLabelId == 0) {
throw new IllegalArgumentException("No label.");
throw new IllegalArgumentException("Search label must be a resource reference.");
}
}
@@ -441,13 +441,17 @@ public final class SearchableInfo implements Parcelable {
xml.close();
if (DBG) {
Log.d(LOG_TAG, "Checked " + activityInfo.name
+ ",label=" + searchable.getLabelId()
+ ",icon=" + searchable.getIconId()
+ ",suggestAuthority=" + searchable.getSuggestAuthority()
+ ",target=" + searchable.getSearchActivity().getClassName()
+ ",global=" + searchable.shouldIncludeInGlobalSearch()
+ ",threshold=" + searchable.getSuggestThreshold());
if (searchable != null) {
Log.d(LOG_TAG, "Checked " + activityInfo.name
+ ",label=" + searchable.getLabelId()
+ ",icon=" + searchable.getIconId()
+ ",suggestAuthority=" + searchable.getSuggestAuthority()
+ ",target=" + searchable.getSearchActivity().getClassName()
+ ",global=" + searchable.shouldIncludeInGlobalSearch()
+ ",threshold=" + searchable.getSuggestThreshold());
} else {
Log.d(LOG_TAG, "Checked " + activityInfo.name + ", no searchable meta-data");
}
}
return searchable;
}

View File

@@ -219,7 +219,20 @@
</service>
<!-- Application components used for search manager tests -->
<!-- TODO: Removed temporarily - need to be replaced using mocks -->
<activity android:name=".SearchableActivity"
android:label="Searchable Activity">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<provider android:name=".SuggestionProvider"
android:authorities="com.android.unit_tests.SuggestionProvider">
</provider>
<!-- Used to test IPC. -->
<service android:name=".binder.BinderTestService"

View File

@@ -50,5 +50,8 @@
<item quantity="other">Some dogs</item>
</plurals>
<string name="searchable_label">SearchManager Test</string>
<string name="searchable_hint">A search hint</string>
<!-- <string name="layout_six_text_text">F</string> -->
</resources>

View File

@@ -15,7 +15,12 @@
-->
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="SearchManagerTest"
android:hint="SearchManagerTest Hint"
/>
android:label="@string/searchable_label"
android:hint="@string/searchable_hint"
android:searchSuggestAuthority="com.android.unit_tests.SuggestionProvider"
>
<actionkey android:keycode="KEYCODE_CALL"
android:suggestActionMsgColumn="suggest_action_msg_call" />
</searchable>

View File

@@ -23,7 +23,10 @@ import android.app.ISearchManager;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.server.search.SearchableInfo;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
@@ -37,12 +40,11 @@ import android.util.AndroidRuntimeException;
* com.android.unit_tests/android.test.InstrumentationTestRunner
*/
public class SearchManagerTest extends ActivityInstrumentationTestCase2<LocalActivity> {
// If non-zero, enable a set of tests that start and stop the search manager.
// This is currently disabled because it's causing an unwanted jump from the unit test
// activity into the contacts activity. We'll put this back after we disable that jump.
private static final int TEST_SEARCH_START = 0;
private ComponentName SEARCHABLE_ACTIVITY =
new ComponentName("com.android.unit_tests",
"com.android.unit_tests.SearchableActivity");
/*
* Bug list of test ideas.
*
@@ -88,7 +90,30 @@ public class SearchManagerTest extends ActivityInstrumentationTestCase2<LocalAct
super.setUp();
Activity testActivity = getActivity();
mContext = (Context)testActivity;
mContext = testActivity;
}
private ISearchManager getSearchManagerService() {
return ISearchManager.Stub.asInterface(
ServiceManager.getService(Context.SEARCH_SERVICE));
}
// Checks that the search UI is visible.
private void assertSearchVisible() {
SearchManager searchManager = (SearchManager)
mContext.getSystemService(Context.SEARCH_SERVICE);
assertTrue("SearchManager thinks search UI isn't visible when it should be",
searchManager.isVisible());
}
// Checks that the search UI is not visible.
// This checks both the SearchManager and the SearchManagerService,
// since SearchManager keeps a local variable for the visibility.
private void assertSearchNotVisible() {
SearchManager searchManager = (SearchManager)
mContext.getSystemService(Context.SEARCH_SERVICE);
assertFalse("SearchManager thinks search UI is visible when it shouldn't be",
searchManager.isVisible());
}
/**
@@ -97,9 +122,7 @@ public class SearchManagerTest extends ActivityInstrumentationTestCase2<LocalAct
*/
@MediumTest
public void testSearchManagerInterfaceAvailable() {
ISearchManager searchManager1 = ISearchManager.Stub.asInterface(
ServiceManager.getService(Context.SEARCH_SERVICE));
assertNotNull(searchManager1);
assertNotNull(getSearchManagerService());
}
/**
@@ -135,38 +158,127 @@ public class SearchManagerTest extends ActivityInstrumentationTestCase2<LocalAct
SearchManager searchManager2 = (SearchManager)
mContext.getSystemService(Context.SEARCH_SERVICE);
assertNotNull(searchManager2);
assertSame( searchManager1, searchManager2 );
assertSame(searchManager1, searchManager2 );
}
@MediumTest
public void testSearchables() {
SearchableInfo si;
si = SearchManager.getSearchableInfo(SEARCHABLE_ACTIVITY, false);
assertNotNull(si);
assertFalse(SearchManager.isDefaultSearchable(si));
si = SearchManager.getSearchableInfo(SEARCHABLE_ACTIVITY, true);
assertNotNull(si);
assertTrue(SearchManager.isDefaultSearchable(si));
si = SearchManager.getSearchableInfo(null, true);
assertNotNull(si);
assertTrue(SearchManager.isDefaultSearchable(si));
}
/**
* Tests that rapid calls to start-stop-start doesn't cause problems.
*/
@MediumTest
public void testSearchManagerFastInvocations() throws Exception {
SearchManager searchManager = (SearchManager)
mContext.getSystemService(Context.SEARCH_SERVICE);
assertNotNull(searchManager);
assertSearchNotVisible();
searchManager.startSearch(null, false, SEARCHABLE_ACTIVITY, null, false);
assertSearchVisible();
searchManager.stopSearch();
searchManager.startSearch(null, false, SEARCHABLE_ACTIVITY, null, false);
searchManager.stopSearch();
assertSearchNotVisible();
}
/**
* Tests that startSearch() is idempotent.
*/
@MediumTest
public void testStartSearchIdempotent() throws Exception {
SearchManager searchManager = (SearchManager)
mContext.getSystemService(Context.SEARCH_SERVICE);
assertNotNull(searchManager);
assertSearchNotVisible();
searchManager.startSearch(null, false, SEARCHABLE_ACTIVITY, null, false);
searchManager.startSearch(null, false, SEARCHABLE_ACTIVITY, null, false);
assertSearchVisible();
searchManager.stopSearch();
assertSearchNotVisible();
}
/**
* Tests that stopSearch() is idempotent and can be called when the search UI is not visible.
*/
@MediumTest
public void testStopSearchIdempotent() throws Exception {
SearchManager searchManager = (SearchManager)
mContext.getSystemService(Context.SEARCH_SERVICE);
assertNotNull(searchManager);
assertSearchNotVisible();
searchManager.stopSearch();
assertSearchNotVisible();
searchManager.startSearch(null, false, SEARCHABLE_ACTIVITY, null, false);
assertSearchVisible();
searchManager.stopSearch();
searchManager.stopSearch();
assertSearchNotVisible();
}
/**
* The goal of this test is to confirm that we can start and then
* stop a simple search.
*/
@MediumTest
public void testSearchManagerInvocations() {
@MediumTest
public void testSearchManagerInvocations() throws Exception {
SearchManager searchManager = (SearchManager)
mContext.getSystemService(Context.SEARCH_SERVICE);
assertNotNull(searchManager);
// TODO: make a real component name, or remove this need
final ComponentName cn = new ComponentName("", "");
assertSearchNotVisible();
if (TEST_SEARCH_START != 0) {
// These tests should simply run to completion w/o exceptions
searchManager.startSearch(null, false, cn, null, false);
searchManager.stopSearch();
searchManager.startSearch("", false, cn, null, false);
searchManager.stopSearch();
searchManager.startSearch("test search string", false, cn, null, false);
searchManager.stopSearch();
searchManager.startSearch("test search string", true, cn, null, false);
searchManager.stopSearch();
}
}
// These tests should simply run to completion w/o exceptions
searchManager.startSearch(null, false, SEARCHABLE_ACTIVITY, null, false);
assertSearchVisible();
searchManager.stopSearch();
assertSearchNotVisible();
searchManager.startSearch("", false, SEARCHABLE_ACTIVITY, null, false);
assertSearchVisible();
searchManager.stopSearch();
assertSearchNotVisible();
searchManager.startSearch("test search string", false, SEARCHABLE_ACTIVITY, null, false);
assertSearchVisible();
searchManager.stopSearch();
assertSearchNotVisible();
searchManager.startSearch("test search string", true, SEARCHABLE_ACTIVITY, null, false);
assertSearchVisible();
searchManager.stopSearch();
assertSearchNotVisible();
}
@MediumTest
public void testSearchDialogState() throws Exception {
SearchManager searchManager = (SearchManager)
mContext.getSystemService(Context.SEARCH_SERVICE);
assertNotNull(searchManager);
Bundle searchState;
// search dialog not visible, so no state should be stored
searchState = searchManager.saveSearchDialog();
assertNull(searchState);
searchManager.startSearch("test search string", true, SEARCHABLE_ACTIVITY, null, false);
searchState = searchManager.saveSearchDialog();
assertNotNull(searchState);
searchManager.stopSearch();
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.unit_tests;
import android.app.Activity;
import android.os.Bundle;
public class SearchableActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finish();
}
}

View File

@@ -0,0 +1,110 @@
/*
* 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.unit_tests;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
/** Simple test provider that runs in the local process.
*
* Used by {@link SearchManagerTest}.
*/
public class SuggestionProvider extends ContentProvider {
private static final String TAG = "SuggestionProvider";
private static final int SEARCH_SUGGESTIONS = 1;
private static final UriMatcher sURLMatcher = new UriMatcher(
UriMatcher.NO_MATCH);
static {
sURLMatcher.addURI("*", SearchManager.SUGGEST_URI_PATH_QUERY,
SEARCH_SUGGESTIONS);
sURLMatcher.addURI("*", SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
SEARCH_SUGGESTIONS);
}
private static final String[] COLUMNS = new String[] {
"_id",
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
SearchManager.SUGGEST_COLUMN_QUERY
};
public SuggestionProvider() {
}
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(Uri url, String[] projectionIn, String selection,
String[] selectionArgs, String sort) {
int match = sURLMatcher.match(url);
switch (match) {
case SEARCH_SUGGESTIONS:
String query = url.getLastPathSegment();
MatrixCursor cursor = new MatrixCursor(COLUMNS);
String[] suffixes = { "", "a", " foo", "XXXXXXXXXXXXXXXXX" };
for (String suffix : suffixes) {
addRow(cursor, query + suffix);
}
return cursor;
default:
throw new IllegalArgumentException("Unknown URL: " + url);
}
}
private void addRow(MatrixCursor cursor, String string) {
long id = cursor.getCount();
cursor.newRow().add(id).add(string).add(Intent.ACTION_SEARCH).add(string);
}
@Override
public String getType(Uri url) {
int match = sURLMatcher.match(url);
switch (match) {
case SEARCH_SUGGESTIONS:
return SearchManager.SUGGEST_MIME_TYPE;
default:
throw new IllegalArgumentException("Unknown URL: " + url);
}
}
@Override
public int update(Uri url, ContentValues values, String where, String[] whereArgs) {
throw new UnsupportedOperationException("update not supported");
}
@Override
public Uri insert(Uri url, ContentValues initialValues) {
throw new UnsupportedOperationException("insert not supported");
}
@Override
public int delete(Uri url, String where, String[] whereArgs) {
throw new UnsupportedOperationException("delete not supported");
}
}