Run search UI on its own thread.

Details:

- Add a new SearchDialogWrapper class that makes sure
  all access to the SearchDialog is run one a single thread
  other than the main ServerThread.
- Don't save/restore seach dialog state in Activity.
  This resulted in lots of calls to the SearchManager
  throughout the life cycle of all activities, for
  the questionable benefit of restoring the search dialog
  in a few cases.
- Remove search UI state save/restore, and the isVisible()
  method from SearchManagerService. They are no longer used,
  and were tricky to implement since they return values from
  the search UI thread to the service.
- Handle configuration changes in searchDialogWrapper instead
  of calling through from Activity.

Fixes http://b/issue?id=1938101

TODO:
- Activity.performPause() calls stopSearch(). This call may not happen
  until the new activity has been started. If the new activity starts a
  search immediately, this search could be cancelled by the old activity's
  call top stopSearch().
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index ca9632a..df50f77 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -606,7 +606,6 @@
     private static final String SAVED_DIALOG_IDS_KEY = "android:savedDialogIds";
     private static final String SAVED_DIALOGS_TAG = "android:savedDialogs";
     private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_";
-    private static final String SAVED_SEARCH_DIALOG_KEY = "android:search_dialog";
 
     private SparseArray<Dialog> mManagedDialogs;
 
@@ -630,7 +629,6 @@
     /*package*/ int mConfigChangeFlags;
     /*package*/ Configuration mCurrentConfig;
     private SearchManager mSearchManager;
-    private Bundle mSearchDialogState = null;
 
     private Window mWindow;
 
@@ -808,13 +806,6 @@
     final void performRestoreInstanceState(Bundle savedInstanceState) {
         onRestoreInstanceState(savedInstanceState);
         restoreManagedDialogs(savedInstanceState);
-        
-        // Also restore the state of a search dialog (if any)
-        // TODO more generic than just this manager
-        Bundle searchState = savedInstanceState.getBundle(SAVED_SEARCH_DIALOG_KEY);
-        if (searchState != null) {
-            mSearchManager.restoreSearchDialog(searchState);
-        }
     }
 
     /**
@@ -1030,14 +1021,6 @@
     final void performSaveInstanceState(Bundle outState) {
         onSaveInstanceState(outState);
         saveManagedDialogs(outState);
-
-        // Also save the state of a search dialog (if any)
-        // TODO more generic than just this manager
-        // 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);
-        }
     }
 
     /**
@@ -1317,10 +1300,6 @@
                 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;
     }
 
     /**
@@ -1341,11 +1320,7 @@
      */
     public void onConfigurationChanged(Configuration newConfig) {
         mCalled = true;
-        
-        // also update search dialog if showing
-        // TODO more generic than just this manager
-        mSearchManager.onConfigurationChanged(newConfig);
-        
+
         if (mWindow != null) {
             // Pass the configuration changed event to the window
             mWindow.onConfigurationChanged(newConfig);
@@ -3575,20 +3550,12 @@
                 "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();
+        // dismiss the search dialog if it is open
         mSearchManager.stopSearch();
     }
     
diff --git a/core/java/android/app/ISearchManager.aidl b/core/java/android/app/ISearchManager.aidl
index e8bd60a..5b62192 100644
--- a/core/java/android/app/ISearchManager.aidl
+++ b/core/java/android/app/ISearchManager.aidl
@@ -36,8 +36,4 @@
             boolean globalSearch,
             ISearchManagerCallback searchManagerCallback);
     void stopSearch();
-    boolean isVisible();
-    Bundle onSaveInstanceState();
-    void onRestoreInstanceState(in Bundle savedInstanceState);
-    void onConfigurationChanged(in Configuration newConfig);
 }
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java
index 2bcc9c3..022a9d9 100644
--- a/core/java/android/app/SearchDialog.java
+++ b/core/java/android/app/SearchDialog.java
@@ -19,13 +19,11 @@
 import static android.app.SuggestionsAdapter.getColumnString;
 
 import android.content.ActivityNotFoundException;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -33,8 +31,8 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.SystemClock;
@@ -95,11 +93,7 @@
 
     private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12;
     private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
-    
-    // interaction with runtime
-    private IntentFilter mCloseDialogsFilter;
-    private IntentFilter mPackageFilter;
-    
+
     // views & widgets
     private TextView mBadgeLabel;
     private ImageView mAppIcon;
@@ -210,15 +204,7 @@
 
         // Touching outside of the search dialog will dismiss it 
         setCanceledOnTouchOutside(true);
-        
-        // Set up broadcast filters
-        mCloseDialogsFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
-        mPackageFilter = new IntentFilter();
-        mPackageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
-        mPackageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
-        mPackageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
-        mPackageFilter.addDataScheme("package");
-        
+
         // Save voice intent for later queries/launching
         mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
         mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -382,15 +368,6 @@
         
         return true;
     }
-    
-    @Override
-    protected void onStart() {
-        super.onStart();
-        
-        // receive broadcasts
-        getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
-        getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
-    }
 
     /**
      * The search dialog is being dismissed, so handle all of the local shutdown operations.
@@ -401,14 +378,7 @@
     @Override
     public void onStop() {
         super.onStop();
-        
-        // stop receiving broadcasts (throws exception if none registered)
-        try {
-            getContext().unregisterReceiver(mBroadcastReceiver);
-        } catch (RuntimeException e) {
-            // This is OK - it just means we didn't have any registered
-        }
-        
+
         closeSuggestionsAdapter();
         
         // dump extra memory we're hanging on to
@@ -455,12 +425,15 @@
     /**
      * Save the minimal set of data necessary to recreate the search
      * 
-     * @return A bundle with the state of the dialog.
+     * @return A bundle with the state of the dialog, or {@code null} if the search
+     *         dialog is not showing.
      */
     @Override
     public Bundle onSaveInstanceState() {
+        if (!isShowing()) return null;
+
         Bundle bundle = new Bundle();
-        
+
         // setup info so I can recreate this particular search       
         bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
         bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
@@ -483,6 +456,8 @@
      */
     @Override
     public void onRestoreInstanceState(Bundle savedInstanceState) {
+        if (savedInstanceState == null) return;
+
         ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
         Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
         boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
@@ -509,7 +484,7 @@
     /**
      * Called after resources have changed, e.g. after screen rotation or locale change.
      */
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigurationChanged() {
         if (isShowing()) {
             // Redraw (resources may have changed)
             updateSearchButton();
@@ -1014,35 +989,11 @@
             return false;
         }
     };
-        
-    /**
-     * When the ACTION_CLOSE_SYSTEM_DIALOGS intent is received, we should close ourselves 
-     * immediately, in order to allow a higher-priority UI to take over
-     * (e.g. phone call received).
-     * 
-     * When a package is added, removed or changed, our current context
-     * may no longer be valid.  This would only happen if a package is installed/removed exactly
-     * when the search bar is open.  So for now we're just going to close the search
-     * bar.  
-     * Anything fancier would require some checks to see if the user's context was still valid.
-     * Which would be messier.
-     */
-    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
-                cancel();
-            } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)
-                    || Intent.ACTION_PACKAGE_REMOVED.equals(action)
-                    || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
-                cancel();
-            }
-        }
-    };
 
     @Override
     public void cancel() {
+        if (!isShowing()) return;
+
         // We made sure the IME was displayed, so also make sure it is closed
         // when we go away.
         InputMethodManager imm = (InputMethodManager)getContext()
diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java
index e5ba6a4..5d25f10 100644
--- a/core/java/android/app/SearchManager.java
+++ b/core/java/android/app/SearchManager.java
@@ -20,7 +20,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.content.res.Configuration;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
@@ -1730,59 +1729,6 @@
     }
 
     /**
-     * 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 mService.onSaveInstanceState();
-        } catch (RemoteException ex) {
-            Log.e(TAG, "onSaveInstanceState() failed: " + ex);
-            return null;
-        }
-    }
-
-    /**
-     * Restores the state of the search dialog.
-     *
-     * @param searchDialogState Bundle to read the state from.
-     *
-     * @hide
-     */
-    public void restoreSearchDialog(Bundle searchDialogState) {
-        if (DBG) debug("restoreSearchDialog(" + searchDialogState + ")");
-        if (searchDialogState == null) return;
-        try {
-            mService.onRestoreInstanceState(searchDialogState);
-        } catch (RemoteException ex) {
-            Log.e(TAG, "onRestoreInstanceState() failed: " + ex);
-        }
-    }
-
-    /**
-     * Update the search dialog after a configuration change.
-     *
-     * @param newConfig The new configuration.
-     *
-     * @hide
-     */
-    public void onConfigurationChanged(Configuration newConfig) {
-        if (DBG) debug("onConfigurationChanged(" + newConfig + "), mIsShowing=" + mIsShowing);
-        if (!mIsShowing) return;
-        try {
-            mService.onConfigurationChanged(newConfig);
-        } catch (RemoteException ex) {
-            Log.e(TAG, "onConfigurationChanged() failed:" + ex);
-        }
-    }
-
-    /**
      * Gets information about a searchable activity. This method is static so that it can
      * be used from non-Activity contexts.
      *
diff --git a/core/java/android/server/search/SearchDialogWrapper.java b/core/java/android/server/search/SearchDialogWrapper.java
new file mode 100644
index 0000000..dbc1e7f
--- /dev/null
+++ b/core/java/android/server/search/SearchDialogWrapper.java
@@ -0,0 +1,320 @@
+/*
+ * 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.server.search;
+
+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.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Runs an instance of {@link SearchDialog} on its own thread.
+ */
+class SearchDialogWrapper
+implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {
+
+    private static final String TAG = "SearchManagerService";
+    private static final boolean DBG = false;
+
+    private static final String DISABLE_SEARCH_PROPERTY = "dev.disablesearchdialog";
+
+    private static final String SEARCH_UI_THREAD_NAME = "SearchDialog";
+    private static final int SEARCH_UI_THREAD_PRIORITY =
+        android.os.Process.THREAD_PRIORITY_FOREGROUND;
+
+    // Takes no arguments
+    private static final int MSG_INIT = 0;
+    // Takes these arguments:
+    // arg1: selectInitialQuery, 0 = false, 1 = true
+    // arg2: globalSearch, 0 = false, 1 = true
+    // obj: searchManagerCallback
+    // data[KEY_INITIAL_QUERY]: initial query
+    // data[KEY_LAUNCH_ACTIVITY]: launch activity
+    // data[KEY_APP_SEARCH_DATA]: app search data
+    private static final int MSG_START_SEARCH = 1;
+    // Takes no arguments
+    private static final int MSG_STOP_SEARCH = 2;
+    // Takes no arguments
+    private static final int MSG_ON_CONFIGURATION_CHANGED = 3;
+
+    private static final String KEY_INITIAL_QUERY = "q";
+    private static final String KEY_LAUNCH_ACTIVITY = "a";
+    private static final String KEY_APP_SEARCH_DATA = "d";
+
+    // Context used for getting search UI resources
+    private final Context mContext;
+
+    // Handles messages on the search UI thread.
+    private final SearchDialogHandler mSearchUiThread;
+
+    // The search UI
+    SearchDialog mSearchDialog;
+
+    // If the search UI is visible, this is the callback for the client that showed it.
+    ISearchManagerCallback mCallback = null;
+
+    // Allows disabling of search dialog for stress testing runs
+    private final boolean mDisabledOnBoot;
+
+    /**
+     * Creates a new search dialog wrapper and a search UI thread. The search dialog itself will
+     * be created some asynchronously on the search UI thread.
+     *
+     * @param context Context used for getting search UI resources.
+     */
+    public SearchDialogWrapper(Context context) {
+        mContext = context;
+
+        mDisabledOnBoot = !TextUtils.isEmpty(SystemProperties.get(DISABLE_SEARCH_PROPERTY));
+
+        // Create the search UI thread
+        HandlerThread t = new HandlerThread(SEARCH_UI_THREAD_NAME, SEARCH_UI_THREAD_PRIORITY);
+        t.start();
+        mSearchUiThread = new SearchDialogHandler(t.getLooper());
+
+        // Create search UI on the search UI thread
+        mSearchUiThread.sendEmptyMessage(MSG_INIT);
+    }
+
+    /**
+     * Initializes the search UI.
+     * Must be called from the search UI thread.
+     */
+    private void init() {
+        mSearchDialog = new SearchDialog(mContext);
+        mSearchDialog.setOnCancelListener(this);
+        mSearchDialog.setOnDismissListener(this);
+    }
+
+    private void registerBroadcastReceiver() {
+        IntentFilter closeDialogsFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        mContext.registerReceiver(mBroadcastReceiver, closeDialogsFilter);
+        IntentFilter configurationChangedFilter =
+                new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
+        mContext.registerReceiver(mBroadcastReceiver, configurationChangedFilter);
+    }
+
+    private void unregisterBroadcastReceiver() {
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
+
+    /**
+     * Closes the search dialog when requested by the system (e.g. when a phone call comes in).
+     */
+    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
+                if (DBG) debug(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+                stopSearch();
+            } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) {
+                if (DBG) debug(Intent.ACTION_CONFIGURATION_CHANGED);
+                onConfigurationChanged();
+            }
+        }
+    };
+
+    //
+    // External API
+    //
+
+    /**
+     * Launches the search UI.
+     * Can be called from any thread.
+     *
+     * @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()");
+        Message msg = Message.obtain();
+        msg.what = MSG_START_SEARCH;
+        msg.arg1 = selectInitialQuery ? 1 : 0;
+        msg.arg2 = globalSearch ? 1 : 0;
+        msg.obj = searchManagerCallback;
+        Bundle msgData = msg.getData();
+        msgData.putString(KEY_INITIAL_QUERY, initialQuery);
+        msgData.putParcelable(KEY_LAUNCH_ACTIVITY, launchActivity);
+        msgData.putBundle(KEY_APP_SEARCH_DATA, appSearchData);
+        mSearchUiThread.sendMessage(msg);
+    }
+
+    /**
+     * Cancels the search dialog.
+     * Can be called from any thread.
+     */
+    public void stopSearch() {
+        if (DBG) debug("stopSearch()");
+        mSearchUiThread.sendEmptyMessage(MSG_STOP_SEARCH);
+    }
+
+    /**
+     * Updates the search UI in response to a configuration change.
+     * Can be called from any thread.
+     */
+    void onConfigurationChanged() {
+        if (DBG) debug("onConfigurationChanged()");
+        mSearchUiThread.sendEmptyMessage(MSG_ON_CONFIGURATION_CHANGED);
+    }
+
+    //
+    // Implementation methods that run on the search UI thread
+    //
+
+    private class SearchDialogHandler extends Handler {
+
+        public SearchDialogHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_INIT:
+                    init();
+                    break;
+                case MSG_START_SEARCH:
+                    handleStartSearchMessage(msg);
+                    break;
+                case MSG_STOP_SEARCH:
+                    performStopSearch();
+                    break;
+                case MSG_ON_CONFIGURATION_CHANGED:
+                    performOnConfigurationChanged();
+                    break;
+            }
+        }
+
+        private void handleStartSearchMessage(Message msg) {
+            Bundle msgData = msg.getData();
+            String initialQuery = msgData.getString(KEY_INITIAL_QUERY);
+            boolean selectInitialQuery = msg.arg1 != 0;
+            ComponentName launchActivity =
+                    (ComponentName) msgData.getParcelable(KEY_LAUNCH_ACTIVITY);
+            Bundle appSearchData = msgData.getBundle(KEY_APP_SEARCH_DATA);
+            boolean globalSearch = msg.arg2 != 0;
+            ISearchManagerCallback searchManagerCallback = (ISearchManagerCallback) msg.obj;
+            performStartSearch(initialQuery, selectInitialQuery, launchActivity,
+                    appSearchData, globalSearch, searchManagerCallback);
+        }
+
+    }
+
+    /**
+     * Actually launches the search UI.
+     * This must be called on the search UI thread.
+     */
+    void performStartSearch(String initialQuery,
+            boolean selectInitialQuery,
+            ComponentName launchActivity,
+            Bundle appSearchData,
+            boolean globalSearch,
+            ISearchManagerCallback searchManagerCallback) {
+        if (DBG) debug("performStartSearch()");
+
+        if (mDisabledOnBoot) {
+            Log.d(TAG, "ignoring start search request because " + DISABLE_SEARCH_PROPERTY
+                    + " system property is set.");
+            return;
+        }
+
+        registerBroadcastReceiver();
+        mCallback = searchManagerCallback;
+        mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData,
+                globalSearch);
+    }
+
+    /**
+     * Actually cancels the search UI.
+     * This must be called on the search UI thread.
+     */
+    void performStopSearch() {
+        if (DBG) debug("performStopSearch()");
+        mSearchDialog.cancel();
+    }
+
+    /**
+     * Must be called from the search UI thread.
+     */
+    void performOnConfigurationChanged() {
+        if (DBG) debug("performOnConfigurationChanged()");
+        mSearchDialog.onConfigurationChanged();
+    }
+
+    /**
+     * Called by {@link SearchDialog} when it goes away.
+     */
+    public void onDismiss(DialogInterface dialog) {
+        if (DBG) debug("onDismiss()");
+        if (mCallback != null) {
+            try {
+                // should be safe to do on the search UI thread, since it's a oneway interface
+                mCallback.onDismiss();
+            } catch (DeadObjectException ex) {
+                // The process that hosted the callback has died, do nothing
+            } catch (RemoteException ex) {
+                Log.e(TAG, "onDismiss() failed: " + ex);
+            }
+            // we don't need the callback anymore, release it
+            mCallback = null;
+        }
+        unregisterBroadcastReceiver();
+    }
+
+    /**
+     * Called by {@link SearchDialog} when the user or activity cancels search.
+     * Whenever this method is called, {@link #onDismiss} is always called afterwards.
+     */
+    public void onCancel(DialogInterface dialog) {
+        if (DBG) debug("onCancel()");
+        if (mCallback != null) {
+            try {
+                // should be safe to do on the search UI thread, since it's a oneway interface
+                mCallback.onCancel();
+            } catch (DeadObjectException ex) {
+                // The process that hosted the callback has died, do nothing
+            } catch (RemoteException ex) {
+                Log.e(TAG, "onCancel() failed: " + ex);
+            }
+        }
+    }
+
+    private static void debug(String msg) {
+        Thread thread = Thread.currentThread();
+        Log.d(TAG, msg + " (" + thread.getName() + "-" + thread.getId() + ")");
+    }
+}
diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java
index 373e61f..87adfb3 100644
--- a/core/java/android/server/search/SearchManagerService.java
+++ b/core/java/android/server/search/SearchManagerService.java
@@ -18,52 +18,38 @@
 
 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.os.SystemProperties;
-import android.text.TextUtils;
 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
- * presentation (UI).  Its function is to maintain the map & list of "searchable"
- * items, which provides a mapping from individual activities (where a user might have
- * invoked search) to specific searchable activities (where the search will be dispatched).
+ * The search manager service handles the search UI, and maintains a registry of searchable
+ * activities.
  */
-public class SearchManagerService extends ISearchManager.Stub
-        implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener
-{
-        // general debugging support
+public class SearchManagerService extends ISearchManager.Stub {
+
+    // general debugging support
     private static final String TAG = "SearchManagerService";
     private static final boolean DBG = false;
 
-        // class maintenance and general shared data
+    // Context that the service is running in.
     private final Context mContext;
-    private final Handler mHandler;
-    private boolean mSearchablesDirty;
-    private final Searchables mSearchables;
 
-    final SearchDialog mSearchDialog;
-    ISearchManagerCallback mCallback = null;
+    // This field is initialized in initialize(), and then never modified.
+    // It is volatile since it can be accessed by multiple threads.
+    private volatile Searchables mSearchables;
 
-    private final boolean mDisabledOnBoot;
-
-    private static final String DISABLE_SEARCH_PROPERTY = "dev.disablesearchdialog";
+    // This field is initialized in initialize(), and then never modified.
+    // It is volatile since it can be accessed by multiple threads.
+    private volatile SearchDialogWrapper mSearchDialog;
 
     /**
      * Initializes the Search Manager service in the provided system context.
@@ -73,82 +59,71 @@
      */
     public SearchManagerService(Context context)  {
         mContext = context;
-        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.
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(Intent.ACTION_PACKAGE_ADDED);
-        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
-        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
-        filter.addDataScheme("package");
-        mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
-
-        // After startup settles down, preload the searchables list,
-        // which will reduce the delay when the search UI is invoked.
-        mHandler.post(mRunUpdateSearchable);
-
-        // allows disabling of search dialog for stress testing runs
-        mDisabledOnBoot = !TextUtils.isEmpty(SystemProperties.get(DISABLE_SEARCH_PROPERTY));
+        // call initialize() after all pending actions on the main system thread have finished
+        new Handler().post(new Runnable() {
+            public void run() {
+                initialize();
+            }
+        });
     }
 
     /**
-     * Listens for intent broadcasts.
-     *
-     * The primary purpose here is to refresh the "searchables" list
-     * if packages are added/removed.
+     * Initializes the search UI and the list of searchable activities.
      */
-    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+    void initialize() {
+        mSearchables = createSearchables();
+        mSearchDialog = new SearchDialogWrapper(mContext);
+    }
+
+    private Searchables createSearchables() {
+        Searchables searchables = new Searchables(mContext);
+        searchables.buildSearchableList();
+
+        IntentFilter packageFilter = new IntentFilter();
+        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        packageFilter.addDataScheme("package");
+        mContext.registerReceiver(mPackageChangedReceiver, packageFilter);
+
+        return searchables;
+    }
+
+    /**
+     * Refreshes the "searchables" list when packages are added/removed.
+     */
+    private BroadcastReceiver mPackageChangedReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
 
-            // First, test for intents that matter at any time
-            if (action.equals(Intent.ACTION_PACKAGE_ADDED) ||
-                action.equals(Intent.ACTION_PACKAGE_REMOVED) ||
-                action.equals(Intent.ACTION_PACKAGE_CHANGED)) {
-                mSearchablesDirty = true;
-                mHandler.post(mRunUpdateSearchable);
-                return;
+            if (Intent.ACTION_PACKAGE_ADDED.equals(action) ||
+                    Intent.ACTION_PACKAGE_REMOVED.equals(action) ||
+                    Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+                if (DBG) Log.d(TAG, "Got " + action);
+                // Dismiss search dialog, since the search context may no longer be valid
+                mSearchDialog.stopSearch();
+                // Update list of searchable activities
+                mSearchables.buildSearchableList();
+                broadcastSearchablesChanged();
             }
         }
     };
 
     /**
-     * This runnable (for the main handler / UI thread) will update the searchables list.
+     * Informs all listeners that the list of searchables has been updated.
      */
-    private Runnable mRunUpdateSearchable = new Runnable() {
-        public void run() {
-            updateSearchablesIfDirty();
-        }
-    };
-
-    /**
-     * Updates the list of searchables, either at startup or in response to
-     * a package add/remove broadcast message.
-     */
-    private void updateSearchables() {
-        if (DBG) debug("updateSearchables()");
-        mSearchables.buildSearchableList();
-        mSearchablesDirty = false;
+    void broadcastSearchablesChanged() {
+        mContext.sendBroadcast(
+                new Intent(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED));
     }
 
-    /**
-     * Updates the list of searchables if needed.
-     */
-    private void updateSearchablesIfDirty() {
-        if (mSearchablesDirty) {
-            updateSearchables();
-        }
-    }
+    //
+    // Searchable activities API
+    //
 
     /**
-     * Returns the SearchableInfo for a given activity
+     * 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
@@ -158,226 +133,84 @@
      * @return Returns a SearchableInfo record describing the parameters of the search,
      * or null if no searchable metadata was available.
      */
-    public SearchableInfo getSearchableInfo(ComponentName launchActivity, boolean globalSearch) {
-        updateSearchablesIfDirty();
-        SearchableInfo si = null;
+    public SearchableInfo getSearchableInfo(final ComponentName launchActivity,
+            final boolean globalSearch) {
+        if (mSearchables == null) return null;
         if (globalSearch) {
-            si = mSearchables.getDefaultSearchable();
+            return mSearchables.getDefaultSearchable();
         } else {
             if (launchActivity == null) {
                 Log.e(TAG, "getSearchableInfo(), activity == null");
                 return null;
             }
-            si = mSearchables.getSearchableInfo(launchActivity);
+            return mSearchables.getSearchableInfo(launchActivity);
         }
-
-        return si;
     }
 
     /**
      * Returns a list of the searchable activities that can be included in global search.
      */
     public List<SearchableInfo> getSearchablesInGlobalSearch() {
-        updateSearchablesIfDirty();
+        if (mSearchables == null) return null;
         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.
+     * Returns a list of the searchable activities that handle web searches.
+     * Can be called from any thread.
      */
-    /*package*/ void performStartSearch(String initialQuery,
+    public List<SearchableInfo> getSearchablesForWebSearch() {
+        if (mSearchables == null) return null;
+        return mSearchables.getSearchablesForWebSearchList();
+    }
+
+    /**
+     * Returns the default searchable activity for web searches.
+     * Can be called from any thread.
+     */
+    public SearchableInfo getDefaultSearchableForWebSearch() {
+        if (mSearchables == null) return null;
+        return mSearchables.getDefaultSearchableForWebSearch();
+    }
+
+    /**
+     * Sets the default searchable activity for web searches.
+     * Can be called from any thread.
+     */
+    public void setDefaultWebSearch(final ComponentName component) {
+        if (mSearchables == null) return;
+        mSearchables.setDefaultWebSearch(component);
+        broadcastSearchablesChanged();
+    }
+
+    // Search UI API
+
+    /**
+     * Launches the search UI. Can be called from any thread.
+     *
+     * @see SearchManager#startSearch(String, boolean, ComponentName, Bundle, boolean)
+     */
+    public void startSearch(String initialQuery,
             boolean selectInitialQuery,
             ComponentName launchActivity,
             Bundle appSearchData,
             boolean globalSearch,
             ISearchManagerCallback searchManagerCallback) {
-        if (DBG) debug("performStartSearch()");
-
-        if (mDisabledOnBoot) {
-            Log.d(TAG, "ignoring start search request because " + DISABLE_SEARCH_PROPERTY
-                    + " system property is set.");
-            return;
-        }
-
-        mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData,
-                globalSearch);
-        if (searchManagerCallback != null) {
-            mCallback = searchManagerCallback;
-        }
+        if (mSearchDialog == null) return;
+        mSearchDialog.startSearch(initialQuery,
+                selectInitialQuery,
+                launchActivity,
+                appSearchData,
+                globalSearch,
+                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.
-     */
-    public List<SearchableInfo> getSearchablesForWebSearch() {
-        updateSearchablesIfDirty();
-        return mSearchables.getSearchablesForWebSearchList();
-    }
-
-    /**
-     * Returns the default searchable activity for web searches.
-     */
-    public SearchableInfo getDefaultSearchableForWebSearch() {
-        updateSearchablesIfDirty();
-        return mSearchables.getDefaultSearchableForWebSearch();
-    }
-
-    /**
-     * Sets the default searchable activity for web searches.
-     */
-    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() + ")");
+        if (mSearchDialog == null) return;
+        mSearchDialog.stopSearch();
     }
 
 }
diff --git a/core/java/android/server/search/Searchables.java b/core/java/android/server/search/Searchables.java
index c7cc8ed..b959907 100644
--- a/core/java/android/server/search/Searchables.java
+++ b/core/java/android/server/search/Searchables.java
@@ -17,7 +17,6 @@
 package android.server.search;
 
 import com.android.internal.app.ResolverActivity;
-import com.android.internal.R;
 
 import android.app.SearchManager;
 import android.content.ComponentName;
@@ -27,7 +26,6 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
 import android.os.Bundle;
 import android.util.Log;
 
@@ -264,7 +262,7 @@
         }
 
         // Find the default web search provider.
-        ComponentName webSearchActivity = getPreferredWebSearchActivity();
+        ComponentName webSearchActivity = getPreferredWebSearchActivity(mContext);
         SearchableInfo newDefaultSearchableForWebSearch = null;
         if (webSearchActivity != null) {
             newDefaultSearchableForWebSearch = newSearchablesMap.get(webSearchActivity);
@@ -283,9 +281,6 @@
             mDefaultSearchable = newDefaultSearchable;
             mDefaultSearchableForWebSearch = newDefaultSearchableForWebSearch;
         }
-
-        // Inform all listeners that the list of searchables has been updated.
-        mContext.sendBroadcast(new Intent(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED));
     }
 
     /**
@@ -295,9 +290,10 @@
      * @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 boolean setPreferredActivity(ComponentName component, String action) {
+    private static boolean setPreferredActivity(Context context,
+            ComponentName component, String action) {
         Log.d(LOG_TAG, "Checking component " + component);
-        PackageManager pm = mContext.getPackageManager();
+        PackageManager pm = context.getPackageManager();
         ActivityInfo ai;
         try {
             ai = pm.getActivityInfo(component, 0);
@@ -326,10 +322,10 @@
         return true;
     }
 
-    public ComponentName getPreferredWebSearchActivity() {
+    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 = mContext.getPackageManager();
+        PackageManager pm = context.getPackageManager();
         ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
 
         if (ri == null || ri.activityInfo.name.equals(ResolverActivity.class.getName())) {
@@ -338,11 +334,11 @@
             // 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 = mContext.getResources().getStringArray(
+            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(component, Intent.ACTION_WEB_SEARCH)) {
+                if (setPreferredActivity(context, component, Intent.ACTION_WEB_SEARCH)) {
                     return component;
                 }
             }
@@ -354,7 +350,8 @@
             if (cn.flattenToShortString().equals(GOOGLE_SEARCH_COMPONENT_NAME)) {
                 ComponentName enhancedGoogleSearch = ComponentName.unflattenFromString(
                         ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME);
-                if (setPreferredActivity(enhancedGoogleSearch, Intent.ACTION_WEB_SEARCH)) {
+                if (setPreferredActivity(context, enhancedGoogleSearch,
+                        Intent.ACTION_WEB_SEARCH)) {
                     return enhancedGoogleSearch;
                 }
             }
@@ -397,7 +394,7 @@
      * Sets the default searchable activity for web searches.
      */
     public synchronized void setDefaultWebSearch(ComponentName component) {
-        setPreferredActivity(component, Intent.ACTION_WEB_SEARCH);
+        setPreferredActivity(mContext, component, Intent.ACTION_WEB_SEARCH);
         buildSearchableList();
     }
 }
diff --git a/tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java b/tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java
index c4f1ab6..4c5fefc 100644
--- a/tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java
+++ b/tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java
@@ -107,8 +107,6 @@
     }
 
     // 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);
@@ -245,22 +243,4 @@
         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();
-    }
-
 }