OSDN Git Service

Run search dialog in the system process.
authorBjorn Bringert <bringert@android.com>
Fri, 5 Jun 2009 12:22:28 +0000 (13:22 +0100)
committerBjorn Bringert <bringert@android.com>
Mon, 15 Jun 2009 08:05:26 +0000 (09:05 +0100)
Fixes http://b/issue?id=1905863

This is needed to address two security issues with global search:
http://b/issue?id=1871088 (Apps can read content providers through GlobalSearch)
http://b/issue?id=1819627 (Apps can use GlobalSearch to launch arbirtrary intents)

This also fixes http://b/issue?id=1693153 (SearchManager.OnDismissListener
never gets called)

To fix the security issues, GlobalSearch also needs to require
a non-app permission to access its content provider and launch intents.

14 files changed:
Android.mk
core/java/android/app/Activity.java
core/java/android/app/ISearchManager.aidl
core/java/android/app/ISearchManagerCallback.aidl [new file with mode: 0644]
core/java/android/app/SearchDialog.java
core/java/android/app/SearchManager.java
core/java/android/server/search/SearchManagerService.java
core/java/android/server/search/SearchableInfo.java
tests/AndroidTests/AndroidManifest.xml
tests/AndroidTests/res/values/strings.xml
tests/AndroidTests/res/xml/searchable.xml
tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java
tests/AndroidTests/src/com/android/unit_tests/SearchableActivity.java [new file with mode: 0644]
tests/AndroidTests/src/com/android/unit_tests/SuggestionProvider.java [new file with mode: 0644]

index bffd04c..6e292a8 100644 (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 \
index f9b3d05..7fb3449 100644 (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() {
index 374423e..e8bd60a 100644 (file)
 
 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);
 }
diff --git a/core/java/android/app/ISearchManagerCallback.aidl b/core/java/android/app/ISearchManagerCallback.aidl
new file mode 100644 (file)
index 0000000..bdfb2ba
--- /dev/null
@@ -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();
+}
index 7de6572..9141c4c 100644 (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.
index 820f192..1ddd20a 100644 (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
-     */
-    public void onCancel(DialogInterface dialog) {
-        if (dialog == mSearchDialog) {
-            if (mCancelListener != null) {
-                mCancelListener.onCancel();
+
+    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) {
+        throw new UnsupportedOperationException();
+    }
+    public void onDismiss(DialogInterface dialog) {
+        throw new UnsupportedOperationException();
     }
 
     /**
-     * Save instance state so we can recreate after a rotation.
-     * 
+     * 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
      */
-    void saveSearchDialog(Bundle outState, String key) {
-        if (mSearchDialog != null && mSearchDialog.isShowing()) {
-            Bundle searchDialogState = mSearchDialog.onSaveInstanceState();
-            outState.putBundle(key, searchDialogState);
+    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;
         }
     }
 
     /**
-     * Restore instance state after a rotation.
-     * 
+     * Restores the state of the search dialog.
+     *
+     * @param searchDialogState Bundle to read the state from.
+     *
      * @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 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);
         }
     }
-    
+
     /**
-     * Hook for updating layout on a rotation
-     * 
+     * Update the search dialog after a configuration change.
+     *
+     * @param newConfig The new configuration.
+     *
      * @hide
      */
-    void onConfigurationChanged(Configuration newConfig) {
-        if (mSearchDialog != null && mSearchDialog.isShowing()) {
-            mSearchDialog.onConfigurationChanged(newConfig);
+    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);
         }
     }
-    
+
     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() + ")");
+    }
 }
index 060bcea..db812d1 100644 (file)
 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() + ")");
+    }
+
 }
index 4df7368..90dfa0b 100644 (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;
     }
index fd6e6d8..55d4d64 100644 (file)
         </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"
index 21c72cf..49d8ae7 100644 (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>
index a40d53d..9d293b5 100644 (file)
 -->
 
 <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>
\ No newline at end of file
index f3c1542..f03a779 100644 (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("", "");
-
-        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();
-        }
-     }
+        assertSearchNotVisible();
 
-}
+        // 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();
+    }
+
+}
diff --git a/tests/AndroidTests/src/com/android/unit_tests/SearchableActivity.java b/tests/AndroidTests/src/com/android/unit_tests/SearchableActivity.java
new file mode 100644 (file)
index 0000000..53f40e9
--- /dev/null
@@ -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();
+    }
+
+}
diff --git a/tests/AndroidTests/src/com/android/unit_tests/SuggestionProvider.java b/tests/AndroidTests/src/com/android/unit_tests/SuggestionProvider.java
new file mode 100644 (file)
index 0000000..bc61e27
--- /dev/null
@@ -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");
+    }
+}