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