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