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