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