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