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