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