SearchView API

Implements the basic requirements for in-app search. More work to be done.
diff --git a/api/current.xml b/api/current.xml
index 819c730..e28bb58 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -4926,6 +4926,17 @@
  visibility="public"
 >
 </field>
+<field name="iconifiedByDefault"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="16843579"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
 <field name="id"
  type="int"
  transient="false"
@@ -233481,6 +233492,228 @@
 >
 </method>
 </class>
+<class name="SearchView"
+ extends="android.widget.LinearLayout"
+ abstract="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<constructor name="SearchView"
+ type="android.widget.SearchView"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="context" type="android.content.Context">
+</parameter>
+</constructor>
+<constructor name="SearchView"
+ type="android.widget.SearchView"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="context" type="android.content.Context">
+</parameter>
+<parameter name="attrs" type="android.util.AttributeSet">
+</parameter>
+</constructor>
+<method name="getSuggestionsAdapter"
+ return="android.widget.CursorAdapter"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="isIconfiedByDefault"
+ return="boolean"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="isSubmitButtonEnabled"
+ return="boolean"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="setIconifiedByDefault"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="iconified" type="boolean">
+</parameter>
+</method>
+<method name="setOnQueryChangeListener"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="listener" type="android.widget.SearchView.OnQueryChangeListener">
+</parameter>
+</method>
+<method name="setQuery"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="query" type="java.lang.CharSequence">
+</parameter>
+<parameter name="submit" type="boolean">
+</parameter>
+</method>
+<method name="setQueryHint"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="hint" type="java.lang.CharSequence">
+</parameter>
+</method>
+<method name="setSearchableInfo"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="searchable" type="android.app.SearchableInfo">
+</parameter>
+</method>
+<method name="setSubmitButtonEnabled"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="enabled" type="boolean">
+</parameter>
+</method>
+<method name="setSuggestionsAdapter"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="adapter" type="android.widget.CursorAdapter">
+</parameter>
+</method>
+</class>
+<interface name="SearchView.FilterableListAdapter"
+ abstract="true"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<implements name="android.widget.Filterable">
+</implements>
+<implements name="android.widget.ListAdapter">
+</implements>
+</interface>
+<interface name="SearchView.OnCloseListener"
+ abstract="true"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<method name="onClose"
+ return="boolean"
+ abstract="true"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+</interface>
+<interface name="SearchView.OnQueryChangeListener"
+ abstract="true"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<method name="onQueryTextChanged"
+ return="boolean"
+ abstract="true"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="newText" type="java.lang.String">
+</parameter>
+</method>
+<method name="onSubmitQuery"
+ return="boolean"
+ abstract="true"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="query" type="java.lang.String">
+</parameter>
+</method>
+</interface>
 <interface name="SectionIndexer"
  abstract="true"
  static="false"
diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java
new file mode 100644
index 0000000..fe1c9da
--- /dev/null
+++ b/core/java/android/widget/SearchView.java
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2010 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.widget;
+
+import static android.widget.SuggestionsAdapter.getColumnString;
+
+import com.android.internal.R;
+
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.TextView.OnEditorActionListener;
+
+import java.util.WeakHashMap;
+
+/**
+ * Provides the user interface elements for the user to enter a search query and submit a
+ * request to a search provider. Shows a list of query suggestions or results, if
+ * available and allows the user to pick a suggestion or result to launch into.
+ */
+public class SearchView extends LinearLayout {
+
+    private static final boolean DBG = false;
+    private static final String LOG_TAG = "SearchView";
+
+    private OnQueryChangeListener mOnQueryChangeListener;
+    private OnCloseListener mOnCloseListener;
+
+    private boolean mIconifiedByDefault;
+    private CursorAdapter mSuggestionsAdapter;
+    private View mSearchButton;
+    private View mSubmitButton;
+    private View mCloseButton;
+    private View mSearchEditFrame;
+    private AutoCompleteTextView mQueryTextView;
+    private boolean mSubmitButtonEnabled;
+    private CharSequence mQueryHint;
+
+    private SearchableInfo mSearchable;
+
+    // A weak map of drawables we've gotten from other packages, so we don't load them
+    // more than once.
+    private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
+            new WeakHashMap<String, Drawable.ConstantState>();
+
+    /**
+     * Callbacks for changes to the query text.
+     */
+    public interface OnQueryChangeListener {
+
+        /**
+         * Called when the user submits the query. This could be due to a key press on the
+         * keyboard or due to pressing a submit button.
+         * The listener can override the standard behavior by returning true
+         * to indicate that it has handled the submit request. Otherwise return false to
+         * let the SearchView handle the submission by launching any associated intent.
+         *
+         * @param query the query text that is to be submitted
+         *
+         * @return true if the query has been handled by the listener, false to let the
+         * SearchView perform the default action.
+         */
+        boolean onSubmitQuery(String query);
+
+        /**
+         * Called when the query text is changed by the user.
+         *
+         * @param newText the new content of the query text field.
+         *
+         * @return false if the SearchView should perform the default action of showing any
+         * suggestions if available, true if the action was handled by the listener.
+         */
+        boolean onQueryTextChanged(String newText);
+    }
+
+    public interface OnCloseListener {
+
+        /**
+         * The user is attempting to close the SearchView.
+         *
+         * @return true if the listener wants to override the default behavior of clearing the
+         * text field and dismissing it, false otherwise.
+         */
+        boolean onClose();
+    }
+
+    public SearchView(Context context) {
+        this(context, null);
+    }
+
+    public SearchView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        LayoutInflater inflater = (LayoutInflater) context
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(R.layout.search_view, this, true);
+
+        mSearchButton = findViewById(R.id.search_button);
+        mQueryTextView = (AutoCompleteTextView) findViewById(R.id.search_src_text);
+        mSearchEditFrame = findViewById(R.id.search_edit_frame);
+        mSubmitButton = findViewById(R.id.search_go_btn);
+        mCloseButton = findViewById(R.id.search_close_btn);
+
+        mSearchButton.setOnClickListener(mOnClickListener);
+        mCloseButton.setOnClickListener(mOnClickListener);
+        mSubmitButton.setOnClickListener(mOnClickListener);
+        mQueryTextView.addTextChangedListener(mTextWatcher);
+        mQueryTextView.setOnEditorActionListener(mOnEditorActionListener);
+        mQueryTextView.setOnItemClickListener(mOnItemClickListener);
+        mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        mSubmitButtonEnabled = false;
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0);
+        setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
+        a.recycle();
+
+        updateViewsVisibility(mIconifiedByDefault);
+    }
+
+    /**
+     * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
+     * to display labels, hints, suggestions, create intents for launching search results screens
+     * and controlling other affordances such as a voice button.
+     *
+     * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
+     * activity or a global search provider.
+     */
+    public void setSearchableInfo(SearchableInfo searchable) {
+        mSearchable = searchable;
+        if (mSearchable != null) {
+            updateSearchAutoComplete();
+        }
+        updateViewsVisibility(mIconifiedByDefault);
+    }
+
+    /**
+     * Sets a listener for user actions within the SearchView.
+     *
+     * @param listener the listener object that receives callbacks when the user performs
+     * actions in the SearchView such as clicking on buttons or typing a query.
+     */
+    public void setOnQueryChangeListener(OnQueryChangeListener listener) {
+        mOnQueryChangeListener = listener;
+    }
+
+    /**
+     * Sets a query string in the text field and optionally submits the query as well.
+     *
+     * @param query the query string. This replaces any query text already present in the
+     * text field.
+     * @param submit whether to submit the query right now or only update the contents of
+     * text field.
+     */
+    public void setQuery(CharSequence query, boolean submit) {
+        mQueryTextView.setText(query);
+        // If the query is not empty and submit is requested, submit the query
+        if (submit && !TextUtils.isEmpty(query)) {
+            onSubmitQuery();
+        }
+    }
+
+    /**
+     * Sets the hint text to display in the query text field. This overrides any hint specified
+     * in the SearchableInfo.
+     *
+     * @param hint the hint text to display
+     */
+    public void setQueryHint(CharSequence hint) {
+        mQueryHint = hint;
+        updateQueryHint();
+    }
+
+    /**
+     * Sets the default or resting state of the search field. If true, a single search icon is
+     * shown by default and expands to show the text field and other buttons when pressed. Also,
+     * if the default state is iconified, then it collapses to that state when the close button
+     * is pressed.
+     *
+     * @param iconified
+     */
+    public void setIconifiedByDefault(boolean iconified) {
+        mIconifiedByDefault = iconified;
+        updateViewsVisibility(iconified);
+    }
+
+    public boolean isIconfiedByDefault() {
+        return mIconifiedByDefault;
+    }
+
+    /**
+     * Enables showing a submit button when the query is non-empty. In cases where the SearchView
+     * is being used to filter the contents of the current activity and doesn't launch a separate
+     * results activity, then the submit button should be disabled.
+     *
+     * @param enabled true to show a submit button for submitting queries, false if a submit
+     * button is not required.
+     */
+    public void setSubmitButtonEnabled(boolean enabled) {
+        mSubmitButton.setVisibility(enabled ? VISIBLE : GONE);
+        mSubmitButtonEnabled = enabled;
+    }
+
+    /**
+     * Returns whether the submit button is enabled when necessary or never displayed.
+     *
+     * @return whether the submit button is enabled automatically when necessary
+     */
+    public boolean isSubmitButtonEnabled() {
+        return mSubmitButtonEnabled;
+    }
+
+    public interface FilterableListAdapter extends ListAdapter, Filterable {
+    }
+
+    /**
+     * You can set a custom adapter if you wish. Otherwise the default adapter is used to
+     * display the suggestions from the suggestions provider associated with the SearchableInfo.
+     *
+     * @see #setSearchableInfo(SearchableInfo)
+     */
+    public void setSuggestionsAdapter(CursorAdapter adapter) {
+        mSuggestionsAdapter = adapter;
+
+        mQueryTextView.setAdapter(mSuggestionsAdapter);
+    }
+
+    /**
+     * Returns the adapter used for suggestions, if any.
+     * @return the suggestions adapter
+     */
+    public CursorAdapter getSuggestionsAdapter() {
+        return mSuggestionsAdapter;
+    }
+
+    private void updateViewsVisibility(boolean collapsed) {
+        // Visibility of views that are visible when collapsed
+        int visCollapsed = collapsed? VISIBLE : GONE;
+        // Visibility of views that are visible when expanded
+        int visExpanded = collapsed? GONE : VISIBLE;
+
+        mSearchButton.setVisibility(visCollapsed);
+        mSubmitButton.setVisibility(mSubmitButtonEnabled ? visExpanded : GONE);
+        mSearchEditFrame.setVisibility(visExpanded);
+
+        setImeVisibility(!collapsed);
+    }
+
+    private void setImeVisibility(boolean visible) {
+        // We made sure the IME was displayed, so also make sure it is closed
+        // when we go away.
+        InputMethodManager imm = (InputMethodManager)
+                getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (imm != null) {
+            if (visible) {
+                imm.showSoftInputUnchecked(0, null);
+            } else {
+                imm.hideSoftInputFromWindow(getWindowToken(), 0);
+            }
+        }
+    }
+
+    private final OnClickListener mOnClickListener = new OnClickListener() {
+
+        public void onClick(View v) {
+            if (v == mSearchButton) {
+                onSearchClicked();
+            } else if (v == mCloseButton) {
+                onCloseClicked();
+            } else if (v == mSubmitButton) {
+                onSubmitQuery();
+            }
+        }
+    };
+
+    /**
+     * Handles the key down event for dealing with action keys.
+     *
+     * @param keyCode This is the keycode of the typed key, and is the same value as
+     *        found in the KeyEvent parameter.
+     * @param event The complete event record for the typed key
+     *
+     * @return true if the event was handled here, or false if not.
+     */
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (mSearchable == null) {
+            return false;
+        }
+
+        // if it's an action specified by the searchable activity, launch the
+        // entered query with the action key
+        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+        if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
+            launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText().toString());
+            return true;
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+
+    private void updateQueryHint() {
+        if (mQueryHint != null) {
+            mQueryTextView.setHint(mQueryHint);
+        } else if (mSearchable != null) {
+            CharSequence hint = null;
+            int hintId = mSearchable.getHintId();
+            if (hintId != 0) {
+                hint = getContext().getString(hintId);
+            }
+            if (hint != null) {
+                mQueryTextView.setHint(hint);
+            }
+        }
+    }
+
+    /**
+     * Updates the auto-complete text view.
+     */
+    private void updateSearchAutoComplete() {
+        // close any existing suggestions adapter
+        //closeSuggestionsAdapter();
+
+        mQueryTextView.setDropDownAnimationStyle(0); // no animation
+
+        // attach the suggestions adapter, if suggestions are available
+        // The existence of a suggestions authority is the proxy for "suggestions available here"
+        if (mSearchable.getSuggestAuthority() != null) {
+            mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
+                    this, mSearchable, mOutsideDrawablesCache);
+            mQueryTextView.setAdapter(mSuggestionsAdapter);
+        }
+    }
+
+    private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
+
+        /**
+         * Called when the input method default action key is pressed.
+         */
+        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+            onSubmitQuery();
+            return true;
+        }
+    };
+
+    private void onTextChanged(CharSequence newText) {
+        CharSequence text = mQueryTextView.getText();
+        boolean hasText = !TextUtils.isEmpty(text);
+        if (isSubmitButtonEnabled()) {
+            mSubmitButton.setVisibility(hasText ? VISIBLE : GONE);
+        }
+        if (mOnQueryChangeListener != null)
+            mOnQueryChangeListener.onQueryTextChanged(newText.toString());
+    }
+
+    private void onSubmitQuery() {
+        CharSequence query = mQueryTextView.getText();
+        if (!TextUtils.isEmpty(query)) {
+            if (mOnQueryChangeListener == null
+                    || !mOnQueryChangeListener.onSubmitQuery(query.toString())) {
+                if (mSearchable != null) {
+                    launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
+                }
+            }
+        }
+    }
+
+    private void onCloseClicked() {
+        if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
+            mQueryTextView.setText("");
+            updateViewsVisibility(mIconifiedByDefault);
+        }
+    }
+
+    private void onSearchClicked() {
+        mQueryTextView.requestFocus();
+        updateViewsVisibility(false);
+    }
+
+    private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
+
+        /**
+         * Implements OnItemClickListener
+         */
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            if (DBG)
+                Log.d(LOG_TAG, "onItemClick() position " + position);
+            launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
+        }
+    };
+
+    private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
+
+        /**
+         * Implements OnItemSelectedListener
+         */
+        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+            if (DBG)
+                Log.d(LOG_TAG, "onItemSelected() position " + position);
+            // A suggestion has been selected, rewrite the query if possible,
+            // otherwise the restore the original query.
+            rewriteQueryFromSuggestion(position);
+        }
+
+        /**
+         * Implements OnItemSelectedListener
+         */
+        public void onNothingSelected(AdapterView<?> parent) {
+            if (DBG)
+                Log.d(LOG_TAG, "onNothingSelected()");
+        }
+    };
+
+    /**
+     * Query rewriting.
+     */
+    private void rewriteQueryFromSuggestion(int position) {
+        CharSequence oldQuery = mQueryTextView.getText();
+        Cursor c = mSuggestionsAdapter.getCursor();
+        if (c == null) {
+            return;
+        }
+        if (c.moveToPosition(position)) {
+            // Get the new query from the suggestion.
+            CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
+            if (newQuery != null) {
+                // The suggestion rewrites the query.
+                // Update the text field, without getting new suggestions.
+                setQuery(newQuery);
+            } else {
+                // The suggestion does not rewrite the query, restore the user's query.
+                setQuery(oldQuery);
+            }
+        } else {
+            // We got a bad position, restore the user's query.
+            setQuery(oldQuery);
+        }
+    }
+
+    /**
+     * Launches an intent based on a suggestion.
+     *
+     * @param position The index of the suggestion to create the intent from.
+     * @param actionKey The key code of the action key that was pressed,
+     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+     * @param actionMsg The message for the action key that was pressed,
+     *        or <code>null</code> if none.
+     * @return true if a successful launch, false if could not (e.g. bad position).
+     */
+    private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
+        Cursor c = mSuggestionsAdapter.getCursor();
+        if ((c != null) && c.moveToPosition(position)) {
+
+            Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
+
+            // launch the intent
+            launchIntent(intent);
+
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Launches an intent, including any special intent handling.
+     */
+    private void launchIntent(Intent intent) {
+        if (intent == null) {
+            return;
+        }
+        try {
+            // If the intent was created from a suggestion, it will always have an explicit
+            // component here.
+            getContext().startActivity(intent);
+        } catch (RuntimeException ex) {
+            Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
+        }
+    }
+
+    /**
+     * Sets the text in the query box, without updating the suggestions.
+     */
+    private void setQuery(CharSequence query) {
+        mQueryTextView.setText(query, false);
+    }
+
+    private void launchQuerySearch(int actionKey, String actionMsg, String query) {
+        String action = Intent.ACTION_SEARCH;
+        Intent intent = createIntent(action, null, null, query, null, actionKey, actionMsg);
+        getContext().startActivity(intent);
+    }
+
+    /**
+     * Constructs an intent from the given information and the search dialog state.
+     *
+     * @param action Intent action.
+     * @param data Intent data, or <code>null</code>.
+     * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
+     * @param query Intent query, or <code>null</code>.
+     * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
+     * @param actionKey The key code of the action key that was pressed,
+     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+     * @param actionMsg The message for the action key that was pressed,
+     *        or <code>null</code> if none.
+     * @param mode The search mode, one of the acceptable values for
+     *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
+     * @return The intent.
+     */
+    private Intent createIntent(String action, Uri data, String extraData, String query,
+            String componentName, int actionKey, String actionMsg) {
+        // Now build the Intent
+        Intent intent = new Intent(action);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        // We need CLEAR_TOP to avoid reusing an old task that has other activities
+        // on top of the one we want. We don't want to do this in in-app search though,
+        // as it can be destructive to the activity stack.
+        if (data != null) {
+            intent.setData(data);
+        }
+        intent.putExtra(SearchManager.USER_QUERY, query);
+        if (query != null) {
+            intent.putExtra(SearchManager.QUERY, query);
+        }
+        if (extraData != null) {
+            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+        }
+        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
+            intent.putExtra(SearchManager.ACTION_KEY, actionKey);
+            intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
+        }
+        intent.setComponent(mSearchable.getSearchActivity());
+        return intent;
+    }
+
+    /**
+     * When a particular suggestion has been selected, perform the various lookups required
+     * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
+     * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
+     * the suggestion includes a data id.
+     *
+     * @param c The suggestions cursor, moved to the row of the user's selection
+     * @param actionKey The key code of the action key that was pressed,
+     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+     * @param actionMsg The message for the action key that was pressed,
+     *        or <code>null</code> if none.
+     * @return An intent for the suggestion at the cursor's position.
+     */
+    private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
+        try {
+            // use specific action if supplied, or default action if supplied, or fixed default
+            String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
+
+            // some items are display only, or have effect via the cursor respond click reporting.
+            if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
+                return null;
+            }
+
+            if (action == null) {
+                action = mSearchable.getSuggestIntentAction();
+            }
+            if (action == null) {
+                action = Intent.ACTION_SEARCH;
+            }
+
+            // use specific data if supplied, or default data if supplied
+            String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
+            if (data == null) {
+                data = mSearchable.getSuggestIntentData();
+            }
+            // then, if an ID was provided, append it.
+            if (data != null) {
+                String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
+                if (id != null) {
+                    data = data + "/" + Uri.encode(id);
+                }
+            }
+            Uri dataUri = (data == null) ? null : Uri.parse(data);
+
+            String componentName = getColumnString(
+                    c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
+
+            String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
+            String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
+
+            return createIntent(action, dataUri, extraData, query, componentName, actionKey,
+                    actionMsg);
+        } catch (RuntimeException e ) {
+            int rowNum;
+            try {                       // be really paranoid now
+                rowNum = c.getPosition();
+            } catch (RuntimeException e2 ) {
+                rowNum = -1;
+            }
+            Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
+                            " returned exception" + e.toString());
+            return null;
+        }
+    }
+
+    /**
+     * Callback to watch the text field for empty/non-empty
+     */
+    private TextWatcher mTextWatcher = new TextWatcher() {
+
+        public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
+
+        public void onTextChanged(CharSequence s, int start,
+                int before, int after) {
+            SearchView.this.onTextChanged(s);
+        }
+
+        public void afterTextChanged(Editable s) {
+        }
+    };
+}
diff --git a/core/java/android/widget/SuggestionsAdapter.java b/core/java/android/widget/SuggestionsAdapter.java
new file mode 100644
index 0000000..1b2449e
--- /dev/null
+++ b/core/java/android/widget/SuggestionsAdapter.java
@@ -0,0 +1,705 @@
+/*
+ * 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.widget;
+
+import com.android.internal.R;
+
+import android.app.SearchDialog;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContentResolver.OpenResourceIdResult;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.WeakHashMap;
+
+/**
+ * Provides the contents for the suggestion drop-down list.in {@link SearchDialog}.
+ *
+ * @hide
+ */
+class SuggestionsAdapter extends ResourceCursorAdapter {
+
+    private static final boolean DBG = false;
+    private static final String LOG_TAG = "SuggestionsAdapter";
+    private static final int QUERY_LIMIT = 50;
+
+    private SearchManager mSearchManager;
+    private SearchView mSearchView;
+    private SearchableInfo mSearchable;
+    private Context mProviderContext;
+    private WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache;
+    private SparseArray<Drawable.ConstantState> mBackgroundsCache;
+    private boolean mClosed = false;
+
+    // URL color
+    private ColorStateList mUrlColor;
+
+    // Cached column indexes, updated when the cursor changes.
+    private int mText1Col;
+    private int mText2Col;
+    private int mText2UrlCol;
+    private int mIconName1Col;
+    private int mIconName2Col;
+    private int mBackgroundColorCol;
+
+    static final int NONE = -1;
+
+    private final Runnable mStartSpinnerRunnable;
+    private final Runnable mStopSpinnerRunnable;
+
+    /**
+     * The amount of time we delay in the filter when the user presses the delete key.
+     * @see Filter#setDelayer(android.widget.Filter.Delayer).
+     */
+    private static final long DELETE_KEY_POST_DELAY = 500L;
+
+    public SuggestionsAdapter(Context context, SearchView searchView,
+            SearchableInfo searchable,
+            WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) {
+        super(context,
+                com.android.internal.R.layout.search_dropdown_item_icons_2line,
+                null,   // no initial cursor
+                true);  // auto-requery
+        mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
+        mSearchView = searchView;
+        mSearchable = searchable;
+
+        // set up provider resources (gives us icons, etc.)
+        Context activityContext = mSearchable.getActivityContext(mContext);
+        mProviderContext = mSearchable.getProviderContext(mContext, activityContext);
+
+        mOutsideDrawablesCache = outsideDrawablesCache;
+        mBackgroundsCache = new SparseArray<Drawable.ConstantState>();
+
+        mStartSpinnerRunnable = new Runnable() {
+                public void run() {
+                // mSearchView.setWorking(true); // TODO:
+                }
+            };
+
+        mStopSpinnerRunnable = new Runnable() {
+            public void run() {
+                // mSearchView.setWorking(false); // TODO:
+            }
+        };
+
+        // delay 500ms when deleting
+        getFilter().setDelayer(new Filter.Delayer() {
+
+            private int mPreviousLength = 0;
+
+            public long getPostingDelay(CharSequence constraint) {
+                if (constraint == null) return 0;
+
+                long delay = constraint.length() < mPreviousLength ? DELETE_KEY_POST_DELAY : 0;
+                mPreviousLength = constraint.length();
+                return delay;
+            }
+        });
+    }
+
+    /**
+     * Overridden to always return <code>false</code>, since we cannot be sure that
+     * suggestion sources return stable IDs.
+     */
+    @Override
+    public boolean hasStableIds() {
+        return false;
+    }
+
+    /**
+     * Use the search suggestions provider to obtain a live cursor.  This will be called
+     * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
+     * The results will be processed in the UI thread and changeCursor() will be called.
+     */
+    @Override
+    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+        if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");
+        String query = (constraint == null) ? "" : constraint.toString();
+        /**
+         * for in app search we show the progress spinner until the cursor is returned with
+         * the results.
+         */
+        Cursor cursor = null;
+        //mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO:
+        try {
+            cursor = mSearchManager.getSuggestions(mSearchable, query, QUERY_LIMIT);
+            // trigger fill window so the spinner stays up until the results are copied over and
+            // closer to being ready
+            if (cursor != null) {
+                cursor.getCount();
+                return cursor;
+            }
+        } catch (RuntimeException e) {
+            Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
+        }
+        // If cursor is null or an exception was thrown, stop the spinner and return null.
+        // changeCursor doesn't get called if cursor is null
+        // mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO:
+        return null;
+    }
+
+    public void close() {
+        if (DBG) Log.d(LOG_TAG, "close()");
+        changeCursor(null);
+        mClosed = true;
+    }
+
+    @Override
+    public void notifyDataSetChanged() {
+        if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged");
+        super.notifyDataSetChanged();
+
+        // mSearchView.onDataSetChanged(); // TODO:
+
+        updateSpinnerState(getCursor());
+    }
+
+    @Override
+    public void notifyDataSetInvalidated() {
+        if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated");
+        super.notifyDataSetInvalidated();
+
+        updateSpinnerState(getCursor());
+    }
+
+    private void updateSpinnerState(Cursor cursor) {
+        Bundle extras = cursor != null ? cursor.getExtras() : null;
+        if (DBG) {
+            Log.d(LOG_TAG, "updateSpinnerState - extra = "
+                + (extras != null
+                        ? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)
+                        : null));
+        }
+        // Check if the Cursor indicates that the query is not complete and show the spinner
+        if (extras != null
+                && extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) {
+            // mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO:
+            return;
+        }
+        // If cursor is null or is done, stop the spinner
+        // mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO:
+    }
+
+    /**
+     * Cache columns.
+     */
+    @Override
+    public void changeCursor(Cursor c) {
+        if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
+
+        if (mClosed) {
+            Log.w(LOG_TAG, "Tried to change cursor after adapter was closed.");
+            if (c != null) c.close();
+            return;
+        }
+
+        try {
+            super.changeCursor(c);
+
+            if (c != null) {
+                mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
+                mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
+                mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
+                mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
+                mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
+                mBackgroundColorCol =
+                        c.getColumnIndex(SearchManager.SUGGEST_COLUMN_BACKGROUND_COLOR);
+            }
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "error changing cursor and caching columns", e);
+        }
+    }
+
+    /**
+     * Tags the view with cached child view look-ups.
+     */
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        View v = super.newView(context, cursor, parent);
+        v.setTag(new ChildViewCache(v));
+        return v;
+    }
+
+    /**
+     * Cache of the child views of drop-drown list items, to avoid looking up the children
+     * each time the contents of a list item are changed.
+     */
+    private final static class ChildViewCache {
+        public final TextView mText1;
+        public final TextView mText2;
+        public final ImageView mIcon1;
+        public final ImageView mIcon2;
+
+        public ChildViewCache(View v) {
+            mText1 = (TextView) v.findViewById(com.android.internal.R.id.text1);
+            mText2 = (TextView) v.findViewById(com.android.internal.R.id.text2);
+            mIcon1 = (ImageView) v.findViewById(com.android.internal.R.id.icon1);
+            mIcon2 = (ImageView) v.findViewById(com.android.internal.R.id.icon2);
+        }
+    }
+
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        ChildViewCache views = (ChildViewCache) view.getTag();
+
+        int backgroundColor = 0;
+        if (mBackgroundColorCol != -1) {
+            backgroundColor = cursor.getInt(mBackgroundColorCol);
+        }
+        Drawable background = getItemBackground(backgroundColor);
+        view.setBackgroundDrawable(background);
+
+        if (views.mText1 != null) {
+            String text1 = getStringOrNull(cursor, mText1Col);
+            setViewText(views.mText1, text1);
+        }
+        if (views.mText2 != null) {
+            // First check TEXT_2_URL
+            CharSequence text2 = getStringOrNull(cursor, mText2UrlCol);
+            if (text2 != null) {
+                text2 = formatUrl(text2);
+            } else {
+                text2 = getStringOrNull(cursor, mText2Col);
+            }
+            
+            // If no second line of text is indicated, allow the first line of text
+            // to be up to two lines if it wants to be.
+            if (TextUtils.isEmpty(text2)) {
+                if (views.mText1 != null) {
+                    views.mText1.setSingleLine(false);
+                    views.mText1.setMaxLines(2);
+                }
+            } else {
+                if (views.mText1 != null) {
+                    views.mText1.setSingleLine(true);
+                    views.mText1.setMaxLines(1);
+                }
+            }
+            setViewText(views.mText2, text2);
+        }
+
+        if (views.mIcon1 != null) {
+            setViewDrawable(views.mIcon1, getIcon1(cursor));
+        }
+        if (views.mIcon2 != null) {
+            setViewDrawable(views.mIcon2, getIcon2(cursor));
+        }
+    }
+
+    private CharSequence formatUrl(CharSequence url) {
+        if (mUrlColor == null) {
+            // Lazily get the URL color from the current theme.
+            TypedValue colorValue = new TypedValue();
+            mContext.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true);
+            mUrlColor = mContext.getResources().getColorStateList(colorValue.resourceId);
+        }
+
+        SpannableString text = new SpannableString(url);
+        text.setSpan(new TextAppearanceSpan(null, 0, 0, mUrlColor, null),
+                0, url.length(),
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        return text;
+    }
+
+    /**
+     * Gets a drawable with no color when selected or pressed, and the given color when
+     * neither selected nor pressed.
+     *
+     * @return A drawable, or {@code null} if the given color is transparent.
+     */
+    private Drawable getItemBackground(int backgroundColor) {
+        if (backgroundColor == 0) {
+            return null;
+        } else {
+            Drawable.ConstantState cachedBg = mBackgroundsCache.get(backgroundColor);
+            if (cachedBg != null) {
+                if (DBG) Log.d(LOG_TAG, "Background cache hit for color " + backgroundColor);
+                return cachedBg.newDrawable(mProviderContext.getResources());
+            }
+            if (DBG) Log.d(LOG_TAG, "Creating new background for color " + backgroundColor);
+            ColorDrawable transparent = new ColorDrawable(0);
+            ColorDrawable background = new ColorDrawable(backgroundColor);
+            StateListDrawable newBg = new StateListDrawable();
+            newBg.addState(new int[]{android.R.attr.state_selected}, transparent);
+            newBg.addState(new int[]{android.R.attr.state_pressed}, transparent);
+            newBg.addState(new int[]{}, background);
+            mBackgroundsCache.put(backgroundColor, newBg.getConstantState());
+            return newBg;
+        }
+    }
+
+    private void setViewText(TextView v, CharSequence text) {
+        // Set the text even if it's null, since we need to clear any previous text.
+        v.setText(text);
+
+        if (TextUtils.isEmpty(text)) {
+            v.setVisibility(View.GONE);
+        } else {
+            v.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private Drawable getIcon1(Cursor cursor) {
+        if (mIconName1Col < 0) {
+            return null;
+        }
+        String value = cursor.getString(mIconName1Col);
+        Drawable drawable = getDrawableFromResourceValue(value);
+        if (drawable != null) {
+            return drawable;
+        }
+        return getDefaultIcon1(cursor);
+    }
+
+    private Drawable getIcon2(Cursor cursor) {
+        if (mIconName2Col < 0) {
+            return null;
+        }
+        String value = cursor.getString(mIconName2Col);
+        return getDrawableFromResourceValue(value);
+    }
+
+    /**
+     * Sets the drawable in an image view, makes sure the view is only visible if there
+     * is a drawable.
+     */
+    private void setViewDrawable(ImageView v, Drawable drawable) {
+        // Set the icon even if the drawable is null, since we need to clear any
+        // previous icon.
+        v.setImageDrawable(drawable);
+
+        if (drawable == null) {
+            v.setVisibility(View.GONE);
+        } else {
+            v.setVisibility(View.VISIBLE);
+
+            // This is a hack to get any animated drawables (like a 'working' spinner)
+            // to animate. You have to setVisible true on an AnimationDrawable to get
+            // it to start animating, but it must first have been false or else the
+            // call to setVisible will be ineffective. We need to clear up the story
+            // about animated drawables in the future, see http://b/1878430.
+            drawable.setVisible(false, false);
+            drawable.setVisible(true, false);
+        }
+    }
+
+    /**
+     * Gets the text to show in the query field when a suggestion is selected.
+     *
+     * @param cursor The Cursor to read the suggestion data from. The Cursor should already
+     *        be moved to the suggestion that is to be read from.
+     * @return The text to show, or <code>null</code> if the query should not be
+     *         changed when selecting this suggestion.
+     */
+    @Override
+    public CharSequence convertToString(Cursor cursor) {
+        if (cursor == null) {
+            return null;
+        }
+
+        String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY);
+        if (query != null) {
+            return query;
+        }
+
+        if (mSearchable.shouldRewriteQueryFromData()) {
+            String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
+            if (data != null) {
+                return data;
+            }
+        }
+
+        if (mSearchable.shouldRewriteQueryFromText()) {
+            String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1);
+            if (text1 != null) {
+                return text1;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * This method is overridden purely to provide a bit of protection against
+     * flaky content providers.
+     *
+     * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
+     */
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        try {
+            return super.getView(position, convertView, parent);
+        } catch (RuntimeException e) {
+            Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);
+            // Put exception string in item title
+            View v = newView(mContext, mCursor, parent);
+            if (v != null) {
+                ChildViewCache views = (ChildViewCache) v.getTag();
+                TextView tv = views.mText1;
+                tv.setText(e.toString());
+            }
+            return v;
+        }
+    }
+
+    /**
+     * Gets a drawable given a value provided by a suggestion provider.
+     *
+     * This value could be just the string value of a resource id
+     * (e.g., "2130837524"), in which case we will try to retrieve a drawable from
+     * the provider's resources. If the value is not an integer, it is
+     * treated as a Uri and opened with
+     * {@link ContentResolver#openOutputStream(android.net.Uri, String)}.
+     *
+     * All resources and URIs are read using the suggestion provider's context.
+     *
+     * If the string is not formatted as expected, or no drawable can be found for
+     * the provided value, this method returns null.
+     *
+     * @param drawableId a string like "2130837524",
+     *        "android.resource://com.android.alarmclock/2130837524",
+     *        or "content://contacts/photos/253".
+     * @return a Drawable, or null if none found
+     */
+    private Drawable getDrawableFromResourceValue(String drawableId) {
+        if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) {
+            return null;
+        }
+        try {
+            // First, see if it's just an integer
+            int resourceId = Integer.parseInt(drawableId);
+            // It's an int, look for it in the cache
+            String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE
+                    + "://" + mProviderContext.getPackageName() + "/" + resourceId;
+            // Must use URI as cache key, since ints are app-specific
+            Drawable drawable = checkIconCache(drawableUri);
+            if (drawable != null) {
+                return drawable;
+            }
+            // Not cached, find it by resource ID
+            drawable = mProviderContext.getResources().getDrawable(resourceId);
+            // Stick it in the cache, using the URI as key
+            storeInIconCache(drawableUri, drawable);
+            return drawable;
+        } catch (NumberFormatException nfe) {
+            // It's not an integer, use it as a URI
+            Drawable drawable = checkIconCache(drawableId);
+            if (drawable != null) {
+                return drawable;
+            }
+            Uri uri = Uri.parse(drawableId);
+            drawable = getDrawable(uri);
+            storeInIconCache(drawableId, drawable);
+            return drawable;
+        } catch (Resources.NotFoundException nfe) {
+            // It was an integer, but it couldn't be found, bail out
+            Log.w(LOG_TAG, "Icon resource not found: " + drawableId);
+            return null;
+        }
+    }
+
+    /**
+     * Gets a drawable by URI, without using the cache.
+     *
+     * @return A drawable, or {@code null} if the drawable could not be loaded.
+     */
+    private Drawable getDrawable(Uri uri) {
+        try {
+            String scheme = uri.getScheme();
+            if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+                // Load drawables through Resources, to get the source density information
+                OpenResourceIdResult r =
+                    mProviderContext.getContentResolver().getResourceId(uri);
+                try {
+                    return r.r.getDrawable(r.id);
+                } catch (Resources.NotFoundException ex) {
+                    throw new FileNotFoundException("Resource does not exist: " + uri);
+                }
+            } else {
+                // Let the ContentResolver handle content and file URIs.
+                InputStream stream = mProviderContext.getContentResolver().openInputStream(uri);
+                if (stream == null) {
+                    throw new FileNotFoundException("Failed to open " + uri);
+                }
+                try {
+                    return Drawable.createFromStream(stream, null);
+                } finally {
+                    try {
+                        stream.close();
+                    } catch (IOException ex) {
+                        Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex);
+                    }
+                }
+            }
+        } catch (FileNotFoundException fnfe) {
+            Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage());
+            return null;
+        }
+    }
+
+    private Drawable checkIconCache(String resourceUri) {
+        Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri);
+        if (cached == null) {
+            return null;
+        }
+        if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri);
+        return cached.newDrawable();
+    }
+
+    private void storeInIconCache(String resourceUri, Drawable drawable) {
+        if (drawable != null) {
+            mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState());
+        }
+    }
+
+    /**
+     * Gets the left-hand side icon that will be used for the current suggestion
+     * if the suggestion contains an icon column but no icon or a broken icon.
+     *
+     * @param cursor A cursor positioned at the current suggestion.
+     * @return A non-null drawable.
+     */
+    private Drawable getDefaultIcon1(Cursor cursor) {
+        // First check the component that the suggestion is originally from
+        String c = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
+        if (c != null) {
+            ComponentName component = ComponentName.unflattenFromString(c);
+            if (component != null) {
+                Drawable drawable = getActivityIconWithCache(component);
+                if (drawable != null) {
+                    return drawable;
+                }
+            } else {
+                Log.w(LOG_TAG, "Bad component name: " + c);
+            }
+        }
+
+        // Then check the component that gave us the suggestion
+        Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity());
+        if (drawable != null) {
+            return drawable;
+        }
+
+        // Fall back to a default icon
+        return mContext.getPackageManager().getDefaultActivityIcon();
+    }
+
+    /**
+     * Gets the activity or application icon for an activity.
+     * Uses the local icon cache for fast repeated lookups.
+     *
+     * @param component Name of an activity.
+     * @return A drawable, or {@code null} if neither the activity nor the application
+     *         has an icon set.
+     */
+    private Drawable getActivityIconWithCache(ComponentName component) {
+        // First check the icon cache
+        String componentIconKey = component.flattenToShortString();
+        // Using containsKey() since we also store null values.
+        if (mOutsideDrawablesCache.containsKey(componentIconKey)) {
+            Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey);
+            return cached == null ? null : cached.newDrawable(mProviderContext.getResources());
+        }
+        // Then try the activity or application icon
+        Drawable drawable = getActivityIcon(component);
+        // Stick it in the cache so we don't do this lookup again.
+        Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState();
+        mOutsideDrawablesCache.put(componentIconKey, toCache);
+        return drawable;
+    }
+
+    /**
+     * Gets the activity or application icon for an activity.
+     *
+     * @param component Name of an activity.
+     * @return A drawable, or {@code null} if neither the acitivy or the application
+     *         have an icon set.
+     */
+    private Drawable getActivityIcon(ComponentName component) {
+        PackageManager pm = mContext.getPackageManager();
+        final ActivityInfo activityInfo;
+        try {
+            activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA);
+        } catch (NameNotFoundException ex) {
+            Log.w(LOG_TAG, ex.toString());
+            return null;
+        }
+        int iconId = activityInfo.getIconResource();
+        if (iconId == 0) return null;
+        String pkg = component.getPackageName();
+        Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo);
+        if (drawable == null) {
+            Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for "
+                    + component.flattenToShortString());
+            return null;
+        }
+        return drawable;
+    }
+
+    /**
+     * Gets the value of a string column by name.
+     *
+     * @param cursor Cursor to read the value from.
+     * @param columnName The name of the column to read.
+     * @return The value of the given column, or <code>null</null>
+     *         if the cursor does not contain the given column.
+     */
+    public static String getColumnString(Cursor cursor, String columnName) {
+        int col = cursor.getColumnIndex(columnName);
+        return getStringOrNull(cursor, col);
+    }
+
+    private static String getStringOrNull(Cursor cursor, int col) {
+        if (col == NONE) {
+            return null;
+        }
+        try {
+            return cursor.getString(col);
+        } catch (Exception e) {
+            Log.e(LOG_TAG,
+                    "unexpected error retrieving valid column from cursor, "
+                            + "did the remote process die?", e);
+            return null;
+        }
+    }
+}
diff --git a/core/res/res/layout/search_view.xml b/core/res/res/layout/search_view.xml
new file mode 100644
index 0000000..c229b59
--- /dev/null
+++ b/core/res/res/layout/search_view.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2010 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.
+ */
+
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/search_bar"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:focusable="true"
+    android:descendantFocusability="afterDescendants">
+
+    <!-- This is actually used for the badge icon *or* the badge label (or neither) -->
+    <TextView
+        android:id="@+id/search_badge"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="2dip"
+        android:drawablePadding="0dip"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorPrimaryInverse"
+        android:visibility="gone"
+    />
+
+    <ImageView
+        android:id="@+id/search_button"
+        android:layout_height="36dip"
+        android:layout_width="36dip"
+        android:layout_marginRight="7dip"
+        android:layout_gravity="center_vertical"
+        android:src="@android:drawable/ic_btn_search"
+    />
+
+    <!-- Inner layout contains the app icon, button(s) and EditText -->
+    <LinearLayout
+        android:id="@+id/search_edit_frame"
+        android:layout_width="300dp"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:orientation="horizontal"
+        android:background="?android:attr/editTextBackground">
+
+        <ImageView
+            android:id="@+id/search_app_icon"
+            android:layout_height="24dip"
+            android:layout_width="24dip"
+            android:layout_marginRight="7dip"
+            android:layout_gravity="bottom"
+            android:src="@android:drawable/ic_btn_search"
+        />
+
+        <AutoCompleteTextView
+            android:id="@+id/search_src_text"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+            android:layout_weight="1"
+            android:paddingLeft="8dip"
+            android:paddingRight="6dip"
+            android:drawablePadding="2dip"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:background="@null"
+            android:inputType="text|textAutoComplete"
+            android:imeOptions="actionSearch"
+            android:dropDownWidth="300dp"
+            android:dropDownHeight="wrap_content"
+            android:dropDownAnchor="@id/search_edit_frame"
+            android:dropDownVerticalOffset="0dip"
+            android:dropDownHorizontalOffset="0dip"
+            android:popupBackground="@android:drawable/search_dropdown_background"
+        />
+
+        <ImageView
+            android:id="@+id/search_close_btn"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_gravity="bottom"
+            android:src="@android:drawable/btn_close"
+        />
+    </LinearLayout>
+
+    <ImageView
+        android:id="@+id/search_go_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_marginLeft="4dip"
+        android:layout_marginRight="4dip"
+        android:src="@android:drawable/ic_btn_find_next"
+    />
+
+    <ImageButton
+        android:id="@+id/search_voice_btn"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_marginLeft="0dip"
+        android:layout_marginTop="-6.5dip"
+        android:layout_marginBottom="-7dip"
+        android:layout_marginRight="-5dip"
+        android:background="@drawable/btn_search_dialog_voice"
+        android:src="@android:drawable/ic_btn_speak_now"
+        android:visibility="gone"
+    />
+</LinearLayout>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 7d54ea1..aedfb90 100755
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -4094,4 +4094,9 @@
         <attr name="closeButtonStyle" format="reference" />
     </declare-styleable>
 
+    <declare-styleable name="SearchView">
+        <!-- The default state of the SearchView. If true, it will be iconified when not in
+             use and expanded when clicked. -->
+        <attr name="iconifiedByDefault" format="boolean"/>
+    </declare-styleable>
 </resources>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index bff22a6..07a6481 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -1343,6 +1343,7 @@
   <public type="attr" name="closeButtonStyle" />
   <public type="attr" name="titleTextStyle" />
   <public type="attr" name="subtitleTextStyle" />
+  <public type="attr" name="iconifiedByDefault" />
 
   <public type="anim" name="animator_fade_in" />
   <public type="anim" name="animator_fade_out" />
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index ef9d1b7..fbc32fb 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -443,14 +443,8 @@
         <item name="android:background">@android:drawable/btn_default</item>
     </style>
 
-    <style name="Widget.AutoCompleteTextView">
-        <item name="android:focusable">true</item>
-        <item name="android:focusableInTouchMode">true</item>
-        <item name="android:clickable">true</item>
-        <item name="android:background">@android:drawable/edit_text</item>
+    <style name="Widget.AutoCompleteTextView" parent="Widget.EditText">
         <item name="android:completionHintView">@android:layout/simple_dropdown_hint</item>
-        <item name="android:textAppearance">?android:attr/textAppearanceMediumInverse</item>
-        <item name="android:gravity">center_vertical</item>
         <item name="android:completionThreshold">2</item>
         <item name="android:dropDownSelector">@android:drawable/list_selector_background</item>
         <item name="android:popupBackground">@android:drawable/spinner_dropdown_background</item>