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.annotation.Nullable;
     22 import android.annotation.UnsupportedAppUsage;
     23 import android.app.PendingIntent;
     24 import android.app.SearchManager;
     25 import android.app.SearchableInfo;
     26 import android.content.ActivityNotFoundException;
     27 import android.content.ComponentName;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.pm.PackageManager;
     31 import android.content.pm.ResolveInfo;
     32 import android.content.res.Configuration;
     33 import android.content.res.Resources;
     34 import android.content.res.TypedArray;
     35 import android.database.Cursor;
     36 import android.graphics.Rect;
     37 import android.graphics.drawable.Drawable;
     38 import android.net.Uri;
     39 import android.os.Build;
     40 import android.os.Bundle;
     41 import android.os.Parcel;
     42 import android.os.Parcelable;
     43 import android.speech.RecognizerIntent;
     44 import android.text.Editable;
     45 import android.text.InputType;
     46 import android.text.Spannable;
     47 import android.text.SpannableStringBuilder;
     48 import android.text.TextUtils;
     49 import android.text.TextWatcher;
     50 import android.text.style.ImageSpan;
     51 import android.util.AttributeSet;
     52 import android.util.DisplayMetrics;
     53 import android.util.Log;
     54 import android.util.TypedValue;
     55 import android.view.CollapsibleActionView;
     56 import android.view.KeyEvent;
     57 import android.view.LayoutInflater;
     58 import android.view.MotionEvent;
     59 import android.view.TouchDelegate;
     60 import android.view.View;
     61 import android.view.ViewConfiguration;
     62 import android.view.inputmethod.EditorInfo;
     63 import android.view.inputmethod.InputConnection;
     64 import android.view.inputmethod.InputMethodManager;
     65 import android.view.inspector.InspectableProperty;
     66 import android.widget.AdapterView.OnItemClickListener;
     67 import android.widget.AdapterView.OnItemSelectedListener;
     68 import android.widget.TextView.OnEditorActionListener;
     69 
     70 import com.android.internal.R;
     71 
     72 import java.util.WeakHashMap;
     73 
     74 /**
     75  * A widget that provides a user interface for the user to enter a search query and submit a request
     76  * to a search provider. Shows a list of query suggestions or results, if available, and allows the
     77  * user to pick a suggestion or result to launch into.
     78  *
     79  * <p>
     80  * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
     81  * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
     82  * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
     83  * </p>
     84  * <p>
     85  * If you want the search field to always be visible, then call setIconifiedByDefault(false).
     86  * </p>
     87  *
     88  * <div class="special reference">
     89  * <h3>Developer Guides</h3>
     90  * <p>For information about using {@code SearchView}, read the
     91  * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
     92  * </div>
     93  *
     94  * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
     95  * @attr ref android.R.styleable#SearchView_iconifiedByDefault
     96  * @attr ref android.R.styleable#SearchView_imeOptions
     97  * @attr ref android.R.styleable#SearchView_inputType
     98  * @attr ref android.R.styleable#SearchView_maxWidth
     99  * @attr ref android.R.styleable#SearchView_queryHint
    100  */
    101 public class SearchView extends LinearLayout implements CollapsibleActionView {
    102 
    103     private static final boolean DBG = false;
    104     private static final String LOG_TAG = "SearchView";
    105 
    106     /**
    107      * Private constant for removing the microphone in the keyboard.
    108      */
    109     private static final String IME_OPTION_NO_MICROPHONE = "nm";
    110 
    111     @UnsupportedAppUsage
    112     private final SearchAutoComplete mSearchSrcTextView;
    113     @UnsupportedAppUsage
    114     private final View mSearchEditFrame;
    115     @UnsupportedAppUsage
    116     private final View mSearchPlate;
    117     @UnsupportedAppUsage
    118     private final View mSubmitArea;
    119     @UnsupportedAppUsage
    120     private final ImageView mSearchButton;
    121     private final ImageView mGoButton;
    122     @UnsupportedAppUsage
    123     private final ImageView mCloseButton;
    124     @UnsupportedAppUsage
    125     private final ImageView mVoiceButton;
    126     private final View mDropDownAnchor;
    127 
    128     private UpdatableTouchDelegate mTouchDelegate;
    129     private Rect mSearchSrcTextViewBounds = new Rect();
    130     private Rect mSearchSrtTextViewBoundsExpanded = new Rect();
    131     private int[] mTemp = new int[2];
    132     private int[] mTemp2 = new int[2];
    133 
    134     /** Icon optionally displayed when the SearchView is collapsed. */
    135     private final ImageView mCollapsedIcon;
    136 
    137     /** Drawable used as an EditText hint. */
    138     @UnsupportedAppUsage
    139     private final Drawable mSearchHintIcon;
    140 
    141     // Resources used by SuggestionsAdapter to display suggestions.
    142     private final int mSuggestionRowLayout;
    143     private final int mSuggestionCommitIconResId;
    144 
    145     // Intents used for voice searching.
    146     private final Intent mVoiceWebSearchIntent;
    147     private final Intent mVoiceAppSearchIntent;
    148 
    149     private final CharSequence mDefaultQueryHint;
    150 
    151     @UnsupportedAppUsage
    152     private OnQueryTextListener mOnQueryChangeListener;
    153     private OnCloseListener mOnCloseListener;
    154     private OnFocusChangeListener mOnQueryTextFocusChangeListener;
    155     private OnSuggestionListener mOnSuggestionListener;
    156     private OnClickListener mOnSearchClickListener;
    157 
    158     @UnsupportedAppUsage
    159     private boolean mIconifiedByDefault;
    160     @UnsupportedAppUsage
    161     private boolean mIconified;
    162     @UnsupportedAppUsage
    163     private CursorAdapter mSuggestionsAdapter;
    164     private boolean mSubmitButtonEnabled;
    165     private CharSequence mQueryHint;
    166     private boolean mQueryRefinement;
    167     @UnsupportedAppUsage
    168     private boolean mClearingFocus;
    169     private int mMaxWidth;
    170     @UnsupportedAppUsage
    171     private boolean mVoiceButtonEnabled;
    172     private CharSequence mOldQueryText;
    173     @UnsupportedAppUsage
    174     private CharSequence mUserQuery;
    175     @UnsupportedAppUsage
    176     private boolean mExpandedInActionView;
    177     @UnsupportedAppUsage
    178     private int mCollapsedImeOptions;
    179 
    180     private SearchableInfo mSearchable;
    181     private Bundle mAppSearchData;
    182 
    183     private Runnable mUpdateDrawableStateRunnable = new Runnable() {
    184         public void run() {
    185             updateFocusedState();
    186         }
    187     };
    188 
    189     private Runnable mReleaseCursorRunnable = new Runnable() {
    190         public void run() {
    191             if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) {
    192                 mSuggestionsAdapter.changeCursor(null);
    193             }
    194         }
    195     };
    196 
    197     // A weak map of drawables we've gotten from other packages, so we don't load them
    198     // more than once.
    199     private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
    200             new WeakHashMap<String, Drawable.ConstantState>();
    201 
    202     /**
    203      * Callbacks for changes to the query text.
    204      */
    205     public interface OnQueryTextListener {
    206 
    207         /**
    208          * Called when the user submits the query. This could be due to a key press on the
    209          * keyboard or due to pressing a submit button.
    210          * The listener can override the standard behavior by returning true
    211          * to indicate that it has handled the submit request. Otherwise return false to
    212          * let the SearchView handle the submission by launching any associated intent.
    213          *
    214          * @param query the query text that is to be submitted
    215          *
    216          * @return true if the query has been handled by the listener, false to let the
    217          * SearchView perform the default action.
    218          */
    219         boolean onQueryTextSubmit(String query);
    220 
    221         /**
    222          * Called when the query text is changed by the user.
    223          *
    224          * @param newText the new content of the query text field.
    225          *
    226          * @return false if the SearchView should perform the default action of showing any
    227          * suggestions if available, true if the action was handled by the listener.
    228          */
    229         boolean onQueryTextChange(String newText);
    230     }
    231 
    232     public interface OnCloseListener {
    233 
    234         /**
    235          * The user is attempting to close the SearchView.
    236          *
    237          * @return true if the listener wants to override the default behavior of clearing the
    238          * text field and dismissing it, false otherwise.
    239          */
    240         boolean onClose();
    241     }
    242 
    243     /**
    244      * Callback interface for selection events on suggestions. These callbacks
    245      * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
    246      */
    247     public interface OnSuggestionListener {
    248 
    249         /**
    250          * Called when a suggestion was selected by navigating to it.
    251          * @param position the absolute position in the list of suggestions.
    252          *
    253          * @return true if the listener handles the event and wants to override the default
    254          * behavior of possibly rewriting the query based on the selected item, false otherwise.
    255          */
    256         boolean onSuggestionSelect(int position);
    257 
    258         /**
    259          * Called when a suggestion was clicked.
    260          * @param position the absolute position of the clicked item in the list of suggestions.
    261          *
    262          * @return true if the listener handles the event and wants to override the default
    263          * behavior of launching any intent or submitting a search query specified on that item.
    264          * Return false otherwise.
    265          */
    266         boolean onSuggestionClick(int position);
    267     }
    268 
    269     public SearchView(Context context) {
    270         this(context, null);
    271     }
    272 
    273     public SearchView(Context context, AttributeSet attrs) {
    274         this(context, attrs, R.attr.searchViewStyle);
    275     }
    276 
    277     public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
    278         this(context, attrs, defStyleAttr, 0);
    279     }
    280 
    281     public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    282         super(context, attrs, defStyleAttr, defStyleRes);
    283 
    284         final TypedArray a = context.obtainStyledAttributes(
    285                 attrs, R.styleable.SearchView, defStyleAttr, defStyleRes);
    286         saveAttributeDataForStyleable(context, R.styleable.SearchView,
    287                 attrs, a, defStyleAttr, defStyleRes);
    288         final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
    289                 Context.LAYOUT_INFLATER_SERVICE);
    290         final int layoutResId = a.getResourceId(
    291                 R.styleable.SearchView_layout, R.layout.search_view);
    292         inflater.inflate(layoutResId, this, true);
    293 
    294         mSearchSrcTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
    295         mSearchSrcTextView.setSearchView(this);
    296 
    297         mSearchEditFrame = findViewById(R.id.search_edit_frame);
    298         mSearchPlate = findViewById(R.id.search_plate);
    299         mSubmitArea = findViewById(R.id.submit_area);
    300         mSearchButton = (ImageView) findViewById(R.id.search_button);
    301         mGoButton = (ImageView) findViewById(R.id.search_go_btn);
    302         mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
    303         mVoiceButton = (ImageView) findViewById(R.id.search_voice_btn);
    304         mCollapsedIcon = (ImageView) findViewById(R.id.search_mag_icon);
    305 
    306         // Set up icons and backgrounds.
    307         mSearchPlate.setBackground(a.getDrawable(R.styleable.SearchView_queryBackground));
    308         mSubmitArea.setBackground(a.getDrawable(R.styleable.SearchView_submitBackground));
    309         mSearchButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
    310         mGoButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_goIcon));
    311         mCloseButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_closeIcon));
    312         mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_voiceIcon));
    313         mCollapsedIcon.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
    314 
    315         // Prior to L MR1, the search hint icon defaulted to searchIcon. If the
    316         // style does not have an explicit value set, fall back to that.
    317         if (a.hasValueOrEmpty(R.styleable.SearchView_searchHintIcon)) {
    318             mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchHintIcon);
    319         } else {
    320             mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchIcon);
    321         }
    322 
    323         // Extract dropdown layout resource IDs for later use.
    324         mSuggestionRowLayout = a.getResourceId(R.styleable.SearchView_suggestionRowLayout,
    325                 R.layout.search_dropdown_item_icons_2line);
    326         mSuggestionCommitIconResId = a.getResourceId(R.styleable.SearchView_commitIcon, 0);
    327 
    328         mSearchButton.setOnClickListener(mOnClickListener);
    329         mCloseButton.setOnClickListener(mOnClickListener);
    330         mGoButton.setOnClickListener(mOnClickListener);
    331         mVoiceButton.setOnClickListener(mOnClickListener);
    332         mSearchSrcTextView.setOnClickListener(mOnClickListener);
    333 
    334         mSearchSrcTextView.addTextChangedListener(mTextWatcher);
    335         mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener);
    336         mSearchSrcTextView.setOnItemClickListener(mOnItemClickListener);
    337         mSearchSrcTextView.setOnItemSelectedListener(mOnItemSelectedListener);
    338         mSearchSrcTextView.setOnKeyListener(mTextKeyListener);
    339 
    340         // Inform any listener of focus changes
    341         mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
    342 
    343             public void onFocusChange(View v, boolean hasFocus) {
    344                 if (mOnQueryTextFocusChangeListener != null) {
    345                     mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
    346                 }
    347             }
    348         });
    349         setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
    350 
    351         final int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
    352         if (maxWidth != -1) {
    353             setMaxWidth(maxWidth);
    354         }
    355 
    356         mDefaultQueryHint = a.getText(R.styleable.SearchView_defaultQueryHint);
    357         mQueryHint = a.getText(R.styleable.SearchView_queryHint);
    358 
    359         final int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
    360         if (imeOptions != -1) {
    361             setImeOptions(imeOptions);
    362         }
    363 
    364         final int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
    365         if (inputType != -1) {
    366             setInputType(inputType);
    367         }
    368 
    369         if (getFocusable() == FOCUSABLE_AUTO) {
    370             setFocusable(FOCUSABLE);
    371         }
    372 
    373         a.recycle();
    374 
    375         // Save voice intent for later queries/launching
    376         mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
    377         mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    378         mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
    379                 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
    380 
    381         mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    382         mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    383 
    384         mDropDownAnchor = findViewById(mSearchSrcTextView.getDropDownAnchor());
    385         if (mDropDownAnchor != null) {
    386             mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
    387                 @Override
    388                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
    389                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
    390                     adjustDropDownSizeAndPosition();
    391                 }
    392             });
    393         }
    394 
    395         updateViewsVisibility(mIconifiedByDefault);
    396         updateQueryHint();
    397     }
    398 
    399     int getSuggestionRowLayout() {
    400         return mSuggestionRowLayout;
    401     }
    402 
    403     int getSuggestionCommitIconResId() {
    404         return mSuggestionCommitIconResId;
    405     }
    406 
    407     /**
    408      * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
    409      * to display labels, hints, suggestions, create intents for launching search results screens
    410      * and controlling other affordances such as a voice button.
    411      *
    412      * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
    413      * activity or a global search provider.
    414      */
    415     public void setSearchableInfo(SearchableInfo searchable) {
    416         mSearchable = searchable;
    417         if (mSearchable != null) {
    418             updateSearchAutoComplete();
    419             updateQueryHint();
    420         }
    421         // Cache the voice search capability
    422         mVoiceButtonEnabled = hasVoiceSearch();
    423 
    424         if (mVoiceButtonEnabled) {
    425             // Disable the microphone on the keyboard, as a mic is displayed near the text box
    426             // TODO: use imeOptions to disable voice input when the new API will be available
    427             mSearchSrcTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
    428         }
    429         updateViewsVisibility(isIconified());
    430     }
    431 
    432     /**
    433      * Sets the APP_DATA for legacy SearchDialog use.
    434      * @param appSearchData bundle provided by the app when launching the search dialog
    435      * @hide
    436      */
    437     public void setAppSearchData(Bundle appSearchData) {
    438         mAppSearchData = appSearchData;
    439     }
    440 
    441     /**
    442      * Sets the IME options on the query text field.
    443      *
    444      * @see TextView#setImeOptions(int)
    445      * @param imeOptions the options to set on the query text field
    446      *
    447      * @attr ref android.R.styleable#SearchView_imeOptions
    448      */
    449     public void setImeOptions(int imeOptions) {
    450         mSearchSrcTextView.setImeOptions(imeOptions);
    451     }
    452 
    453     /**
    454      * Returns the IME options set on the query text field.
    455      * @return the ime options
    456      * @see TextView#setImeOptions(int)
    457      *
    458      * @attr ref android.R.styleable#SearchView_imeOptions
    459      */
    460     public int getImeOptions() {
    461         return mSearchSrcTextView.getImeOptions();
    462     }
    463 
    464     /**
    465      * Sets the input type on the query text field.
    466      *
    467      * @see TextView#setInputType(int)
    468      * @param inputType the input type to set on the query text field
    469      *
    470      * @attr ref android.R.styleable#SearchView_inputType
    471      */
    472     public void setInputType(int inputType) {
    473         mSearchSrcTextView.setInputType(inputType);
    474     }
    475 
    476     /**
    477      * Returns the input type set on the query text field.
    478      * @return the input type
    479      *
    480      * @attr ref android.R.styleable#SearchView_inputType
    481      */
    482     public int getInputType() {
    483         return mSearchSrcTextView.getInputType();
    484     }
    485 
    486     /** @hide */
    487     @Override
    488     public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    489         // Don't accept focus if in the middle of clearing focus
    490         if (mClearingFocus) return false;
    491         // Check if SearchView is focusable.
    492         if (!isFocusable()) return false;
    493         // If it is not iconified, then give the focus to the text field
    494         if (!isIconified()) {
    495             boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect);
    496             if (result) {
    497                 updateViewsVisibility(false);
    498             }
    499             return result;
    500         } else {
    501             return super.requestFocus(direction, previouslyFocusedRect);
    502         }
    503     }
    504 
    505     /** @hide */
    506     @Override
    507     public void clearFocus() {
    508         mClearingFocus = true;
    509         super.clearFocus();
    510         mSearchSrcTextView.clearFocus();
    511         mSearchSrcTextView.setImeVisibility(false);
    512         mClearingFocus = false;
    513     }
    514 
    515     /**
    516      * Sets a listener for user actions within the SearchView.
    517      *
    518      * @param listener the listener object that receives callbacks when the user performs
    519      * actions in the SearchView such as clicking on buttons or typing a query.
    520      */
    521     public void setOnQueryTextListener(OnQueryTextListener listener) {
    522         mOnQueryChangeListener = listener;
    523     }
    524 
    525     /**
    526      * Sets a listener to inform when the user closes the SearchView.
    527      *
    528      * @param listener the listener to call when the user closes the SearchView.
    529      */
    530     public void setOnCloseListener(OnCloseListener listener) {
    531         mOnCloseListener = listener;
    532     }
    533 
    534     /**
    535      * Sets a listener to inform when the focus of the query text field changes.
    536      *
    537      * @param listener the listener to inform of focus changes.
    538      */
    539     public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
    540         mOnQueryTextFocusChangeListener = listener;
    541     }
    542 
    543     /**
    544      * Sets a listener to inform when a suggestion is focused or clicked.
    545      *
    546      * @param listener the listener to inform of suggestion selection events.
    547      */
    548     public void setOnSuggestionListener(OnSuggestionListener listener) {
    549         mOnSuggestionListener = listener;
    550     }
    551 
    552     /**
    553      * Sets a listener to inform when the search button is pressed. This is only
    554      * relevant when the text field is not visible by default. Calling {@link #setIconified
    555      * setIconified(false)} can also cause this listener to be informed.
    556      *
    557      * @param listener the listener to inform when the search button is clicked or
    558      * the text field is programmatically de-iconified.
    559      */
    560     public void setOnSearchClickListener(OnClickListener listener) {
    561         mOnSearchClickListener = listener;
    562     }
    563 
    564     /**
    565      * Returns the query string currently in the text field.
    566      *
    567      * @return the query string
    568      */
    569     @InspectableProperty(hasAttributeId = false)
    570     public CharSequence getQuery() {
    571         return mSearchSrcTextView.getText();
    572     }
    573 
    574     /**
    575      * Sets a query string in the text field and optionally submits the query as well.
    576      *
    577      * @param query the query string. This replaces any query text already present in the
    578      * text field.
    579      * @param submit whether to submit the query right now or only update the contents of
    580      * text field.
    581      */
    582     public void setQuery(CharSequence query, boolean submit) {
    583         mSearchSrcTextView.setText(query);
    584         if (query != null) {
    585             mSearchSrcTextView.setSelection(mSearchSrcTextView.length());
    586             mUserQuery = query;
    587         }
    588 
    589         // If the query is not empty and submit is requested, submit the query
    590         if (submit && !TextUtils.isEmpty(query)) {
    591             onSubmitQuery();
    592         }
    593     }
    594 
    595     /**
    596      * Sets the hint text to display in the query text field. This overrides
    597      * any hint specified in the {@link SearchableInfo}.
    598      * <p>
    599      * This value may be specified as an empty string to prevent any query hint
    600      * from being displayed.
    601      *
    602      * @param hint the hint text to display or {@code null} to clear
    603      * @attr ref android.R.styleable#SearchView_queryHint
    604      */
    605     public void setQueryHint(@Nullable CharSequence hint) {
    606         mQueryHint = hint;
    607         updateQueryHint();
    608     }
    609 
    610     /**
    611      * Returns the hint text that will be displayed in the query text field.
    612      * <p>
    613      * The displayed query hint is chosen in the following order:
    614      * <ol>
    615      * <li>Non-null value set with {@link #setQueryHint(CharSequence)}
    616      * <li>Value specified in XML using
    617      *     {@link android.R.styleable#SearchView_queryHint android:queryHint}
    618      * <li>Valid string resource ID exposed by the {@link SearchableInfo} via
    619      *     {@link SearchableInfo#getHintId()}
    620      * <li>Default hint provided by the theme against which the view was
    621      *     inflated
    622      * </ol>
    623      *
    624      * @return the displayed query hint text, or {@code null} if none set
    625      * @attr ref android.R.styleable#SearchView_queryHint
    626      */
    627     @InspectableProperty
    628     @Nullable
    629     public CharSequence getQueryHint() {
    630         final CharSequence hint;
    631         if (mQueryHint != null) {
    632             hint = mQueryHint;
    633         } else if (mSearchable != null && mSearchable.getHintId() != 0) {
    634             hint = getContext().getText(mSearchable.getHintId());
    635         } else {
    636             hint = mDefaultQueryHint;
    637         }
    638         return hint;
    639     }
    640 
    641     /**
    642      * Sets the default or resting state of the search field. If true, a single search icon is
    643      * shown by default and expands to show the text field and other buttons when pressed. Also,
    644      * if the default state is iconified, then it collapses to that state when the close button
    645      * is pressed. Changes to this property will take effect immediately.
    646      *
    647      * <p>The default value is true.</p>
    648      *
    649      * @param iconified whether the search field should be iconified by default
    650      *
    651      * @attr ref android.R.styleable#SearchView_iconifiedByDefault
    652      */
    653     public void setIconifiedByDefault(boolean iconified) {
    654         if (mIconifiedByDefault == iconified) return;
    655         mIconifiedByDefault = iconified;
    656         updateViewsVisibility(iconified);
    657         updateQueryHint();
    658     }
    659 
    660     /**
    661      * Returns the default iconified state of the search field.
    662      * @return
    663      *
    664      * @deprecated use {@link #isIconifiedByDefault()}
    665      * @attr ref android.R.styleable#SearchView_iconifiedByDefault
    666      */
    667     @Deprecated
    668     public boolean isIconfiedByDefault() {
    669         return mIconifiedByDefault;
    670     }
    671 
    672     /**
    673      * Returns the default iconified state of the search field.
    674      *
    675      * @attr ref android.R.styleable#SearchView_iconifiedByDefault
    676      */
    677     @InspectableProperty
    678     public boolean isIconifiedByDefault() {
    679         return mIconifiedByDefault;
    680     }
    681 
    682     /**
    683      * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
    684      * a temporary state and does not override the default iconified state set by
    685      * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
    686      * a false here will only be valid until the user closes the field. And if the default
    687      * state is expanded, then a true here will only clear the text field and not close it.
    688      *
    689      * @param iconify a true value will collapse the SearchView to an icon, while a false will
    690      * expand it.
    691      */
    692     public void setIconified(boolean iconify) {
    693         if (iconify) {
    694             onCloseClicked();
    695         } else {
    696             onSearchClicked();
    697         }
    698     }
    699 
    700     /**
    701      * Returns the current iconified state of the SearchView.
    702      *
    703      * @return true if the SearchView is currently iconified, false if the search field is
    704      * fully visible.
    705      */
    706     @InspectableProperty(hasAttributeId = false)
    707     public boolean isIconified() {
    708         return mIconified;
    709     }
    710 
    711     /**
    712      * Enables showing a submit button when the query is non-empty. In cases where the SearchView
    713      * is being used to filter the contents of the current activity and doesn't launch a separate
    714      * results activity, then the submit button should be disabled.
    715      *
    716      * @param enabled true to show a submit button for submitting queries, false if a submit
    717      * button is not required.
    718      */
    719     public void setSubmitButtonEnabled(boolean enabled) {
    720         mSubmitButtonEnabled = enabled;
    721         updateViewsVisibility(isIconified());
    722     }
    723 
    724     /**
    725      * Returns whether the submit button is enabled when necessary or never displayed.
    726      *
    727      * @return whether the submit button is enabled automatically when necessary
    728      */
    729     public boolean isSubmitButtonEnabled() {
    730         return mSubmitButtonEnabled;
    731     }
    732 
    733     /**
    734      * Specifies if a query refinement button should be displayed alongside each suggestion
    735      * or if it should depend on the flags set in the individual items retrieved from the
    736      * suggestions provider. Clicking on the query refinement button will replace the text
    737      * in the query text field with the text from the suggestion. This flag only takes effect
    738      * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
    739      * and not when using a custom adapter.
    740      *
    741      * @param enable true if all items should have a query refinement button, false if only
    742      * those items that have a query refinement flag set should have the button.
    743      *
    744      * @see SearchManager#SUGGEST_COLUMN_FLAGS
    745      * @see SearchManager#FLAG_QUERY_REFINEMENT
    746      */
    747     public void setQueryRefinementEnabled(boolean enable) {
    748         mQueryRefinement = enable;
    749         if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
    750             ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
    751                     enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
    752         }
    753     }
    754 
    755     /**
    756      * Returns whether query refinement is enabled for all items or only specific ones.
    757      * @return true if enabled for all items, false otherwise.
    758      */
    759     public boolean isQueryRefinementEnabled() {
    760         return mQueryRefinement;
    761     }
    762 
    763     /**
    764      * You can set a custom adapter if you wish. Otherwise the default adapter is used to
    765      * display the suggestions from the suggestions provider associated with the SearchableInfo.
    766      *
    767      * @see #setSearchableInfo(SearchableInfo)
    768      */
    769     public void setSuggestionsAdapter(CursorAdapter adapter) {
    770         mSuggestionsAdapter = adapter;
    771 
    772         mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
    773     }
    774 
    775     /**
    776      * Returns the adapter used for suggestions, if any.
    777      * @return the suggestions adapter
    778      */
    779     public CursorAdapter getSuggestionsAdapter() {
    780         return mSuggestionsAdapter;
    781     }
    782 
    783     /**
    784      * Makes the view at most this many pixels wide
    785      *
    786      * @attr ref android.R.styleable#SearchView_maxWidth
    787      */
    788     public void setMaxWidth(int maxpixels) {
    789         mMaxWidth = maxpixels;
    790 
    791         requestLayout();
    792     }
    793 
    794     /**
    795      * Gets the specified maximum width in pixels, if set. Returns zero if
    796      * no maximum width was specified.
    797      * @return the maximum width of the view
    798      *
    799      * @attr ref android.R.styleable#SearchView_maxWidth
    800      */
    801     @InspectableProperty
    802     public int getMaxWidth() {
    803         return mMaxWidth;
    804     }
    805 
    806     @Override
    807     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    808         // Let the standard measurements take effect in iconified state.
    809         if (isIconified()) {
    810             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    811             return;
    812         }
    813 
    814         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    815         int width = MeasureSpec.getSize(widthMeasureSpec);
    816 
    817         switch (widthMode) {
    818         case MeasureSpec.AT_MOST:
    819             // If there is an upper limit, don't exceed maximum width (explicit or implicit)
    820             if (mMaxWidth > 0) {
    821                 width = Math.min(mMaxWidth, width);
    822             } else {
    823                 width = Math.min(getPreferredWidth(), width);
    824             }
    825             break;
    826         case MeasureSpec.EXACTLY:
    827             // If an exact width is specified, still don't exceed any specified maximum width
    828             if (mMaxWidth > 0) {
    829                 width = Math.min(mMaxWidth, width);
    830             }
    831             break;
    832         case MeasureSpec.UNSPECIFIED:
    833             // Use maximum width, if specified, else preferred width
    834             width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
    835             break;
    836         }
    837         widthMode = MeasureSpec.EXACTLY;
    838 
    839         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    840         int height = MeasureSpec.getSize(heightMeasureSpec);
    841 
    842         switch (heightMode) {
    843             case MeasureSpec.AT_MOST:
    844                 height = Math.min(getPreferredHeight(), height);
    845                 break;
    846             case MeasureSpec.UNSPECIFIED:
    847                 height = getPreferredHeight();
    848                 break;
    849         }
    850         heightMode = MeasureSpec.EXACTLY;
    851 
    852         super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode),
    853                 MeasureSpec.makeMeasureSpec(height, heightMode));
    854     }
    855 
    856     @Override
    857     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    858         super.onLayout(changed, left, top, right, bottom);
    859 
    860         if (changed) {
    861             // Expand mSearchSrcTextView touch target to be the height of the parent in order to
    862             // allow it to be up to 48dp.
    863             getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds);
    864             mSearchSrtTextViewBoundsExpanded.set(
    865                     mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top);
    866             if (mTouchDelegate == null) {
    867                 mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded,
    868                         mSearchSrcTextViewBounds, mSearchSrcTextView);
    869                 setTouchDelegate(mTouchDelegate);
    870             } else {
    871                 mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds);
    872             }
    873         }
    874     }
    875 
    876     private void getChildBoundsWithinSearchView(View view, Rect rect) {
    877         view.getLocationInWindow(mTemp);
    878         getLocationInWindow(mTemp2);
    879         final int top = mTemp[1] - mTemp2[1];
    880         final int left = mTemp[0] - mTemp2[0];
    881         rect.set(left , top, left + view.getWidth(), top + view.getHeight());
    882     }
    883 
    884     private int getPreferredWidth() {
    885         return getContext().getResources()
    886                 .getDimensionPixelSize(R.dimen.search_view_preferred_width);
    887     }
    888 
    889     private int getPreferredHeight() {
    890         return getContext().getResources()
    891                 .getDimensionPixelSize(R.dimen.search_view_preferred_height);
    892     }
    893 
    894     @UnsupportedAppUsage
    895     private void updateViewsVisibility(final boolean collapsed) {
    896         mIconified = collapsed;
    897         // Visibility of views that are visible when collapsed
    898         final int visCollapsed = collapsed ? VISIBLE : GONE;
    899         // Is there text in the query
    900         final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
    901 
    902         mSearchButton.setVisibility(visCollapsed);
    903         updateSubmitButton(hasText);
    904         mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
    905 
    906         final int iconVisibility;
    907         if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) {
    908             iconVisibility = GONE;
    909         } else {
    910             iconVisibility = VISIBLE;
    911         }
    912         mCollapsedIcon.setVisibility(iconVisibility);
    913 
    914         updateCloseButton();
    915         updateVoiceButton(!hasText);
    916         updateSubmitArea();
    917     }
    918 
    919     private boolean hasVoiceSearch() {
    920         if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
    921             Intent testIntent = null;
    922             if (mSearchable.getVoiceSearchLaunchWebSearch()) {
    923                 testIntent = mVoiceWebSearchIntent;
    924             } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
    925                 testIntent = mVoiceAppSearchIntent;
    926             }
    927             if (testIntent != null) {
    928                 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
    929                         PackageManager.MATCH_DEFAULT_ONLY);
    930                 return ri != null;
    931             }
    932         }
    933         return false;
    934     }
    935 
    936     private boolean isSubmitAreaEnabled() {
    937         return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
    938     }
    939 
    940     @UnsupportedAppUsage
    941     private void updateSubmitButton(boolean hasText) {
    942         int visibility = GONE;
    943         if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
    944                 && (hasText || !mVoiceButtonEnabled)) {
    945             visibility = VISIBLE;
    946         }
    947         mGoButton.setVisibility(visibility);
    948     }
    949 
    950     @UnsupportedAppUsage
    951     private void updateSubmitArea() {
    952         int visibility = GONE;
    953         if (isSubmitAreaEnabled()
    954                 && (mGoButton.getVisibility() == VISIBLE
    955                         || mVoiceButton.getVisibility() == VISIBLE)) {
    956             visibility = VISIBLE;
    957         }
    958         mSubmitArea.setVisibility(visibility);
    959     }
    960 
    961     private void updateCloseButton() {
    962         final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
    963         // Should we show the close button? It is not shown if there's no focus,
    964         // field is not iconified by default and there is no text in it.
    965         final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
    966         mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
    967         final Drawable closeButtonImg = mCloseButton.getDrawable();
    968         if (closeButtonImg != null){
    969             closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
    970         }
    971     }
    972 
    973     private void postUpdateFocusedState() {
    974         post(mUpdateDrawableStateRunnable);
    975     }
    976 
    977     private void updateFocusedState() {
    978         final boolean focused = mSearchSrcTextView.hasFocus();
    979         final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET;
    980         final Drawable searchPlateBg = mSearchPlate.getBackground();
    981         if (searchPlateBg != null) {
    982             searchPlateBg.setState(stateSet);
    983         }
    984         final Drawable submitAreaBg = mSubmitArea.getBackground();
    985         if (submitAreaBg != null) {
    986             submitAreaBg.setState(stateSet);
    987         }
    988         invalidate();
    989     }
    990 
    991     @Override
    992     protected void onDetachedFromWindow() {
    993         removeCallbacks(mUpdateDrawableStateRunnable);
    994         post(mReleaseCursorRunnable);
    995         super.onDetachedFromWindow();
    996     }
    997 
    998     /**
    999      * Called by the SuggestionsAdapter
   1000      * @hide
   1001      */
   1002     /* package */void onQueryRefine(CharSequence queryText) {
   1003         setQuery(queryText);
   1004     }
   1005 
   1006     @UnsupportedAppUsage
   1007     private final OnClickListener mOnClickListener = new OnClickListener() {
   1008 
   1009         public void onClick(View v) {
   1010             if (v == mSearchButton) {
   1011                 onSearchClicked();
   1012             } else if (v == mCloseButton) {
   1013                 onCloseClicked();
   1014             } else if (v == mGoButton) {
   1015                 onSubmitQuery();
   1016             } else if (v == mVoiceButton) {
   1017                 onVoiceClicked();
   1018             } else if (v == mSearchSrcTextView) {
   1019                 forceSuggestionQuery();
   1020             }
   1021         }
   1022     };
   1023 
   1024     /**
   1025      * Handles the key down event for dealing with action keys.
   1026      *
   1027      * @param keyCode This is the keycode of the typed key, and is the same value as
   1028      *        found in the KeyEvent parameter.
   1029      * @param event The complete event record for the typed key
   1030      *
   1031      * @return true if the event was handled here, or false if not.
   1032      */
   1033     @Override
   1034     public boolean onKeyDown(int keyCode, KeyEvent event) {
   1035         if (mSearchable == null) {
   1036             return false;
   1037         }
   1038 
   1039         // if it's an action specified by the searchable activity, launch the
   1040         // entered query with the action key
   1041         SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
   1042         if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
   1043             launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText()
   1044                     .toString());
   1045             return true;
   1046         }
   1047 
   1048         return super.onKeyDown(keyCode, event);
   1049     }
   1050 
   1051     /**
   1052      * React to the user typing "enter" or other hardwired keys while typing in
   1053      * the search box. This handles these special keys while the edit box has
   1054      * focus.
   1055      */
   1056     View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
   1057         public boolean onKey(View v, int keyCode, KeyEvent event) {
   1058             // guard against possible race conditions
   1059             if (mSearchable == null) {
   1060                 return false;
   1061             }
   1062 
   1063             if (DBG) {
   1064                 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
   1065                         + mSearchSrcTextView.getListSelection());
   1066             }
   1067 
   1068             // If a suggestion is selected, handle enter, search key, and action keys
   1069             // as presses on the selected suggestion
   1070             if (mSearchSrcTextView.isPopupShowing()
   1071                     && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) {
   1072                 return onSuggestionsKey(v, keyCode, event);
   1073             }
   1074 
   1075             // If there is text in the query box, handle enter, and action keys
   1076             // The search key is handled by the dialog's onKeyDown().
   1077             if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) {
   1078                 if (event.getAction() == KeyEvent.ACTION_UP) {
   1079                     if (keyCode == KeyEvent.KEYCODE_ENTER) {
   1080                         v.cancelLongPress();
   1081 
   1082                         // Launch as a regular search.
   1083                         launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText()
   1084                                 .toString());
   1085                         return true;
   1086                     }
   1087                 }
   1088                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
   1089                     SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
   1090                     if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
   1091                         launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView
   1092                                 .getText().toString());
   1093                         return true;
   1094                     }
   1095                 }
   1096             }
   1097             return false;
   1098         }
   1099     };
   1100 
   1101     /**
   1102      * React to the user typing while in the suggestions list. First, check for
   1103      * action keys. If not handled, try refocusing regular characters into the
   1104      * EditText.
   1105      */
   1106     private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
   1107         // guard against possible race conditions (late arrival after dismiss)
   1108         if (mSearchable == null) {
   1109             return false;
   1110         }
   1111         if (mSuggestionsAdapter == null) {
   1112             return false;
   1113         }
   1114         if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
   1115             // First, check for enter or search (both of which we'll treat as a
   1116             // "click")
   1117             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
   1118                     || keyCode == KeyEvent.KEYCODE_TAB) {
   1119                 int position = mSearchSrcTextView.getListSelection();
   1120                 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
   1121             }
   1122 
   1123             // Next, check for left/right moves, which we use to "return" the
   1124             // user to the edit view
   1125             if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
   1126                 // give "focus" to text editor, with cursor at the beginning if
   1127                 // left key, at end if right key
   1128                 // TODO: Reverse left/right for right-to-left languages, e.g.
   1129                 // Arabic
   1130                 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView
   1131                         .length();
   1132                 mSearchSrcTextView.setSelection(selPoint);
   1133                 mSearchSrcTextView.setListSelection(0);
   1134                 mSearchSrcTextView.clearListSelection();
   1135                 mSearchSrcTextView.ensureImeVisible(true);
   1136 
   1137                 return true;
   1138             }
   1139 
   1140             // Next, check for an "up and out" move
   1141             if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) {
   1142                 // TODO: restoreUserQuery();
   1143                 // let ACTV complete the move
   1144                 return false;
   1145             }
   1146 
   1147             // Next, check for an "action key"
   1148             SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
   1149             if ((actionKey != null)
   1150                     && ((actionKey.getSuggestActionMsg() != null) || (actionKey
   1151                             .getSuggestActionMsgColumn() != null))) {
   1152                 // launch suggestion using action key column
   1153                 int position = mSearchSrcTextView.getListSelection();
   1154                 if (position != ListView.INVALID_POSITION) {
   1155                     Cursor c = mSuggestionsAdapter.getCursor();
   1156                     if (c.moveToPosition(position)) {
   1157                         final String actionMsg = getActionKeyMessage(c, actionKey);
   1158                         if (actionMsg != null && (actionMsg.length() > 0)) {
   1159                             return onItemClicked(position, keyCode, actionMsg);
   1160                         }
   1161                     }
   1162                 }
   1163             }
   1164         }
   1165         return false;
   1166     }
   1167 
   1168     /**
   1169      * For a given suggestion and a given cursor row, get the action message. If
   1170      * not provided by the specific row/column, also check for a single
   1171      * definition (for the action key).
   1172      *
   1173      * @param c The cursor providing suggestions
   1174      * @param actionKey The actionkey record being examined
   1175      *
   1176      * @return Returns a string, or null if no action key message for this
   1177      *         suggestion
   1178      */
   1179     private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
   1180         String result = null;
   1181         // check first in the cursor data, for a suggestion-specific message
   1182         final String column = actionKey.getSuggestActionMsgColumn();
   1183         if (column != null) {
   1184             result = SuggestionsAdapter.getColumnString(c, column);
   1185         }
   1186         // If the cursor didn't give us a message, see if there's a single
   1187         // message defined
   1188         // for the actionkey (for all suggestions)
   1189         if (result == null) {
   1190             result = actionKey.getSuggestActionMsg();
   1191         }
   1192         return result;
   1193     }
   1194 
   1195     private CharSequence getDecoratedHint(CharSequence hintText) {
   1196         // If the field is always expanded or we don't have a search hint icon,
   1197         // then don't add the search icon to the hint.
   1198         if (!mIconifiedByDefault || mSearchHintIcon == null) {
   1199             return hintText;
   1200         }
   1201 
   1202         final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25);
   1203         mSearchHintIcon.setBounds(0, 0, textSize, textSize);
   1204 
   1205         final SpannableStringBuilder ssb = new SpannableStringBuilder("   ");
   1206         ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1207         ssb.append(hintText);
   1208         return ssb;
   1209     }
   1210 
   1211     private void updateQueryHint() {
   1212         final CharSequence hint = getQueryHint();
   1213         mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint));
   1214     }
   1215 
   1216     /**
   1217      * Updates the auto-complete text view.
   1218      */
   1219     private void updateSearchAutoComplete() {
   1220         mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation
   1221         mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold());
   1222         mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions());
   1223         int inputType = mSearchable.getInputType();
   1224         // We only touch this if the input type is set up for text (which it almost certainly
   1225         // should be, in the case of search!)
   1226         if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
   1227             // The existence of a suggestions authority is the proxy for "suggestions
   1228             // are available here"
   1229             inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
   1230             if (mSearchable.getSuggestAuthority() != null) {
   1231                 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
   1232                 // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
   1233                 // auto-completion based on its own semantics, which it will present to the user
   1234                 // as they type. This generally means that the input method should not show its
   1235                 // own candidates, and the spell checker should not be in action. The text editor
   1236                 // supplies its candidates by calling InputMethodManager.displayCompletions(),
   1237                 // which in turn will call InputMethodSession.displayCompletions().
   1238                 inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
   1239             }
   1240         }
   1241         mSearchSrcTextView.setInputType(inputType);
   1242         if (mSuggestionsAdapter != null) {
   1243             mSuggestionsAdapter.changeCursor(null);
   1244         }
   1245         // attach the suggestions adapter, if suggestions are available
   1246         // The existence of a suggestions authority is the proxy for "suggestions available here"
   1247         if (mSearchable.getSuggestAuthority() != null) {
   1248             mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
   1249                     this, mSearchable, mOutsideDrawablesCache);
   1250             mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
   1251             ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
   1252                     mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
   1253                     : SuggestionsAdapter.REFINE_BY_ENTRY);
   1254         }
   1255     }
   1256 
   1257     /**
   1258      * Update the visibility of the voice button.  There are actually two voice search modes,
   1259      * either of which will activate the button.
   1260      * @param empty whether the search query text field is empty. If it is, then the other
   1261      * criteria apply to make the voice button visible.
   1262      */
   1263     private void updateVoiceButton(boolean empty) {
   1264         int visibility = GONE;
   1265         if (mVoiceButtonEnabled && !isIconified() && empty) {
   1266             visibility = VISIBLE;
   1267             mGoButton.setVisibility(GONE);
   1268         }
   1269         mVoiceButton.setVisibility(visibility);
   1270     }
   1271 
   1272     private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
   1273 
   1274         /**
   1275          * Called when the input method default action key is pressed.
   1276          */
   1277         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
   1278             onSubmitQuery();
   1279             return true;
   1280         }
   1281     };
   1282 
   1283     private void onTextChanged(CharSequence newText) {
   1284         CharSequence text = mSearchSrcTextView.getText();
   1285         mUserQuery = text;
   1286         boolean hasText = !TextUtils.isEmpty(text);
   1287         updateSubmitButton(hasText);
   1288         updateVoiceButton(!hasText);
   1289         updateCloseButton();
   1290         updateSubmitArea();
   1291         if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
   1292             mOnQueryChangeListener.onQueryTextChange(newText.toString());
   1293         }
   1294         mOldQueryText = newText.toString();
   1295     }
   1296 
   1297     private void onSubmitQuery() {
   1298         CharSequence query = mSearchSrcTextView.getText();
   1299         if (query != null && TextUtils.getTrimmedLength(query) > 0) {
   1300             if (mOnQueryChangeListener == null
   1301                     || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
   1302                 if (mSearchable != null) {
   1303                     launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
   1304                 }
   1305                 mSearchSrcTextView.setImeVisibility(false);
   1306                 dismissSuggestions();
   1307             }
   1308         }
   1309     }
   1310 
   1311     private void dismissSuggestions() {
   1312         mSearchSrcTextView.dismissDropDown();
   1313     }
   1314 
   1315     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
   1316     private void onCloseClicked() {
   1317         CharSequence text = mSearchSrcTextView.getText();
   1318         if (TextUtils.isEmpty(text)) {
   1319             if (mIconifiedByDefault) {
   1320                 // If the app doesn't override the close behavior
   1321                 if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
   1322                     // hide the keyboard and remove focus
   1323                     clearFocus();
   1324                     // collapse the search field
   1325                     updateViewsVisibility(true);
   1326                 }
   1327             }
   1328         } else {
   1329             mSearchSrcTextView.setText("");
   1330             mSearchSrcTextView.requestFocus();
   1331             mSearchSrcTextView.setImeVisibility(true);
   1332         }
   1333 
   1334     }
   1335 
   1336     private void onSearchClicked() {
   1337         updateViewsVisibility(false);
   1338         mSearchSrcTextView.requestFocus();
   1339         mSearchSrcTextView.setImeVisibility(true);
   1340         if (mOnSearchClickListener != null) {
   1341             mOnSearchClickListener.onClick(this);
   1342         }
   1343     }
   1344 
   1345     private void onVoiceClicked() {
   1346         // guard against possible race conditions
   1347         if (mSearchable == null) {
   1348             return;
   1349         }
   1350         SearchableInfo searchable = mSearchable;
   1351         try {
   1352             if (searchable.getVoiceSearchLaunchWebSearch()) {
   1353                 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
   1354                         searchable);
   1355                 getContext().startActivity(webSearchIntent);
   1356             } else if (searchable.getVoiceSearchLaunchRecognizer()) {
   1357                 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
   1358                         searchable);
   1359                 getContext().startActivity(appSearchIntent);
   1360             }
   1361         } catch (ActivityNotFoundException e) {
   1362             // Should not happen, since we check the availability of
   1363             // voice search before showing the button. But just in case...
   1364             Log.w(LOG_TAG, "Could not find voice search activity");
   1365         }
   1366     }
   1367 
   1368     void onTextFocusChanged() {
   1369         updateViewsVisibility(isIconified());
   1370         // Delayed update to make sure that the focus has settled down and window focus changes
   1371         // don't affect it. A synchronous update was not working.
   1372         postUpdateFocusedState();
   1373         if (mSearchSrcTextView.hasFocus()) {
   1374             forceSuggestionQuery();
   1375         }
   1376     }
   1377 
   1378     @Override
   1379     public void onWindowFocusChanged(boolean hasWindowFocus) {
   1380         super.onWindowFocusChanged(hasWindowFocus);
   1381 
   1382         postUpdateFocusedState();
   1383     }
   1384 
   1385     /**
   1386      * {@inheritDoc}
   1387      */
   1388     @Override
   1389     public void onActionViewCollapsed() {
   1390         setQuery("", false);
   1391         clearFocus();
   1392         updateViewsVisibility(true);
   1393         mSearchSrcTextView.setImeOptions(mCollapsedImeOptions);
   1394         mExpandedInActionView = false;
   1395     }
   1396 
   1397     /**
   1398      * {@inheritDoc}
   1399      */
   1400     @Override
   1401     public void onActionViewExpanded() {
   1402         if (mExpandedInActionView) return;
   1403 
   1404         mExpandedInActionView = true;
   1405         mCollapsedImeOptions = mSearchSrcTextView.getImeOptions();
   1406         mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
   1407         mSearchSrcTextView.setText("");
   1408         setIconified(false);
   1409     }
   1410 
   1411     static class SavedState extends BaseSavedState {
   1412         boolean isIconified;
   1413 
   1414         SavedState(Parcelable superState) {
   1415             super(superState);
   1416         }
   1417 
   1418         public SavedState(Parcel source) {
   1419             super(source);
   1420             isIconified = (Boolean) source.readValue(null);
   1421         }
   1422 
   1423         @Override
   1424         public void writeToParcel(Parcel dest, int flags) {
   1425             super.writeToParcel(dest, flags);
   1426             dest.writeValue(isIconified);
   1427         }
   1428 
   1429         @Override
   1430         public String toString() {
   1431             return "SearchView.SavedState{"
   1432                     + Integer.toHexString(System.identityHashCode(this))
   1433                     + " isIconified=" + isIconified + "}";
   1434         }
   1435 
   1436         public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR =
   1437                 new Parcelable.Creator<SavedState>() {
   1438                     public SavedState createFromParcel(Parcel in) {
   1439                         return new SavedState(in);
   1440                     }
   1441 
   1442                     public SavedState[] newArray(int size) {
   1443                         return new SavedState[size];
   1444                     }
   1445                 };
   1446     }
   1447 
   1448     @Override
   1449     protected Parcelable onSaveInstanceState() {
   1450         Parcelable superState = super.onSaveInstanceState();
   1451         SavedState ss = new SavedState(superState);
   1452         ss.isIconified = isIconified();
   1453         return ss;
   1454     }
   1455 
   1456     @Override
   1457     protected void onRestoreInstanceState(Parcelable state) {
   1458         SavedState ss = (SavedState) state;
   1459         super.onRestoreInstanceState(ss.getSuperState());
   1460         updateViewsVisibility(ss.isIconified);
   1461         requestLayout();
   1462     }
   1463 
   1464     @Override
   1465     public CharSequence getAccessibilityClassName() {
   1466         return SearchView.class.getName();
   1467     }
   1468 
   1469     private void adjustDropDownSizeAndPosition() {
   1470         if (mDropDownAnchor.getWidth() > 1) {
   1471             Resources res = getContext().getResources();
   1472             int anchorPadding = mSearchPlate.getPaddingLeft();
   1473             Rect dropDownPadding = new Rect();
   1474             final boolean isLayoutRtl = isLayoutRtl();
   1475             int iconOffset = mIconifiedByDefault
   1476                     ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
   1477                     + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
   1478                     : 0;
   1479             mSearchSrcTextView.getDropDownBackground().getPadding(dropDownPadding);
   1480             int offset;
   1481             if (isLayoutRtl) {
   1482                 offset = - dropDownPadding.left;
   1483             } else {
   1484                 offset = anchorPadding - (dropDownPadding.left + iconOffset);
   1485             }
   1486             mSearchSrcTextView.setDropDownHorizontalOffset(offset);
   1487             final int width = mDropDownAnchor.getWidth() + dropDownPadding.left
   1488                     + dropDownPadding.right + iconOffset - anchorPadding;
   1489             mSearchSrcTextView.setDropDownWidth(width);
   1490         }
   1491     }
   1492 
   1493     private boolean onItemClicked(int position, int actionKey, String actionMsg) {
   1494         if (mOnSuggestionListener == null
   1495                 || !mOnSuggestionListener.onSuggestionClick(position)) {
   1496             launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
   1497             mSearchSrcTextView.setImeVisibility(false);
   1498             dismissSuggestions();
   1499             return true;
   1500         }
   1501         return false;
   1502     }
   1503 
   1504     private boolean onItemSelected(int position) {
   1505         if (mOnSuggestionListener == null
   1506                 || !mOnSuggestionListener.onSuggestionSelect(position)) {
   1507             rewriteQueryFromSuggestion(position);
   1508             return true;
   1509         }
   1510         return false;
   1511     }
   1512 
   1513     @UnsupportedAppUsage
   1514     private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
   1515 
   1516         /**
   1517          * Implements OnItemClickListener
   1518          */
   1519         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   1520             if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
   1521             onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
   1522         }
   1523     };
   1524 
   1525     private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
   1526 
   1527         /**
   1528          * Implements OnItemSelectedListener
   1529          */
   1530         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
   1531             if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
   1532             SearchView.this.onItemSelected(position);
   1533         }
   1534 
   1535         /**
   1536          * Implements OnItemSelectedListener
   1537          */
   1538         public void onNothingSelected(AdapterView<?> parent) {
   1539             if (DBG)
   1540                 Log.d(LOG_TAG, "onNothingSelected()");
   1541         }
   1542     };
   1543 
   1544     /**
   1545      * Query rewriting.
   1546      */
   1547     private void rewriteQueryFromSuggestion(int position) {
   1548         CharSequence oldQuery = mSearchSrcTextView.getText();
   1549         Cursor c = mSuggestionsAdapter.getCursor();
   1550         if (c == null) {
   1551             return;
   1552         }
   1553         if (c.moveToPosition(position)) {
   1554             // Get the new query from the suggestion.
   1555             CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
   1556             if (newQuery != null) {
   1557                 // The suggestion rewrites the query.
   1558                 // Update the text field, without getting new suggestions.
   1559                 setQuery(newQuery);
   1560             } else {
   1561                 // The suggestion does not rewrite the query, restore the user's query.
   1562                 setQuery(oldQuery);
   1563             }
   1564         } else {
   1565             // We got a bad position, restore the user's query.
   1566             setQuery(oldQuery);
   1567         }
   1568     }
   1569 
   1570     /**
   1571      * Launches an intent based on a suggestion.
   1572      *
   1573      * @param position The index of the suggestion to create the intent from.
   1574      * @param actionKey The key code of the action key that was pressed,
   1575      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1576      * @param actionMsg The message for the action key that was pressed,
   1577      *        or <code>null</code> if none.
   1578      * @return true if a successful launch, false if could not (e.g. bad position).
   1579      */
   1580     private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
   1581         Cursor c = mSuggestionsAdapter.getCursor();
   1582         if ((c != null) && c.moveToPosition(position)) {
   1583 
   1584             Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
   1585 
   1586             // launch the intent
   1587             launchIntent(intent);
   1588 
   1589             return true;
   1590         }
   1591         return false;
   1592     }
   1593 
   1594     /**
   1595      * Launches an intent, including any special intent handling.
   1596      */
   1597     private void launchIntent(Intent intent) {
   1598         if (intent == null) {
   1599             return;
   1600         }
   1601         try {
   1602             // If the intent was created from a suggestion, it will always have an explicit
   1603             // component here.
   1604             getContext().startActivity(intent);
   1605         } catch (RuntimeException ex) {
   1606             Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
   1607         }
   1608     }
   1609 
   1610     /**
   1611      * Sets the text in the query box, without updating the suggestions.
   1612      */
   1613     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
   1614     private void setQuery(CharSequence query) {
   1615         mSearchSrcTextView.setText(query, true);
   1616         // Move the cursor to the end
   1617         mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
   1618     }
   1619 
   1620     private void launchQuerySearch(int actionKey, String actionMsg, String query) {
   1621         String action = Intent.ACTION_SEARCH;
   1622         Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
   1623         getContext().startActivity(intent);
   1624     }
   1625 
   1626     /**
   1627      * Constructs an intent from the given information and the search dialog state.
   1628      *
   1629      * @param action Intent action.
   1630      * @param data Intent data, or <code>null</code>.
   1631      * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
   1632      * @param query Intent query, or <code>null</code>.
   1633      * @param actionKey The key code of the action key that was pressed,
   1634      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1635      * @param actionMsg The message for the action key that was pressed,
   1636      *        or <code>null</code> if none.
   1637      * @param mode The search mode, one of the acceptable values for
   1638      *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
   1639      * @return The intent.
   1640      */
   1641     private Intent createIntent(String action, Uri data, String extraData, String query,
   1642             int actionKey, String actionMsg) {
   1643         // Now build the Intent
   1644         Intent intent = new Intent(action);
   1645         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   1646         // We need CLEAR_TOP to avoid reusing an old task that has other activities
   1647         // on top of the one we want. We don't want to do this in in-app search though,
   1648         // as it can be destructive to the activity stack.
   1649         if (data != null) {
   1650             intent.setData(data);
   1651         }
   1652         intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
   1653         if (query != null) {
   1654             intent.putExtra(SearchManager.QUERY, query);
   1655         }
   1656         if (extraData != null) {
   1657             intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
   1658         }
   1659         if (mAppSearchData != null) {
   1660             intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
   1661         }
   1662         if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
   1663             intent.putExtra(SearchManager.ACTION_KEY, actionKey);
   1664             intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
   1665         }
   1666         intent.setComponent(mSearchable.getSearchActivity());
   1667         return intent;
   1668     }
   1669 
   1670     /**
   1671      * Create and return an Intent that can launch the voice search activity for web search.
   1672      */
   1673     private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
   1674         Intent voiceIntent = new Intent(baseIntent);
   1675         ComponentName searchActivity = searchable.getSearchActivity();
   1676         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
   1677                 : searchActivity.flattenToShortString());
   1678         return voiceIntent;
   1679     }
   1680 
   1681     /**
   1682      * Create and return an Intent that can launch the voice search activity, perform a specific
   1683      * voice transcription, and forward the results to the searchable activity.
   1684      *
   1685      * @param baseIntent The voice app search intent to start from
   1686      * @return A completely-configured intent ready to send to the voice search activity
   1687      */
   1688     private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
   1689         ComponentName searchActivity = searchable.getSearchActivity();
   1690 
   1691         // create the necessary intent to set up a search-and-forward operation
   1692         // in the voice search system.   We have to keep the bundle separate,
   1693         // because it becomes immutable once it enters the PendingIntent
   1694         Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
   1695         queryIntent.setComponent(searchActivity);
   1696         PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
   1697                 PendingIntent.FLAG_ONE_SHOT);
   1698 
   1699         // Now set up the bundle that will be inserted into the pending intent
   1700         // when it's time to do the search.  We always build it here (even if empty)
   1701         // because the voice search activity will always need to insert "QUERY" into
   1702         // it anyway.
   1703         Bundle queryExtras = new Bundle();
   1704         if (mAppSearchData != null) {
   1705             queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
   1706         }
   1707 
   1708         // Now build the intent to launch the voice search.  Add all necessary
   1709         // extras to launch the voice recognizer, and then all the necessary extras
   1710         // to forward the results to the searchable activity
   1711         Intent voiceIntent = new Intent(baseIntent);
   1712 
   1713         // Add all of the configuration options supplied by the searchable's metadata
   1714         String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
   1715         String prompt = null;
   1716         String language = null;
   1717         int maxResults = 1;
   1718 
   1719         Resources resources = getResources();
   1720         if (searchable.getVoiceLanguageModeId() != 0) {
   1721             languageModel = resources.getString(searchable.getVoiceLanguageModeId());
   1722         }
   1723         if (searchable.getVoicePromptTextId() != 0) {
   1724             prompt = resources.getString(searchable.getVoicePromptTextId());
   1725         }
   1726         if (searchable.getVoiceLanguageId() != 0) {
   1727             language = resources.getString(searchable.getVoiceLanguageId());
   1728         }
   1729         if (searchable.getVoiceMaxResults() != 0) {
   1730             maxResults = searchable.getVoiceMaxResults();
   1731         }
   1732         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
   1733         voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
   1734         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
   1735         voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
   1736         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
   1737                 : searchActivity.flattenToShortString());
   1738 
   1739         // Add the values that configure forwarding the results
   1740         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
   1741         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
   1742 
   1743         return voiceIntent;
   1744     }
   1745 
   1746     /**
   1747      * When a particular suggestion has been selected, perform the various lookups required
   1748      * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
   1749      * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
   1750      * the suggestion includes a data id.
   1751      *
   1752      * @param c The suggestions cursor, moved to the row of the user's selection
   1753      * @param actionKey The key code of the action key that was pressed,
   1754      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1755      * @param actionMsg The message for the action key that was pressed,
   1756      *        or <code>null</code> if none.
   1757      * @return An intent for the suggestion at the cursor's position.
   1758      */
   1759     private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
   1760         try {
   1761             // use specific action if supplied, or default action if supplied, or fixed default
   1762             String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
   1763 
   1764             if (action == null) {
   1765                 action = mSearchable.getSuggestIntentAction();
   1766             }
   1767             if (action == null) {
   1768                 action = Intent.ACTION_SEARCH;
   1769             }
   1770 
   1771             // use specific data if supplied, or default data if supplied
   1772             String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
   1773             if (data == null) {
   1774                 data = mSearchable.getSuggestIntentData();
   1775             }
   1776             // then, if an ID was provided, append it.
   1777             if (data != null) {
   1778                 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
   1779                 if (id != null) {
   1780                     data = data + "/" + Uri.encode(id);
   1781                 }
   1782             }
   1783             Uri dataUri = (data == null) ? null : Uri.parse(data);
   1784 
   1785             String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
   1786             String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
   1787 
   1788             return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
   1789         } catch (RuntimeException e ) {
   1790             int rowNum;
   1791             try {                       // be really paranoid now
   1792                 rowNum = c.getPosition();
   1793             } catch (RuntimeException e2 ) {
   1794                 rowNum = -1;
   1795             }
   1796             Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
   1797                             " returned exception.", e);
   1798             return null;
   1799         }
   1800     }
   1801 
   1802     private void forceSuggestionQuery() {
   1803         mSearchSrcTextView.doBeforeTextChanged();
   1804         mSearchSrcTextView.doAfterTextChanged();
   1805     }
   1806 
   1807     static boolean isLandscapeMode(Context context) {
   1808         return context.getResources().getConfiguration().orientation
   1809                 == Configuration.ORIENTATION_LANDSCAPE;
   1810     }
   1811 
   1812     /**
   1813      * Callback to watch the text field for empty/non-empty
   1814      */
   1815     private TextWatcher mTextWatcher = new TextWatcher() {
   1816 
   1817         public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
   1818 
   1819         public void onTextChanged(CharSequence s, int start,
   1820                 int before, int after) {
   1821             SearchView.this.onTextChanged(s);
   1822         }
   1823 
   1824         public void afterTextChanged(Editable s) {
   1825         }
   1826     };
   1827 
   1828     private static class UpdatableTouchDelegate extends TouchDelegate {
   1829         /**
   1830          * View that should receive forwarded touch events
   1831          */
   1832         private final View mDelegateView;
   1833 
   1834         /**
   1835          * Bounds in local coordinates of the containing view that should be mapped to the delegate
   1836          * view. This rect is used for initial hit testing.
   1837          */
   1838         private final Rect mTargetBounds;
   1839 
   1840         /**
   1841          * Bounds in local coordinates of the containing view that are actual bounds of the delegate
   1842          * view. This rect is used for event coordinate mapping.
   1843          */
   1844         private final Rect mActualBounds;
   1845 
   1846         /**
   1847          * mTargetBounds inflated to include some slop. This rect is to track whether the motion events
   1848          * should be considered to be be within the delegate view.
   1849          */
   1850         private final Rect mSlopBounds;
   1851 
   1852         private final int mSlop;
   1853 
   1854         /**
   1855          * True if the delegate had been targeted on a down event (intersected mTargetBounds).
   1856          */
   1857         private boolean mDelegateTargeted;
   1858 
   1859         public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) {
   1860             super(targetBounds, delegateView);
   1861             mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
   1862             mTargetBounds = new Rect();
   1863             mSlopBounds = new Rect();
   1864             mActualBounds = new Rect();
   1865             setBounds(targetBounds, actualBounds);
   1866             mDelegateView = delegateView;
   1867         }
   1868 
   1869         public void setBounds(Rect desiredBounds, Rect actualBounds) {
   1870             mTargetBounds.set(desiredBounds);
   1871             mSlopBounds.set(desiredBounds);
   1872             mSlopBounds.inset(-mSlop, -mSlop);
   1873             mActualBounds.set(actualBounds);
   1874         }
   1875 
   1876         @Override
   1877         public boolean onTouchEvent(MotionEvent event) {
   1878             final int x = (int) event.getX();
   1879             final int y = (int) event.getY();
   1880             boolean sendToDelegate = false;
   1881             boolean hit = true;
   1882             boolean handled = false;
   1883 
   1884             switch (event.getAction()) {
   1885                 case MotionEvent.ACTION_DOWN:
   1886                     if (mTargetBounds.contains(x, y)) {
   1887                         mDelegateTargeted = true;
   1888                         sendToDelegate = true;
   1889                     }
   1890                     break;
   1891                 case MotionEvent.ACTION_UP:
   1892                 case MotionEvent.ACTION_MOVE:
   1893                     sendToDelegate = mDelegateTargeted;
   1894                     if (sendToDelegate) {
   1895                         if (!mSlopBounds.contains(x, y)) {
   1896                             hit = false;
   1897                         }
   1898                     }
   1899                     break;
   1900                 case MotionEvent.ACTION_CANCEL:
   1901                     sendToDelegate = mDelegateTargeted;
   1902                     mDelegateTargeted = false;
   1903                     break;
   1904             }
   1905             if (sendToDelegate) {
   1906                 if (hit && !mActualBounds.contains(x, y)) {
   1907                     // Offset event coordinates to be in the center of the target view since we
   1908                     // are within the targetBounds, but not inside the actual bounds of
   1909                     // mDelegateView
   1910                     event.setLocation(mDelegateView.getWidth() / 2,
   1911                             mDelegateView.getHeight() / 2);
   1912                 } else {
   1913                     // Offset event coordinates to the target view coordinates.
   1914                     event.setLocation(x - mActualBounds.left, y - mActualBounds.top);
   1915                 }
   1916 
   1917                 handled = mDelegateView.dispatchTouchEvent(event);
   1918             }
   1919             return handled;
   1920         }
   1921     }
   1922 
   1923     /**
   1924      * Local subclass for AutoCompleteTextView.
   1925      * @hide
   1926      */
   1927     public static class SearchAutoComplete extends AutoCompleteTextView {
   1928 
   1929         private int mThreshold;
   1930         private SearchView mSearchView;
   1931 
   1932         private boolean mHasPendingShowSoftInputRequest;
   1933         final Runnable mRunShowSoftInputIfNecessary = () -> showSoftInputIfNecessary();
   1934 
   1935         public SearchAutoComplete(Context context) {
   1936             super(context);
   1937             mThreshold = getThreshold();
   1938         }
   1939 
   1940         @UnsupportedAppUsage
   1941         public SearchAutoComplete(Context context, AttributeSet attrs) {
   1942             super(context, attrs);
   1943             mThreshold = getThreshold();
   1944         }
   1945 
   1946         public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) {
   1947             super(context, attrs, defStyleAttrs);
   1948             mThreshold = getThreshold();
   1949         }
   1950 
   1951         public SearchAutoComplete(
   1952                 Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
   1953             super(context, attrs, defStyleAttrs, defStyleRes);
   1954             mThreshold = getThreshold();
   1955         }
   1956 
   1957         @Override
   1958         protected void onFinishInflate() {
   1959             super.onFinishInflate();
   1960             DisplayMetrics metrics = getResources().getDisplayMetrics();
   1961             setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
   1962                     getSearchViewTextMinWidthDp(), metrics));
   1963         }
   1964 
   1965         void setSearchView(SearchView searchView) {
   1966             mSearchView = searchView;
   1967         }
   1968 
   1969         @Override
   1970         public void setThreshold(int threshold) {
   1971             super.setThreshold(threshold);
   1972             mThreshold = threshold;
   1973         }
   1974 
   1975         /**
   1976          * Returns true if the text field is empty, or contains only whitespace.
   1977          */
   1978         private boolean isEmpty() {
   1979             return TextUtils.getTrimmedLength(getText()) == 0;
   1980         }
   1981 
   1982         /**
   1983          * We override this method to avoid replacing the query box text when a
   1984          * suggestion is clicked.
   1985          */
   1986         @Override
   1987         protected void replaceText(CharSequence text) {
   1988         }
   1989 
   1990         /**
   1991          * We override this method to avoid an extra onItemClick being called on
   1992          * the drop-down's OnItemClickListener by
   1993          * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
   1994          * clicked with the trackball.
   1995          */
   1996         @Override
   1997         public void performCompletion() {
   1998         }
   1999 
   2000         /**
   2001          * We override this method to be sure and show the soft keyboard if
   2002          * appropriate when the TextView has focus.
   2003          */
   2004         @Override
   2005         public void onWindowFocusChanged(boolean hasWindowFocus) {
   2006             super.onWindowFocusChanged(hasWindowFocus);
   2007 
   2008             if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
   2009                 // Since InputMethodManager#onPostWindowFocus() will be called after this callback,
   2010                 // it is a bit too early to call InputMethodManager#showSoftInput() here. We still
   2011                 // need to wait until the system calls back onCreateInputConnection() to call
   2012                 // InputMethodManager#showSoftInput().
   2013                 mHasPendingShowSoftInputRequest = true;
   2014 
   2015                 // If in landscape mode, then make sure that the ime is in front of the dropdown.
   2016                 if (isLandscapeMode(getContext())) {
   2017                     ensureImeVisible(true);
   2018                 }
   2019             }
   2020         }
   2021 
   2022         @Override
   2023         protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
   2024             super.onFocusChanged(focused, direction, previouslyFocusedRect);
   2025             mSearchView.onTextFocusChanged();
   2026         }
   2027 
   2028         /**
   2029          * We override this method so that we can allow a threshold of zero,
   2030          * which ACTV does not.
   2031          */
   2032         @Override
   2033         public boolean enoughToFilter() {
   2034             return mThreshold <= 0 || super.enoughToFilter();
   2035         }
   2036 
   2037         @Override
   2038         public boolean onKeyPreIme(int keyCode, KeyEvent event) {
   2039             final boolean consume = super.onKeyPreIme(keyCode, event);
   2040             if (consume && keyCode == KeyEvent.KEYCODE_BACK
   2041                     && event.getAction() == KeyEvent.ACTION_UP) {
   2042                 // If AutoCompleteTextView closed its pop-up, it will return true, in which case
   2043                 // we should also close the IME. Otherwise, the popup is already closed and we can
   2044                 // leave the BACK event alone.
   2045                 setImeVisibility(false);
   2046             }
   2047             return consume;
   2048         }
   2049 
   2050         /**
   2051          * Get minimum width of the search view text entry area.
   2052          */
   2053         private int getSearchViewTextMinWidthDp() {
   2054             final Configuration configuration = getResources().getConfiguration();
   2055             final int width = configuration.screenWidthDp;
   2056             final int height = configuration.screenHeightDp;
   2057             final int orientation = configuration.orientation;
   2058             if (width >= 960 && height >= 720
   2059                     && orientation == Configuration.ORIENTATION_LANDSCAPE) {
   2060                 return 256;
   2061             } else if (width >= 600 || (width >= 640 && height >= 480)) {
   2062                 return 192;
   2063             };
   2064             return 160;
   2065         }
   2066 
   2067         /**
   2068          * We override {@link View#onCreateInputConnection(EditorInfo)} as a signal to schedule a
   2069          * pending {@link InputMethodManager#showSoftInput(View, int)} request (if any).
   2070          */
   2071         @Override
   2072         public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
   2073             final InputConnection ic = super.onCreateInputConnection(editorInfo);
   2074             if (mHasPendingShowSoftInputRequest) {
   2075                 removeCallbacks(mRunShowSoftInputIfNecessary);
   2076                 post(mRunShowSoftInputIfNecessary);
   2077             }
   2078             return ic;
   2079         }
   2080 
   2081         @Override
   2082         public boolean checkInputConnectionProxy(View view) {
   2083             return view == mSearchView;
   2084         }
   2085 
   2086         private void showSoftInputIfNecessary() {
   2087             if (mHasPendingShowSoftInputRequest) {
   2088                 final InputMethodManager imm =
   2089                         getContext().getSystemService(InputMethodManager.class);
   2090                 imm.showSoftInput(this, 0);
   2091                 mHasPendingShowSoftInputRequest = false;
   2092             }
   2093         }
   2094 
   2095         private void setImeVisibility(final boolean visible) {
   2096             final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
   2097             if (!visible) {
   2098                 mHasPendingShowSoftInputRequest = false;
   2099                 removeCallbacks(mRunShowSoftInputIfNecessary);
   2100                 imm.hideSoftInputFromWindow(getWindowToken(), 0);
   2101                 return;
   2102             }
   2103 
   2104             if (imm.isActive(this)) {
   2105                 // This means that SearchAutoComplete is already connected to the IME.
   2106                 // InputMethodManager#showSoftInput() is guaranteed to pass client-side focus check.
   2107                 mHasPendingShowSoftInputRequest = false;
   2108                 removeCallbacks(mRunShowSoftInputIfNecessary);
   2109                 imm.showSoftInput(this, 0);
   2110                 return;
   2111             }
   2112 
   2113             // Otherwise, InputMethodManager#showSoftInput() should be deferred after
   2114             // onCreateInputConnection().
   2115             mHasPendingShowSoftInputRequest = true;
   2116         }
   2117     }
   2118 }
   2119