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