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>