/* * 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 SEARCH_UI_THREAD_NAME = "SearchDialog"; private static final int SEARCH_UI_THREAD_PRIORITY = android.os.Process.THREAD_PRIORITY_DEFAULT; // 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 // data[KEY_TRIGGER]: 0 = false, 1 = true private static final int MSG_START_SEARCH = 1; // Takes no arguments private static final int MSG_STOP_SEARCH = 2; // arg1 is activity id private static final int MSG_ACTIVITY_RESUMING = 3; // obj is the reason private static final int MSG_CLOSING_SYSTEM_DIALOGS = 4; 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"; private static final String KEY_IDENT = "i"; private static final String KEY_TRIGGER = "t"; // 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; // Identity of last activity that started search. private int mStartedIdent = 0; // Identity of currently resumed activity. private int mResumedIdent = 0; // True if we have registered our receivers. private boolean mReceiverRegistered; private volatile boolean mVisible = false; /** * 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; // 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); } public boolean isVisible() { return mVisible; } /** * 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() { if (!mReceiverRegistered) { IntentFilter filter = new IntentFilter( Intent.ACTION_CONFIGURATION_CHANGED); mContext.registerReceiver(mBroadcastReceiver, filter, null, mSearchUiThread); mReceiverRegistered = true; } } private void unregisterBroadcastReceiver() { if (mReceiverRegistered) { mContext.unregisterReceiver(mBroadcastReceiver); mReceiverRegistered = false; } } /** * 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_CONFIGURATION_CHANGED.equals(action)) { if (DBG) debug(Intent.ACTION_CONFIGURATION_CHANGED); performOnConfigurationChanged(); } } }; // // 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, int ident, boolean trigger) { 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); msgData.putInt(KEY_IDENT, ident); msgData.putInt(KEY_TRIGGER, trigger ? 1 : 0); mSearchUiThread.sendMessage(msg); // be a little more eager in setting this so isVisible will return the correct value if // called immediately after startSearch mVisible = true; } /** * Cancels the search dialog. * Can be called from any thread. */ public void stopSearch() { if (DBG) debug("stopSearch()"); mSearchUiThread.sendEmptyMessage(MSG_STOP_SEARCH); // be a little more eager in setting this so isVisible will return the correct value if // called immediately after stopSearch mVisible = false; } /** * Updates the currently resumed activity. * Can be called from any thread. */ public void activityResuming(int ident) { if (DBG) debug("activityResuming(ident=" + ident + ")"); Message msg = Message.obtain(); msg.what = MSG_ACTIVITY_RESUMING; msg.arg1 = ident; mSearchUiThread.sendMessage(msg); } /** * Handles closing of system windows/dialogs * Can be called from any thread. */ public void closingSystemDialogs(String reason) { if (DBG) debug("closingSystemDialogs(reason=" + reason + ")"); Message msg = Message.obtain(); msg.what = MSG_CLOSING_SYSTEM_DIALOGS; msg.obj = reason; mSearchUiThread.sendMessage(msg); } // // 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_ACTIVITY_RESUMING: performActivityResuming(msg.arg1); break; case MSG_CLOSING_SYSTEM_DIALOGS: performClosingSystemDialogs((String)msg.obj); 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; int ident = msgData.getInt(KEY_IDENT); boolean trigger = msgData.getInt(KEY_TRIGGER) != 0; performStartSearch(initialQuery, selectInitialQuery, launchActivity, appSearchData, globalSearch, searchManagerCallback, ident, trigger); } } /** * 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, int ident, boolean trigger) { if (DBG) debug("performStartSearch()"); registerBroadcastReceiver(); mCallback = searchManagerCallback; // clean up any hidden dialog that we were waiting to resume if (mStartedIdent != 0) { mSearchDialog.dismiss(); } mStartedIdent = ident; if (DBG) Log.v(TAG, "******************* DIALOG: start"); mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData, globalSearch); mVisible = true; if (trigger) { mSearchDialog.launchQuerySearch(); } } /** * Actually cancels the search UI. * This must be called on the search UI thread. */ void performStopSearch() { if (DBG) debug("performStopSearch()"); if (DBG) Log.v(TAG, "******************* DIALOG: cancel"); mSearchDialog.cancel(); mVisible = false; mStartedIdent = 0; } /** * Updates the resumed activity * This must be called on the search UI thread. */ void performActivityResuming(int ident) { if (DBG) debug("performResumingActivity(): mStartedIdent=" + mStartedIdent + ", resuming: " + ident); this.mResumedIdent = ident; if (mStartedIdent != 0) { if (mStartedIdent == mResumedIdent) { // we are resuming into the activity where we previously hid the dialog, bring it // back if (DBG) Log.v(TAG, "******************* DIALOG: show"); mSearchDialog.show(); mVisible = true; } else { // resuming into some other activity; hide ourselves in case we ever come back // so we can show ourselves quickly again if (DBG) Log.v(TAG, "******************* DIALOG: hide"); mSearchDialog.hide(); mVisible = false; } } } /** * Updates due to system dialogs being closed * This must be called on the search UI thread. */ void performClosingSystemDialogs(String reason) { if (DBG) debug("performClosingSystemDialogs(): mStartedIdent=" + mStartedIdent + ", reason: " + reason); if (!"search".equals(reason)) { if (DBG) debug(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); performStopSearch(); } } /** * 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()"); mStartedIdent = 0; mVisible = false; callOnDismiss(); // 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()"); callOnCancel(); } private void callOnDismiss() { if (mCallback == null) return; 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); } } private void callOnCancel() { 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() + ")"); } }