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