Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import static android.widget.SuggestionsAdapter.getColumnString;
     20 
     21 import android.app.PendingIntent;
     22 import android.app.SearchManager;
     23 import android.app.SearchableInfo;
     24 import android.content.ActivityNotFoundException;
     25 import android.content.ComponentName;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.pm.PackageManager;
     29 import android.content.pm.ResolveInfo;
     30 import android.content.res.Configuration;
     31 import android.content.res.Resources;
     32 import android.content.res.TypedArray;
     33 import android.database.Cursor;
     34 import android.graphics.Rect;
     35 import android.graphics.drawable.Drawable;
     36 import android.net.Uri;
     37 import android.os.Bundle;
     38 import android.os.Handler;
     39 import android.speech.RecognizerIntent;
     40 import android.text.Editable;
     41 import android.text.InputType;
     42 import android.text.Spannable;
     43 import android.text.SpannableStringBuilder;
     44 import android.text.TextUtils;
     45 import android.text.TextWatcher;
     46 import android.text.style.ImageSpan;
     47 import android.util.AttributeSet;
     48 import android.util.Log;
     49 import android.util.TypedValue;
     50 import android.view.CollapsibleActionView;
     51 import android.view.KeyEvent;
     52 import android.view.LayoutInflater;
     53 import android.view.View;
     54 import android.view.inputmethod.EditorInfo;
     55 import android.view.inputmethod.InputMethodManager;
     56 import android.widget.AdapterView.OnItemClickListener;
     57 import android.widget.AdapterView.OnItemSelectedListener;
     58 import android.widget.TextView.OnEditorActionListener;
     59 
     60 import com.android.internal.R;
     61 
     62 import java.util.WeakHashMap;
     63 
     64 /**
     65  * A widget that provides a user interface for the user to enter a search query and submit a request
     66  * to a search provider. Shows a list of query suggestions or results, if available, and allows the
     67  * user to pick a suggestion or result to launch into.
     68  *
     69  * <p>
     70  * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
     71  * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
     72  * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
     73  * </p>
     74  * <p>
     75  * If you want the search field to always be visible, then call setIconifiedByDefault(false).
     76  * </p>
     77  *
     78  * <p>
     79  * For more information, see the <a href="{@docRoot}guide/topics/search/index.html">Search</a>
     80  * documentation.
     81  * <p>
     82  *
     83  * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
     84  * @attr ref android.R.styleable#SearchView_iconifiedByDefault
     85  * @attr ref android.R.styleable#SearchView_imeOptions
     86  * @attr ref android.R.styleable#SearchView_inputType
     87  * @attr ref android.R.styleable#SearchView_maxWidth
     88  * @attr ref android.R.styleable#SearchView_queryHint
     89  */
     90 public class SearchView extends LinearLayout implements CollapsibleActionView {
     91 
     92     private static final boolean DBG = false;
     93     private static final String LOG_TAG = "SearchView";
     94 
     95     /**
     96      * Private constant for removing the microphone in the keyboard.
     97      */
     98     private static final String IME_OPTION_NO_MICROPHONE = "nm";
     99 
    100     private OnQueryTextListener mOnQueryChangeListener;
    101     private OnCloseListener mOnCloseListener;
    102     private OnFocusChangeListener mOnQueryTextFocusChangeListener;
    103     private OnSuggestionListener mOnSuggestionListener;
    104     private OnClickListener mOnSearchClickListener;
    105 
    106     private boolean mIconifiedByDefault;
    107     private boolean mIconified;
    108     private CursorAdapter mSuggestionsAdapter;
    109     private View mSearchButton;
    110     private View mSubmitButton;
    111     private View mSearchPlate;
    112     private View mSubmitArea;
    113     private ImageView mCloseButton;
    114     private View mSearchEditFrame;
    115     private View mVoiceButton;
    116     private SearchAutoComplete mQueryTextView;
    117     private View mDropDownAnchor;
    118     private ImageView mSearchHintIcon;
    119     private boolean mSubmitButtonEnabled;
    120     private CharSequence mQueryHint;
    121     private boolean mQueryRefinement;
    122     private boolean mClearingFocus;
    123     private int mMaxWidth;
    124     private boolean mVoiceButtonEnabled;
    125     private CharSequence mOldQueryText;
    126     private CharSequence mUserQuery;
    127     private boolean mExpandedInActionView;
    128     private int mCollapsedImeOptions;
    129 
    130     private SearchableInfo mSearchable;
    131     private Bundle mAppSearchData;
    132 
    133     /*
    134      * SearchView can be set expanded before the IME is ready to be shown during
    135      * initial UI setup. The show operation is asynchronous to account for this.
    136      */
    137     private Runnable mShowImeRunnable = new Runnable() {
    138         public void run() {
    139             InputMethodManager imm = (InputMethodManager)
    140                     getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    141 
    142             if (imm != null) {
    143                 imm.showSoftInputUnchecked(0, null);
    144             }
    145         }
    146     };
    147 
    148     private Runnable mUpdateDrawableStateRunnable = new Runnable() {
    149         public void run() {
    150             updateFocusedState();
    151         }
    152     };
    153 
    154     private Runnable mReleaseCursorRunnable = new Runnable() {
    155         public void run() {
    156             if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) {
    157                 mSuggestionsAdapter.changeCursor(null);
    158             }
    159         }
    160     };
    161 
    162     // For voice searching
    163     private final Intent mVoiceWebSearchIntent;
    164     private final Intent mVoiceAppSearchIntent;
    165 
    166     // A weak map of drawables we've gotten from other packages, so we don't load them
    167     // more than once.
    168     private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
    169             new WeakHashMap<String, Drawable.ConstantState>();
    170 
    171     /**
    172      * Callbacks for changes to the query text.
    173      */
    174     public interface OnQueryTextListener {
    175 
    176         /**
    177          * Called when the user submits the query. This could be due to a key press on the
    178          * keyboard or due to pressing a submit button.
    179          * The listener can override the standard behavior by returning true
    180          * to indicate that it has handled the submit request. Otherwise return false to
    181          * let the SearchView handle the submission by launching any associated intent.
    182          *
    183          * @param query the query text that is to be submitted
    184          *
    185          * @return true if the query has been handled by the listener, false to let the
    186          * SearchView perform the default action.
    187          */
    188         boolean onQueryTextSubmit(String query);
    189 
    190         /**
    191          * Called when the query text is changed by the user.
    192          *
    193          * @param newText the new content of the query text field.
    194          *
    195          * @return false if the SearchView should perform the default action of showing any
    196          * suggestions if available, true if the action was handled by the listener.
    197          */
    198         boolean onQueryTextChange(String newText);
    199     }
    200 
    201     public interface OnCloseListener {
    202 
    203         /**
    204          * The user is attempting to close the SearchView.
    205          *
    206          * @return true if the listener wants to override the default behavior of clearing the
    207          * text field and dismissing it, false otherwise.
    208          */
    209         boolean onClose();
    210     }
    211 
    212     /**
    213      * Callback interface for selection events on suggestions. These callbacks
    214      * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
    215      */
    216     public interface OnSuggestionListener {
    217 
    218         /**
    219          * Called when a suggestion was selected by navigating to it.
    220          * @param position the absolute position in the list of suggestions.
    221          *
    222          * @return true if the listener handles the event and wants to override the default
    223          * behavior of possibly rewriting the query based on the selected item, false otherwise.
    224          */
    225         boolean onSuggestionSelect(int position);
    226 
    227         /**
    228          * Called when a suggestion was clicked.
    229          * @param position the absolute position of the clicked item in the list of suggestions.
    230          *
    231          * @return true if the listener handles the event and wants to override the default
    232          * behavior of launching any intent or submitting a search query specified on that item.
    233          * Return false otherwise.
    234          */
    235         boolean onSuggestionClick(int position);
    236     }
    237 
    238     public SearchView(Context context) {
    239         this(context, null);
    240     }
    241 
    242     public SearchView(Context context, AttributeSet attrs) {
    243         super(context, attrs);
    244 
    245         LayoutInflater inflater = (LayoutInflater) context
    246                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    247         inflater.inflate(R.layout.search_view, this, true);
    248 
    249         mSearchButton = findViewById(R.id.search_button);
    250         mQueryTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
    251         mQueryTextView.setSearchView(this);
    252 
    253         mSearchEditFrame = findViewById(R.id.search_edit_frame);
    254         mSearchPlate = findViewById(R.id.search_plate);
    255         mSubmitArea = findViewById(R.id.submit_area);
    256         mSubmitButton = findViewById(R.id.search_go_btn);
    257         mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
    258         mVoiceButton = findViewById(R.id.search_voice_btn);
    259         mSearchHintIcon = (ImageView) findViewById(R.id.search_mag_icon);
    260 
    261         mSearchButton.setOnClickListener(mOnClickListener);
    262         mCloseButton.setOnClickListener(mOnClickListener);
    263         mSubmitButton.setOnClickListener(mOnClickListener);
    264         mVoiceButton.setOnClickListener(mOnClickListener);
    265         mQueryTextView.setOnClickListener(mOnClickListener);
    266 
    267         mQueryTextView.addTextChangedListener(mTextWatcher);
    268         mQueryTextView.setOnEditorActionListener(mOnEditorActionListener);
    269         mQueryTextView.setOnItemClickListener(mOnItemClickListener);
    270         mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener);
    271         mQueryTextView.setOnKeyListener(mTextKeyListener);
    272         // Inform any listener of focus changes
    273         mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
    274 
    275             public void onFocusChange(View v, boolean hasFocus) {
    276                 if (mOnQueryTextFocusChangeListener != null) {
    277                     mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
    278                 }
    279             }
    280         });
    281 
    282         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0);
    283         setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
    284         int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
    285         if (maxWidth != -1) {
    286             setMaxWidth(maxWidth);
    287         }
    288         CharSequence queryHint = a.getText(R.styleable.SearchView_queryHint);
    289         if (!TextUtils.isEmpty(queryHint)) {
    290             setQueryHint(queryHint);
    291         }
    292         int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
    293         if (imeOptions != -1) {
    294             setImeOptions(imeOptions);
    295         }
    296         int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
    297         if (inputType != -1) {
    298             setInputType(inputType);
    299         }
    300 
    301         a.recycle();
    302 
    303         boolean focusable = true;
    304 
    305         a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0);
    306         focusable = a.getBoolean(R.styleable.View_focusable, focusable);
    307         a.recycle();
    308         setFocusable(focusable);
    309 
    310         // Save voice intent for later queries/launching
    311         mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
    312         mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    313         mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
    314                 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
    315 
    316         mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    317         mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    318 
    319         mDropDownAnchor = findViewById(mQueryTextView.getDropDownAnchor());
    320         if (mDropDownAnchor != null) {
    321             mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
    322                 @Override
    323                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
    324                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
    325                     adjustDropDownSizeAndPosition();
    326                 }
    327 
    328             });
    329         }
    330 
    331         updateViewsVisibility(mIconifiedByDefault);
    332         updateQueryHint();
    333     }
    334 
    335     /**
    336      * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
    337      * to display labels, hints, suggestions, create intents for launching search results screens
    338      * and controlling other affordances such as a voice button.
    339      *
    340      * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
    341      * activity or a global search provider.
    342      */
    343     public void setSearchableInfo(SearchableInfo searchable) {
    344         mSearchable = searchable;
    345         if (mSearchable != null) {
    346             updateSearchAutoComplete();
    347             updateQueryHint();
    348         }
    349         // Cache the voice search capability
    350         mVoiceButtonEnabled = hasVoiceSearch();
    351 
    352         if (mVoiceButtonEnabled) {
    353             // Disable the microphone on the keyboard, as a mic is displayed near the text box
    354             // TODO: use imeOptions to disable voice input when the new API will be available
    355             mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
    356         }
    357         updateViewsVisibility(isIconified());
    358     }
    359 
    360     /**
    361      * Sets the APP_DATA for legacy SearchDialog use.
    362      * @param appSearchData bundle provided by the app when launching the search dialog
    363      * @hide
    364      */
    365     public void setAppSearchData(Bundle appSearchData) {
    366         mAppSearchData = appSearchData;
    367     }
    368 
    369     /**
    370      * Sets the IME options on the query text field.
    371      *
    372      * @see TextView#setImeOptions(int)
    373      * @param imeOptions the options to set on the query text field
    374      *
    375      * @attr ref android.R.styleable#SearchView_imeOptions
    376      */
    377     public void setImeOptions(int imeOptions) {
    378         mQueryTextView.setImeOptions(imeOptions);
    379     }
    380 
    381     /**
    382      * Sets the input type on the query text field.
    383      *
    384      * @see TextView#setInputType(int)
    385      * @param inputType the input type to set on the query text field
    386      *
    387      * @attr ref android.R.styleable#SearchView_inputType
    388      */
    389     public void setInputType(int inputType) {
    390         mQueryTextView.setInputType(inputType);
    391     }
    392 
    393     /** @hide */
    394     @Override
    395     public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    396         // Don't accept focus if in the middle of clearing focus
    397         if (mClearingFocus) return false;
    398         // Check if SearchView is focusable.
    399         if (!isFocusable()) return false;
    400         // If it is not iconified, then give the focus to the text field
    401         if (!isIconified()) {
    402             boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect);
    403             if (result) {
    404                 updateViewsVisibility(false);
    405             }
    406             return result;
    407         } else {
    408             return super.requestFocus(direction, previouslyFocusedRect);
    409         }
    410     }
    411 
    412     /** @hide */
    413     @Override
    414     public void clearFocus() {
    415         mClearingFocus = true;
    416         setImeVisibility(false);
    417         super.clearFocus();
    418         mQueryTextView.clearFocus();
    419         mClearingFocus = false;
    420     }
    421 
    422     /**
    423      * Sets a listener for user actions within the SearchView.
    424      *
    425      * @param listener the listener object that receives callbacks when the user performs
    426      * actions in the SearchView such as clicking on buttons or typing a query.
    427      */
    428     public void setOnQueryTextListener(OnQueryTextListener listener) {
    429         mOnQueryChangeListener = listener;
    430     }
    431 
    432     /**
    433      * Sets a listener to inform when the user closes the SearchView.
    434      *
    435      * @param listener the listener to call when the user closes the SearchView.
    436      */
    437     public void setOnCloseListener(OnCloseListener listener) {
    438         mOnCloseListener = listener;
    439     }
    440 
    441     /**
    442      * Sets a listener to inform when the focus of the query text field changes.
    443      *
    444      * @param listener the listener to inform of focus changes.
    445      */
    446     public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
    447         mOnQueryTextFocusChangeListener = listener;
    448     }
    449 
    450     /**
    451      * Sets a listener to inform when a suggestion is focused or clicked.
    452      *
    453      * @param listener the listener to inform of suggestion selection events.
    454      */
    455     public void setOnSuggestionListener(OnSuggestionListener listener) {
    456         mOnSuggestionListener = listener;
    457     }
    458 
    459     /**
    460      * Sets a listener to inform when the search button is pressed. This is only
    461      * relevant when the text field is not visible by default. Calling {@link #setIconified
    462      * setIconified(false)} can also cause this listener to be informed.
    463      *
    464      * @param listener the listener to inform when the search button is clicked or
    465      * the text field is programmatically de-iconified.
    466      */
    467     public void setOnSearchClickListener(OnClickListener listener) {
    468         mOnSearchClickListener = listener;
    469     }
    470 
    471     /**
    472      * Returns the query string currently in the text field.
    473      *
    474      * @return the query string
    475      */
    476     public CharSequence getQuery() {
    477         return mQueryTextView.getText();
    478     }
    479 
    480     /**
    481      * Sets a query string in the text field and optionally submits the query as well.
    482      *
    483      * @param query the query string. This replaces any query text already present in the
    484      * text field.
    485      * @param submit whether to submit the query right now or only update the contents of
    486      * text field.
    487      */
    488     public void setQuery(CharSequence query, boolean submit) {
    489         mQueryTextView.setText(query);
    490         if (query != null) {
    491             mQueryTextView.setSelection(query.length());
    492             mUserQuery = query;
    493         }
    494 
    495         // If the query is not empty and submit is requested, submit the query
    496         if (submit && !TextUtils.isEmpty(query)) {
    497             onSubmitQuery();
    498         }
    499     }
    500 
    501     /**
    502      * Sets the hint text to display in the query text field. This overrides any hint specified
    503      * in the SearchableInfo.
    504      *
    505      * @param hint the hint text to display
    506      *
    507      * @attr ref android.R.styleable#SearchView_queryHint
    508      */
    509     public void setQueryHint(CharSequence hint) {
    510         mQueryHint = hint;
    511         updateQueryHint();
    512     }
    513 
    514     /**
    515      * Sets the default or resting state of the search field. If true, a single search icon is
    516      * shown by default and expands to show the text field and other buttons when pressed. Also,
    517      * if the default state is iconified, then it collapses to that state when the close button
    518      * is pressed. Changes to this property will take effect immediately.
    519      *
    520      * <p>The default value is true.</p>
    521      *
    522      * @param iconified whether the search field should be iconified by default
    523      *
    524      * @attr ref android.R.styleable#SearchView_iconifiedByDefault
    525      */
    526     public void setIconifiedByDefault(boolean iconified) {
    527         if (mIconifiedByDefault == iconified) return;
    528         mIconifiedByDefault = iconified;
    529         updateViewsVisibility(iconified);
    530         updateQueryHint();
    531     }
    532 
    533     /**
    534      * Returns the default iconified state of the search field.
    535      * @return
    536      */
    537     public boolean isIconfiedByDefault() {
    538         return mIconifiedByDefault;
    539     }
    540 
    541     /**
    542      * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
    543      * a temporary state and does not override the default iconified state set by
    544      * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
    545      * a false here will only be valid until the user closes the field. And if the default
    546      * state is expanded, then a true here will only clear the text field and not close it.
    547      *
    548      * @param iconify a true value will collapse the SearchView to an icon, while a false will
    549      * expand it.
    550      */
    551     public void setIconified(boolean iconify) {
    552         if (iconify) {
    553             onCloseClicked();
    554         } else {
    555             onSearchClicked();
    556         }
    557     }
    558 
    559     /**
    560      * Returns the current iconified state of the SearchView.
    561      *
    562      * @return true if the SearchView is currently iconified, false if the search field is
    563      * fully visible.
    564      */
    565     public boolean isIconified() {
    566         return mIconified;
    567     }
    568 
    569     /**
    570      * Enables showing a submit button when the query is non-empty. In cases where the SearchView
    571      * is being used to filter the contents of the current activity and doesn't launch a separate
    572      * results activity, then the submit button should be disabled.
    573      *
    574      * @param enabled true to show a submit button for submitting queries, false if a submit
    575      * button is not required.
    576      */
    577     public void setSubmitButtonEnabled(boolean enabled) {
    578         mSubmitButtonEnabled = enabled;
    579         updateViewsVisibility(isIconified());
    580     }
    581 
    582     /**
    583      * Returns whether the submit button is enabled when necessary or never displayed.
    584      *
    585      * @return whether the submit button is enabled automatically when necessary
    586      */
    587     public boolean isSubmitButtonEnabled() {
    588         return mSubmitButtonEnabled;
    589     }
    590 
    591     /**
    592      * Specifies if a query refinement button should be displayed alongside each suggestion
    593      * or if it should depend on the flags set in the individual items retrieved from the
    594      * suggestions provider. Clicking on the query refinement button will replace the text
    595      * in the query text field with the text from the suggestion. This flag only takes effect
    596      * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
    597      * and not when using a custom adapter.
    598      *
    599      * @param enable true if all items should have a query refinement button, false if only
    600      * those items that have a query refinement flag set should have the button.
    601      *
    602      * @see SearchManager#SUGGEST_COLUMN_FLAGS
    603      * @see SearchManager#FLAG_QUERY_REFINEMENT
    604      */
    605     public void setQueryRefinementEnabled(boolean enable) {
    606         mQueryRefinement = enable;
    607         if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
    608             ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
    609                     enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
    610         }
    611     }
    612 
    613     /**
    614      * Returns whether query refinement is enabled for all items or only specific ones.
    615      * @return true if enabled for all items, false otherwise.
    616      */
    617     public boolean isQueryRefinementEnabled() {
    618         return mQueryRefinement;
    619     }
    620 
    621     /**
    622      * You can set a custom adapter if you wish. Otherwise the default adapter is used to
    623      * display the suggestions from the suggestions provider associated with the SearchableInfo.
    624      *
    625      * @see #setSearchableInfo(SearchableInfo)
    626      */
    627     public void setSuggestionsAdapter(CursorAdapter adapter) {
    628         mSuggestionsAdapter = adapter;
    629 
    630         mQueryTextView.setAdapter(mSuggestionsAdapter);
    631     }
    632 
    633     /**
    634      * Returns the adapter used for suggestions, if any.
    635      * @return the suggestions adapter
    636      */
    637     public CursorAdapter getSuggestionsAdapter() {
    638         return mSuggestionsAdapter;
    639     }
    640 
    641     /**
    642      * Makes the view at most this many pixels wide
    643      *
    644      * @attr ref android.R.styleable#SearchView_maxWidth
    645      */
    646     public void setMaxWidth(int maxpixels) {
    647         mMaxWidth = maxpixels;
    648 
    649         requestLayout();
    650     }
    651 
    652     @Override
    653     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    654         // Let the standard measurements take effect in iconified state.
    655         if (isIconified()) {
    656             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    657             return;
    658         }
    659 
    660         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    661         int width = MeasureSpec.getSize(widthMeasureSpec);
    662 
    663         switch (widthMode) {
    664         case MeasureSpec.AT_MOST:
    665             // If there is an upper limit, don't exceed maximum width (explicit or implicit)
    666             if (mMaxWidth > 0) {
    667                 width = Math.min(mMaxWidth, width);
    668             } else {
    669                 width = Math.min(getPreferredWidth(), width);
    670             }
    671             break;
    672         case MeasureSpec.EXACTLY:
    673             // If an exact width is specified, still don't exceed any specified maximum width
    674             if (mMaxWidth > 0) {
    675                 width = Math.min(mMaxWidth, width);
    676             }
    677             break;
    678         case MeasureSpec.UNSPECIFIED:
    679             // Use maximum width, if specified, else preferred width
    680             width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
    681             break;
    682         }
    683         widthMode = MeasureSpec.EXACTLY;
    684         super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), heightMeasureSpec);
    685     }
    686 
    687     private int getPreferredWidth() {
    688         return getContext().getResources()
    689                 .getDimensionPixelSize(R.dimen.search_view_preferred_width);
    690     }
    691 
    692     private void updateViewsVisibility(final boolean collapsed) {
    693         mIconified = collapsed;
    694         // Visibility of views that are visible when collapsed
    695         final int visCollapsed = collapsed ? VISIBLE : GONE;
    696         // Is there text in the query
    697         final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
    698 
    699         mSearchButton.setVisibility(visCollapsed);
    700         updateSubmitButton(hasText);
    701         mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
    702         mSearchHintIcon.setVisibility(mIconifiedByDefault ? GONE : VISIBLE);
    703         updateCloseButton();
    704         updateVoiceButton(!hasText);
    705         updateSubmitArea();
    706     }
    707 
    708     private boolean hasVoiceSearch() {
    709         if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
    710             Intent testIntent = null;
    711             if (mSearchable.getVoiceSearchLaunchWebSearch()) {
    712                 testIntent = mVoiceWebSearchIntent;
    713             } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
    714                 testIntent = mVoiceAppSearchIntent;
    715             }
    716             if (testIntent != null) {
    717                 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
    718                         PackageManager.MATCH_DEFAULT_ONLY);
    719                 return ri != null;
    720             }
    721         }
    722         return false;
    723     }
    724 
    725     private boolean isSubmitAreaEnabled() {
    726         return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
    727     }
    728 
    729     private void updateSubmitButton(boolean hasText) {
    730         int visibility = GONE;
    731         if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
    732                 && (hasText || !mVoiceButtonEnabled)) {
    733             visibility = VISIBLE;
    734         }
    735         mSubmitButton.setVisibility(visibility);
    736     }
    737 
    738     private void updateSubmitArea() {
    739         int visibility = GONE;
    740         if (isSubmitAreaEnabled()
    741                 && (mSubmitButton.getVisibility() == VISIBLE
    742                         || mVoiceButton.getVisibility() == VISIBLE)) {
    743             visibility = VISIBLE;
    744         }
    745         mSubmitArea.setVisibility(visibility);
    746     }
    747 
    748     private void updateCloseButton() {
    749         final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
    750         // Should we show the close button? It is not shown if there's no focus,
    751         // field is not iconified by default and there is no text in it.
    752         final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
    753         mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
    754         mCloseButton.getDrawable().setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
    755     }
    756 
    757     private void postUpdateFocusedState() {
    758         post(mUpdateDrawableStateRunnable);
    759     }
    760 
    761     private void updateFocusedState() {
    762         boolean focused = mQueryTextView.hasFocus();
    763         mSearchPlate.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET);
    764         mSubmitArea.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET);
    765         invalidate();
    766     }
    767 
    768     @Override
    769     protected void onDetachedFromWindow() {
    770         removeCallbacks(mUpdateDrawableStateRunnable);
    771         post(mReleaseCursorRunnable);
    772         super.onDetachedFromWindow();
    773     }
    774 
    775     private void setImeVisibility(final boolean visible) {
    776         if (visible) {
    777             post(mShowImeRunnable);
    778         } else {
    779             removeCallbacks(mShowImeRunnable);
    780             InputMethodManager imm = (InputMethodManager)
    781                     getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    782 
    783             if (imm != null) {
    784                 imm.hideSoftInputFromWindow(getWindowToken(), 0);
    785             }
    786         }
    787     }
    788 
    789     /**
    790      * Called by the SuggestionsAdapter
    791      * @hide
    792      */
    793     /* package */void onQueryRefine(CharSequence queryText) {
    794         setQuery(queryText);
    795     }
    796 
    797     private final OnClickListener mOnClickListener = new OnClickListener() {
    798 
    799         public void onClick(View v) {
    800             if (v == mSearchButton) {
    801                 onSearchClicked();
    802             } else if (v == mCloseButton) {
    803                 onCloseClicked();
    804             } else if (v == mSubmitButton) {
    805                 onSubmitQuery();
    806             } else if (v == mVoiceButton) {
    807                 onVoiceClicked();
    808             } else if (v == mQueryTextView) {
    809                 forceSuggestionQuery();
    810             }
    811         }
    812     };
    813 
    814     /**
    815      * Handles the key down event for dealing with action keys.
    816      *
    817      * @param keyCode This is the keycode of the typed key, and is the same value as
    818      *        found in the KeyEvent parameter.
    819      * @param event The complete event record for the typed key
    820      *
    821      * @return true if the event was handled here, or false if not.
    822      */
    823     @Override
    824     public boolean onKeyDown(int keyCode, KeyEvent event) {
    825         if (mSearchable == null) {
    826             return false;
    827         }
    828 
    829         // if it's an action specified by the searchable activity, launch the
    830         // entered query with the action key
    831         SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
    832         if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
    833             launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText()
    834                     .toString());
    835             return true;
    836         }
    837 
    838         return super.onKeyDown(keyCode, event);
    839     }
    840 
    841     /**
    842      * React to the user typing "enter" or other hardwired keys while typing in
    843      * the search box. This handles these special keys while the edit box has
    844      * focus.
    845      */
    846     View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
    847         public boolean onKey(View v, int keyCode, KeyEvent event) {
    848             // guard against possible race conditions
    849             if (mSearchable == null) {
    850                 return false;
    851             }
    852 
    853             if (DBG) {
    854                 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
    855                         + mQueryTextView.getListSelection());
    856             }
    857 
    858             // If a suggestion is selected, handle enter, search key, and action keys
    859             // as presses on the selected suggestion
    860             if (mQueryTextView.isPopupShowing()
    861                     && mQueryTextView.getListSelection() != ListView.INVALID_POSITION) {
    862                 return onSuggestionsKey(v, keyCode, event);
    863             }
    864 
    865             // If there is text in the query box, handle enter, and action keys
    866             // The search key is handled by the dialog's onKeyDown().
    867             if (!mQueryTextView.isEmpty() && event.hasNoModifiers()) {
    868                 if (event.getAction() == KeyEvent.ACTION_UP) {
    869                     if (keyCode == KeyEvent.KEYCODE_ENTER) {
    870                         v.cancelLongPress();
    871 
    872                         // Launch as a regular search.
    873                         launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
    874                                 .toString());
    875                         return true;
    876                     }
    877                 }
    878                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
    879                     SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
    880                     if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
    881                         launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView
    882                                 .getText().toString());
    883                         return true;
    884                     }
    885                 }
    886             }
    887             return false;
    888         }
    889     };
    890 
    891     /**
    892      * React to the user typing while in the suggestions list. First, check for
    893      * action keys. If not handled, try refocusing regular characters into the
    894      * EditText.
    895      */
    896     private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
    897         // guard against possible race conditions (late arrival after dismiss)
    898         if (mSearchable == null) {
    899             return false;
    900         }
    901         if (mSuggestionsAdapter == null) {
    902             return false;
    903         }
    904         if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
    905             // First, check for enter or search (both of which we'll treat as a
    906             // "click")
    907             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
    908                     || keyCode == KeyEvent.KEYCODE_TAB) {
    909                 int position = mQueryTextView.getListSelection();
    910                 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
    911             }
    912 
    913             // Next, check for left/right moves, which we use to "return" the
    914             // user to the edit view
    915             if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
    916                 // give "focus" to text editor, with cursor at the beginning if
    917                 // left key, at end if right key
    918                 // TODO: Reverse left/right for right-to-left languages, e.g.
    919                 // Arabic
    920                 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView
    921                         .length();
    922                 mQueryTextView.setSelection(selPoint);
    923                 mQueryTextView.setListSelection(0);
    924                 mQueryTextView.clearListSelection();
    925                 mQueryTextView.ensureImeVisible(true);
    926 
    927                 return true;
    928             }
    929 
    930             // Next, check for an "up and out" move
    931             if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mQueryTextView.getListSelection()) {
    932                 // TODO: restoreUserQuery();
    933                 // let ACTV complete the move
    934                 return false;
    935             }
    936 
    937             // Next, check for an "action key"
    938             SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
    939             if ((actionKey != null)
    940                     && ((actionKey.getSuggestActionMsg() != null) || (actionKey
    941                             .getSuggestActionMsgColumn() != null))) {
    942                 // launch suggestion using action key column
    943                 int position = mQueryTextView.getListSelection();
    944                 if (position != ListView.INVALID_POSITION) {
    945                     Cursor c = mSuggestionsAdapter.getCursor();
    946                     if (c.moveToPosition(position)) {
    947                         final String actionMsg = getActionKeyMessage(c, actionKey);
    948                         if (actionMsg != null && (actionMsg.length() > 0)) {
    949                             return onItemClicked(position, keyCode, actionMsg);
    950                         }
    951                     }
    952                 }
    953             }
    954         }
    955         return false;
    956     }
    957 
    958     /**
    959      * For a given suggestion and a given cursor row, get the action message. If
    960      * not provided by the specific row/column, also check for a single
    961      * definition (for the action key).
    962      *
    963      * @param c The cursor providing suggestions
    964      * @param actionKey The actionkey record being examined
    965      *
    966      * @return Returns a string, or null if no action key message for this
    967      *         suggestion
    968      */
    969     private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
    970         String result = null;
    971         // check first in the cursor data, for a suggestion-specific message
    972         final String column = actionKey.getSuggestActionMsgColumn();
    973         if (column != null) {
    974             result = SuggestionsAdapter.getColumnString(c, column);
    975         }
    976         // If the cursor didn't give us a message, see if there's a single
    977         // message defined
    978         // for the actionkey (for all suggestions)
    979         if (result == null) {
    980             result = actionKey.getSuggestActionMsg();
    981         }
    982         return result;
    983     }
    984 
    985     private int getSearchIconId() {
    986         TypedValue outValue = new TypedValue();
    987         getContext().getTheme().resolveAttribute(com.android.internal.R.attr.searchViewSearchIcon,
    988                 outValue, true);
    989         return outValue.resourceId;
    990     }
    991 
    992     private CharSequence getDecoratedHint(CharSequence hintText) {
    993         // If the field is always expanded, then don't add the search icon to the hint
    994         if (!mIconifiedByDefault) return hintText;
    995 
    996         SpannableStringBuilder ssb = new SpannableStringBuilder("   "); // for the icon
    997         ssb.append(hintText);
    998         Drawable searchIcon = getContext().getResources().getDrawable(getSearchIconId());
    999         int textSize = (int) (mQueryTextView.getTextSize() * 1.25);
   1000         searchIcon.setBounds(0, 0, textSize, textSize);
   1001         ssb.setSpan(new ImageSpan(searchIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1002         return ssb;
   1003     }
   1004 
   1005     private void updateQueryHint() {
   1006         if (mQueryHint != null) {
   1007             mQueryTextView.setHint(getDecoratedHint(mQueryHint));
   1008         } else if (mSearchable != null) {
   1009             CharSequence hint = null;
   1010             int hintId = mSearchable.getHintId();
   1011             if (hintId != 0) {
   1012                 hint = getContext().getString(hintId);
   1013             }
   1014             if (hint != null) {
   1015                 mQueryTextView.setHint(getDecoratedHint(hint));
   1016             }
   1017         } else {
   1018             mQueryTextView.setHint(getDecoratedHint(""));
   1019         }
   1020     }
   1021 
   1022     /**
   1023      * Updates the auto-complete text view.
   1024      */
   1025     private void updateSearchAutoComplete() {
   1026         mQueryTextView.setDropDownAnimationStyle(0); // no animation
   1027         mQueryTextView.setThreshold(mSearchable.getSuggestThreshold());
   1028         mQueryTextView.setImeOptions(mSearchable.getImeOptions());
   1029         int inputType = mSearchable.getInputType();
   1030         // We only touch this if the input type is set up for text (which it almost certainly
   1031         // should be, in the case of search!)
   1032         if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
   1033             // The existence of a suggestions authority is the proxy for "suggestions
   1034             // are available here"
   1035             inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
   1036             if (mSearchable.getSuggestAuthority() != null) {
   1037                 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
   1038             }
   1039         }
   1040         mQueryTextView.setInputType(inputType);
   1041         if (mSuggestionsAdapter != null) {
   1042             mSuggestionsAdapter.changeCursor(null);
   1043         }
   1044         // attach the suggestions adapter, if suggestions are available
   1045         // The existence of a suggestions authority is the proxy for "suggestions available here"
   1046         if (mSearchable.getSuggestAuthority() != null) {
   1047             mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
   1048                     this, mSearchable, mOutsideDrawablesCache);
   1049             mQueryTextView.setAdapter(mSuggestionsAdapter);
   1050             ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
   1051                     mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
   1052                     : SuggestionsAdapter.REFINE_BY_ENTRY);
   1053         }
   1054     }
   1055 
   1056     /**
   1057      * Update the visibility of the voice button.  There are actually two voice search modes,
   1058      * either of which will activate the button.
   1059      * @param empty whether the search query text field is empty. If it is, then the other
   1060      * criteria apply to make the voice button visible.
   1061      */
   1062     private void updateVoiceButton(boolean empty) {
   1063         int visibility = GONE;
   1064         if (mVoiceButtonEnabled && !isIconified() && empty) {
   1065             visibility = VISIBLE;
   1066             mSubmitButton.setVisibility(GONE);
   1067         }
   1068         mVoiceButton.setVisibility(visibility);
   1069     }
   1070 
   1071     private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
   1072 
   1073         /**
   1074          * Called when the input method default action key is pressed.
   1075          */
   1076         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
   1077             onSubmitQuery();
   1078             return true;
   1079         }
   1080     };
   1081 
   1082     private void onTextChanged(CharSequence newText) {
   1083         CharSequence text = mQueryTextView.getText();
   1084         mUserQuery = text;
   1085         boolean hasText = !TextUtils.isEmpty(text);
   1086         updateSubmitButton(hasText);
   1087         updateVoiceButton(!hasText);
   1088         updateCloseButton();
   1089         updateSubmitArea();
   1090         if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
   1091             mOnQueryChangeListener.onQueryTextChange(newText.toString());
   1092         }
   1093         mOldQueryText = newText.toString();
   1094     }
   1095 
   1096     private void onSubmitQuery() {
   1097         CharSequence query = mQueryTextView.getText();
   1098         if (query != null && TextUtils.getTrimmedLength(query) > 0) {
   1099             if (mOnQueryChangeListener == null
   1100                     || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
   1101                 if (mSearchable != null) {
   1102                     launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
   1103                     setImeVisibility(false);
   1104                 }
   1105                 dismissSuggestions();
   1106             }
   1107         }
   1108     }
   1109 
   1110     private void dismissSuggestions() {
   1111         mQueryTextView.dismissDropDown();
   1112     }
   1113 
   1114     private void onCloseClicked() {
   1115         CharSequence text = mQueryTextView.getText();
   1116         if (TextUtils.isEmpty(text)) {
   1117             if (mIconifiedByDefault) {
   1118                 // If the app doesn't override the close behavior
   1119                 if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
   1120                     // hide the keyboard and remove focus
   1121                     clearFocus();
   1122                     // collapse the search field
   1123                     updateViewsVisibility(true);
   1124                 }
   1125             }
   1126         } else {
   1127             mQueryTextView.setText("");
   1128             mQueryTextView.requestFocus();
   1129             setImeVisibility(true);
   1130         }
   1131 
   1132     }
   1133 
   1134     private void onSearchClicked() {
   1135         updateViewsVisibility(false);
   1136         mQueryTextView.requestFocus();
   1137         setImeVisibility(true);
   1138         if (mOnSearchClickListener != null) {
   1139             mOnSearchClickListener.onClick(this);
   1140         }
   1141     }
   1142 
   1143     private void onVoiceClicked() {
   1144         // guard against possible race conditions
   1145         if (mSearchable == null) {
   1146             return;
   1147         }
   1148         SearchableInfo searchable = mSearchable;
   1149         try {
   1150             if (searchable.getVoiceSearchLaunchWebSearch()) {
   1151                 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
   1152                         searchable);
   1153                 getContext().startActivity(webSearchIntent);
   1154             } else if (searchable.getVoiceSearchLaunchRecognizer()) {
   1155                 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
   1156                         searchable);
   1157                 getContext().startActivity(appSearchIntent);
   1158             }
   1159         } catch (ActivityNotFoundException e) {
   1160             // Should not happen, since we check the availability of
   1161             // voice search before showing the button. But just in case...
   1162             Log.w(LOG_TAG, "Could not find voice search activity");
   1163         }
   1164     }
   1165 
   1166     void onTextFocusChanged() {
   1167         updateViewsVisibility(isIconified());
   1168         // Delayed update to make sure that the focus has settled down and window focus changes
   1169         // don't affect it. A synchronous update was not working.
   1170         postUpdateFocusedState();
   1171         if (mQueryTextView.hasFocus()) {
   1172             forceSuggestionQuery();
   1173         }
   1174     }
   1175 
   1176     @Override
   1177     public void onWindowFocusChanged(boolean hasWindowFocus) {
   1178         super.onWindowFocusChanged(hasWindowFocus);
   1179 
   1180         postUpdateFocusedState();
   1181     }
   1182 
   1183     /**
   1184      * {@inheritDoc}
   1185      */
   1186     @Override
   1187     public void onActionViewCollapsed() {
   1188         clearFocus();
   1189         updateViewsVisibility(true);
   1190         mQueryTextView.setImeOptions(mCollapsedImeOptions);
   1191         mExpandedInActionView = false;
   1192     }
   1193 
   1194     /**
   1195      * {@inheritDoc}
   1196      */
   1197     @Override
   1198     public void onActionViewExpanded() {
   1199         if (mExpandedInActionView) return;
   1200 
   1201         mExpandedInActionView = true;
   1202         mCollapsedImeOptions = mQueryTextView.getImeOptions();
   1203         mQueryTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
   1204         mQueryTextView.setText("");
   1205         setIconified(false);
   1206     }
   1207 
   1208     private void adjustDropDownSizeAndPosition() {
   1209         if (mDropDownAnchor.getWidth() > 1) {
   1210             Resources res = getContext().getResources();
   1211             int anchorPadding = mSearchPlate.getPaddingLeft();
   1212             Rect dropDownPadding = new Rect();
   1213             int iconOffset = mIconifiedByDefault
   1214                     ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
   1215                     + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
   1216                     : 0;
   1217             mQueryTextView.getDropDownBackground().getPadding(dropDownPadding);
   1218             mQueryTextView.setDropDownHorizontalOffset(-(dropDownPadding.left + iconOffset)
   1219                     + anchorPadding);
   1220             mQueryTextView.setDropDownWidth(mDropDownAnchor.getWidth() + dropDownPadding.left
   1221                     + dropDownPadding.right + iconOffset - (anchorPadding));
   1222         }
   1223     }
   1224 
   1225     private boolean onItemClicked(int position, int actionKey, String actionMsg) {
   1226         if (mOnSuggestionListener == null
   1227                 || !mOnSuggestionListener.onSuggestionClick(position)) {
   1228             launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
   1229             setImeVisibility(false);
   1230             dismissSuggestions();
   1231             return true;
   1232         }
   1233         return false;
   1234     }
   1235 
   1236     private boolean onItemSelected(int position) {
   1237         if (mOnSuggestionListener == null
   1238                 || !mOnSuggestionListener.onSuggestionSelect(position)) {
   1239             rewriteQueryFromSuggestion(position);
   1240             return true;
   1241         }
   1242         return false;
   1243     }
   1244 
   1245     private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
   1246 
   1247         /**
   1248          * Implements OnItemClickListener
   1249          */
   1250         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   1251             if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
   1252             onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
   1253         }
   1254     };
   1255 
   1256     private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
   1257 
   1258         /**
   1259          * Implements OnItemSelectedListener
   1260          */
   1261         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
   1262             if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
   1263             SearchView.this.onItemSelected(position);
   1264         }
   1265 
   1266         /**
   1267          * Implements OnItemSelectedListener
   1268          */
   1269         public void onNothingSelected(AdapterView<?> parent) {
   1270             if (DBG)
   1271                 Log.d(LOG_TAG, "onNothingSelected()");
   1272         }
   1273     };
   1274 
   1275     /**
   1276      * Query rewriting.
   1277      */
   1278     private void rewriteQueryFromSuggestion(int position) {
   1279         CharSequence oldQuery = mQueryTextView.getText();
   1280         Cursor c = mSuggestionsAdapter.getCursor();
   1281         if (c == null) {
   1282             return;
   1283         }
   1284         if (c.moveToPosition(position)) {
   1285             // Get the new query from the suggestion.
   1286             CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
   1287             if (newQuery != null) {
   1288                 // The suggestion rewrites the query.
   1289                 // Update the text field, without getting new suggestions.
   1290                 setQuery(newQuery);
   1291             } else {
   1292                 // The suggestion does not rewrite the query, restore the user's query.
   1293                 setQuery(oldQuery);
   1294             }
   1295         } else {
   1296             // We got a bad position, restore the user's query.
   1297             setQuery(oldQuery);
   1298         }
   1299     }
   1300 
   1301     /**
   1302      * Launches an intent based on a suggestion.
   1303      *
   1304      * @param position The index of the suggestion to create the intent from.
   1305      * @param actionKey The key code of the action key that was pressed,
   1306      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1307      * @param actionMsg The message for the action key that was pressed,
   1308      *        or <code>null</code> if none.
   1309      * @return true if a successful launch, false if could not (e.g. bad position).
   1310      */
   1311     private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
   1312         Cursor c = mSuggestionsAdapter.getCursor();
   1313         if ((c != null) && c.moveToPosition(position)) {
   1314 
   1315             Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
   1316 
   1317             // launch the intent
   1318             launchIntent(intent);
   1319 
   1320             return true;
   1321         }
   1322         return false;
   1323     }
   1324 
   1325     /**
   1326      * Launches an intent, including any special intent handling.
   1327      */
   1328     private void launchIntent(Intent intent) {
   1329         if (intent == null) {
   1330             return;
   1331         }
   1332         try {
   1333             // If the intent was created from a suggestion, it will always have an explicit
   1334             // component here.
   1335             getContext().startActivity(intent);
   1336         } catch (RuntimeException ex) {
   1337             Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
   1338         }
   1339     }
   1340 
   1341     /**
   1342      * Sets the text in the query box, without updating the suggestions.
   1343      */
   1344     private void setQuery(CharSequence query) {
   1345         mQueryTextView.setText(query, true);
   1346         // Move the cursor to the end
   1347         mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
   1348     }
   1349 
   1350     private void launchQuerySearch(int actionKey, String actionMsg, String query) {
   1351         String action = Intent.ACTION_SEARCH;
   1352         Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
   1353         getContext().startActivity(intent);
   1354     }
   1355 
   1356     /**
   1357      * Constructs an intent from the given information and the search dialog state.
   1358      *
   1359      * @param action Intent action.
   1360      * @param data Intent data, or <code>null</code>.
   1361      * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
   1362      * @param query Intent query, or <code>null</code>.
   1363      * @param actionKey The key code of the action key that was pressed,
   1364      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1365      * @param actionMsg The message for the action key that was pressed,
   1366      *        or <code>null</code> if none.
   1367      * @param mode The search mode, one of the acceptable values for
   1368      *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
   1369      * @return The intent.
   1370      */
   1371     private Intent createIntent(String action, Uri data, String extraData, String query,
   1372             int actionKey, String actionMsg) {
   1373         // Now build the Intent
   1374         Intent intent = new Intent(action);
   1375         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   1376         // We need CLEAR_TOP to avoid reusing an old task that has other activities
   1377         // on top of the one we want. We don't want to do this in in-app search though,
   1378         // as it can be destructive to the activity stack.
   1379         if (data != null) {
   1380             intent.setData(data);
   1381         }
   1382         intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
   1383         if (query != null) {
   1384             intent.putExtra(SearchManager.QUERY, query);
   1385         }
   1386         if (extraData != null) {
   1387             intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
   1388         }
   1389         if (mAppSearchData != null) {
   1390             intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
   1391         }
   1392         if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
   1393             intent.putExtra(SearchManager.ACTION_KEY, actionKey);
   1394             intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
   1395         }
   1396         intent.setComponent(mSearchable.getSearchActivity());
   1397         return intent;
   1398     }
   1399 
   1400     /**
   1401      * Create and return an Intent that can launch the voice search activity for web search.
   1402      */
   1403     private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
   1404         Intent voiceIntent = new Intent(baseIntent);
   1405         ComponentName searchActivity = searchable.getSearchActivity();
   1406         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
   1407                 : searchActivity.flattenToShortString());
   1408         return voiceIntent;
   1409     }
   1410 
   1411     /**
   1412      * Create and return an Intent that can launch the voice search activity, perform a specific
   1413      * voice transcription, and forward the results to the searchable activity.
   1414      *
   1415      * @param baseIntent The voice app search intent to start from
   1416      * @return A completely-configured intent ready to send to the voice search activity
   1417      */
   1418     private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
   1419         ComponentName searchActivity = searchable.getSearchActivity();
   1420 
   1421         // create the necessary intent to set up a search-and-forward operation
   1422         // in the voice search system.   We have to keep the bundle separate,
   1423         // because it becomes immutable once it enters the PendingIntent
   1424         Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
   1425         queryIntent.setComponent(searchActivity);
   1426         PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
   1427                 PendingIntent.FLAG_ONE_SHOT);
   1428 
   1429         // Now set up the bundle that will be inserted into the pending intent
   1430         // when it's time to do the search.  We always build it here (even if empty)
   1431         // because the voice search activity will always need to insert "QUERY" into
   1432         // it anyway.
   1433         Bundle queryExtras = new Bundle();
   1434 
   1435         // Now build the intent to launch the voice search.  Add all necessary
   1436         // extras to launch the voice recognizer, and then all the necessary extras
   1437         // to forward the results to the searchable activity
   1438         Intent voiceIntent = new Intent(baseIntent);
   1439 
   1440         // Add all of the configuration options supplied by the searchable's metadata
   1441         String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
   1442         String prompt = null;
   1443         String language = null;
   1444         int maxResults = 1;
   1445 
   1446         Resources resources = getResources();
   1447         if (searchable.getVoiceLanguageModeId() != 0) {
   1448             languageModel = resources.getString(searchable.getVoiceLanguageModeId());
   1449         }
   1450         if (searchable.getVoicePromptTextId() != 0) {
   1451             prompt = resources.getString(searchable.getVoicePromptTextId());
   1452         }
   1453         if (searchable.getVoiceLanguageId() != 0) {
   1454             language = resources.getString(searchable.getVoiceLanguageId());
   1455         }
   1456         if (searchable.getVoiceMaxResults() != 0) {
   1457             maxResults = searchable.getVoiceMaxResults();
   1458         }
   1459         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
   1460         voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
   1461         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
   1462         voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
   1463         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
   1464                 : searchActivity.flattenToShortString());
   1465 
   1466         // Add the values that configure forwarding the results
   1467         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
   1468         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
   1469 
   1470         return voiceIntent;
   1471     }
   1472 
   1473     /**
   1474      * When a particular suggestion has been selected, perform the various lookups required
   1475      * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
   1476      * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
   1477      * the suggestion includes a data id.
   1478      *
   1479      * @param c The suggestions cursor, moved to the row of the user's selection
   1480      * @param actionKey The key code of the action key that was pressed,
   1481      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1482      * @param actionMsg The message for the action key that was pressed,
   1483      *        or <code>null</code> if none.
   1484      * @return An intent for the suggestion at the cursor's position.
   1485      */
   1486     private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
   1487         try {
   1488             // use specific action if supplied, or default action if supplied, or fixed default
   1489             String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
   1490 
   1491             if (action == null) {
   1492                 action = mSearchable.getSuggestIntentAction();
   1493             }
   1494             if (action == null) {
   1495                 action = Intent.ACTION_SEARCH;
   1496             }
   1497 
   1498             // use specific data if supplied, or default data if supplied
   1499             String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
   1500             if (data == null) {
   1501                 data = mSearchable.getSuggestIntentData();
   1502             }
   1503             // then, if an ID was provided, append it.
   1504             if (data != null) {
   1505                 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
   1506                 if (id != null) {
   1507                     data = data + "/" + Uri.encode(id);
   1508                 }
   1509             }
   1510             Uri dataUri = (data == null) ? null : Uri.parse(data);
   1511 
   1512             String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
   1513             String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
   1514 
   1515             return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
   1516         } catch (RuntimeException e ) {
   1517             int rowNum;
   1518             try {                       // be really paranoid now
   1519                 rowNum = c.getPosition();
   1520             } catch (RuntimeException e2 ) {
   1521                 rowNum = -1;
   1522             }
   1523             Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
   1524                             " returned exception" + e.toString());
   1525             return null;
   1526         }
   1527     }
   1528 
   1529     private void forceSuggestionQuery() {
   1530         mQueryTextView.doBeforeTextChanged();
   1531         mQueryTextView.doAfterTextChanged();
   1532     }
   1533 
   1534     static boolean isLandscapeMode(Context context) {
   1535         return context.getResources().getConfiguration().orientation
   1536                 == Configuration.ORIENTATION_LANDSCAPE;
   1537     }
   1538 
   1539     /**
   1540      * Callback to watch the text field for empty/non-empty
   1541      */
   1542     private TextWatcher mTextWatcher = new TextWatcher() {
   1543 
   1544         public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
   1545 
   1546         public void onTextChanged(CharSequence s, int start,
   1547                 int before, int after) {
   1548             SearchView.this.onTextChanged(s);
   1549         }
   1550 
   1551         public void afterTextChanged(Editable s) {
   1552         }
   1553     };
   1554 
   1555     /**
   1556      * Local subclass for AutoCompleteTextView.
   1557      * @hide
   1558      */
   1559     public static class SearchAutoComplete extends AutoCompleteTextView {
   1560 
   1561         private int mThreshold;
   1562         private SearchView mSearchView;
   1563 
   1564         public SearchAutoComplete(Context context) {
   1565             super(context);
   1566             mThreshold = getThreshold();
   1567         }
   1568 
   1569         public SearchAutoComplete(Context context, AttributeSet attrs) {
   1570             super(context, attrs);
   1571             mThreshold = getThreshold();
   1572         }
   1573 
   1574         public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
   1575             super(context, attrs, defStyle);
   1576             mThreshold = getThreshold();
   1577         }
   1578 
   1579         void setSearchView(SearchView searchView) {
   1580             mSearchView = searchView;
   1581         }
   1582 
   1583         @Override
   1584         public void setThreshold(int threshold) {
   1585             super.setThreshold(threshold);
   1586             mThreshold = threshold;
   1587         }
   1588 
   1589         /**
   1590          * Returns true if the text field is empty, or contains only whitespace.
   1591          */
   1592         private boolean isEmpty() {
   1593             return TextUtils.getTrimmedLength(getText()) == 0;
   1594         }
   1595 
   1596         /**
   1597          * We override this method to avoid replacing the query box text when a
   1598          * suggestion is clicked.
   1599          */
   1600         @Override
   1601         protected void replaceText(CharSequence text) {
   1602         }
   1603 
   1604         /**
   1605          * We override this method to avoid an extra onItemClick being called on
   1606          * the drop-down's OnItemClickListener by
   1607          * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
   1608          * clicked with the trackball.
   1609          */
   1610         @Override
   1611         public void performCompletion() {
   1612         }
   1613 
   1614         /**
   1615          * We override this method to be sure and show the soft keyboard if
   1616          * appropriate when the TextView has focus.
   1617          */
   1618         @Override
   1619         public void onWindowFocusChanged(boolean hasWindowFocus) {
   1620             super.onWindowFocusChanged(hasWindowFocus);
   1621 
   1622             if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
   1623                 InputMethodManager inputManager = (InputMethodManager) getContext()
   1624                         .getSystemService(Context.INPUT_METHOD_SERVICE);
   1625                 inputManager.showSoftInput(this, 0);
   1626                 // If in landscape mode, then make sure that
   1627                 // the ime is in front of the dropdown.
   1628                 if (isLandscapeMode(getContext())) {
   1629                     ensureImeVisible(true);
   1630                 }
   1631             }
   1632         }
   1633 
   1634         @Override
   1635         protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
   1636             super.onFocusChanged(focused, direction, previouslyFocusedRect);
   1637             mSearchView.onTextFocusChanged();
   1638         }
   1639 
   1640         /**
   1641          * We override this method so that we can allow a threshold of zero,
   1642          * which ACTV does not.
   1643          */
   1644         @Override
   1645         public boolean enoughToFilter() {
   1646             return mThreshold <= 0 || super.enoughToFilter();
   1647         }
   1648 
   1649         @Override
   1650         public boolean onKeyPreIme(int keyCode, KeyEvent event) {
   1651             if (keyCode == KeyEvent.KEYCODE_BACK) {
   1652                 // special case for the back key, we do not even try to send it
   1653                 // to the drop down list but instead, consume it immediately
   1654                 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
   1655                     KeyEvent.DispatcherState state = getKeyDispatcherState();
   1656                     if (state != null) {
   1657                         state.startTracking(event, this);
   1658                     }
   1659                     return true;
   1660                 } else if (event.getAction() == KeyEvent.ACTION_UP) {
   1661                     KeyEvent.DispatcherState state = getKeyDispatcherState();
   1662                     if (state != null) {
   1663                         state.handleUpEvent(event);
   1664                     }
   1665                     if (event.isTracking() && !event.isCanceled()) {
   1666                         mSearchView.clearFocus();
   1667                         mSearchView.setImeVisibility(false);
   1668                         return true;
   1669                     }
   1670                 }
   1671             }
   1672             return super.onKeyPreIme(keyCode, event);
   1673         }
   1674 
   1675     }
   1676 }
   1677