• 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             if (direction == FOCUS_BACKWARD) {
496                 final View found = focusSearch(FOCUS_BACKWARD);
497                 if (found != null) {
498                     return found.requestFocus(FOCUS_BACKWARD, previouslyFocusedRect);
499                 }
500                 return false;
501             }
502             boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect);
503             if (result) {
504                 updateViewsVisibility(false);
505             }
506             return result;
507         } else {
508             return super.requestFocus(direction, previouslyFocusedRect);
509         }
510     }
511 
512     /** @hide */
513     @Override
clearFocus()514     public void clearFocus() {
515         mClearingFocus = true;
516         super.clearFocus();
517         mSearchSrcTextView.clearFocus();
518         mSearchSrcTextView.setImeVisibility(false);
519         mClearingFocus = false;
520     }
521 
522     /**
523      * Sets a listener for user actions within the SearchView.
524      *
525      * @param listener the listener object that receives callbacks when the user performs
526      * actions in the SearchView such as clicking on buttons or typing a query.
527      */
setOnQueryTextListener(OnQueryTextListener listener)528     public void setOnQueryTextListener(OnQueryTextListener listener) {
529         mOnQueryChangeListener = listener;
530     }
531 
532     /**
533      * Sets a listener to inform when the user closes the SearchView.
534      *
535      * @param listener the listener to call when the user closes the SearchView.
536      */
setOnCloseListener(OnCloseListener listener)537     public void setOnCloseListener(OnCloseListener listener) {
538         mOnCloseListener = listener;
539     }
540 
541     /**
542      * Sets a listener to inform when the focus of the query text field changes.
543      *
544      * @param listener the listener to inform of focus changes.
545      */
setOnQueryTextFocusChangeListener(OnFocusChangeListener listener)546     public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
547         mOnQueryTextFocusChangeListener = listener;
548     }
549 
550     /**
551      * Sets a listener to inform when a suggestion is focused or clicked.
552      *
553      * @param listener the listener to inform of suggestion selection events.
554      */
setOnSuggestionListener(OnSuggestionListener listener)555     public void setOnSuggestionListener(OnSuggestionListener listener) {
556         mOnSuggestionListener = listener;
557     }
558 
559     /**
560      * Sets a listener to inform when the search button is pressed. This is only
561      * relevant when the text field is not visible by default. Calling {@link #setIconified
562      * setIconified(false)} can also cause this listener to be informed.
563      *
564      * @param listener the listener to inform when the search button is clicked or
565      * the text field is programmatically de-iconified.
566      */
setOnSearchClickListener(OnClickListener listener)567     public void setOnSearchClickListener(OnClickListener listener) {
568         mOnSearchClickListener = listener;
569     }
570 
571     /**
572      * Returns the query string currently in the text field.
573      *
574      * @return the query string
575      */
576     @InspectableProperty(hasAttributeId = false)
getQuery()577     public CharSequence getQuery() {
578         return mSearchSrcTextView.getText();
579     }
580 
581     /**
582      * Sets a query string in the text field and optionally submits the query as well.
583      *
584      * @param query the query string. This replaces any query text already present in the
585      * text field.
586      * @param submit whether to submit the query right now or only update the contents of
587      * text field.
588      */
setQuery(CharSequence query, boolean submit)589     public void setQuery(CharSequence query, boolean submit) {
590         mSearchSrcTextView.setText(query);
591         if (query != null) {
592             mSearchSrcTextView.setSelection(mSearchSrcTextView.length());
593             mUserQuery = query;
594         }
595 
596         // If the query is not empty and submit is requested, submit the query
597         if (submit && !TextUtils.isEmpty(query)) {
598             onSubmitQuery();
599         }
600     }
601 
602     /**
603      * Sets the hint text to display in the query text field. This overrides
604      * any hint specified in the {@link SearchableInfo}.
605      * <p>
606      * This value may be specified as an empty string to prevent any query hint
607      * from being displayed.
608      *
609      * @param hint the hint text to display or {@code null} to clear
610      * @attr ref android.R.styleable#SearchView_queryHint
611      */
setQueryHint(@ullable CharSequence hint)612     public void setQueryHint(@Nullable CharSequence hint) {
613         mQueryHint = hint;
614         updateQueryHint();
615     }
616 
617     /**
618      * Returns the hint text that will be displayed in the query text field.
619      * <p>
620      * The displayed query hint is chosen in the following order:
621      * <ol>
622      * <li>Non-null value set with {@link #setQueryHint(CharSequence)}
623      * <li>Value specified in XML using
624      *     {@link android.R.styleable#SearchView_queryHint android:queryHint}
625      * <li>Valid string resource ID exposed by the {@link SearchableInfo} via
626      *     {@link SearchableInfo#getHintId()}
627      * <li>Default hint provided by the theme against which the view was
628      *     inflated
629      * </ol>
630      *
631      * @return the displayed query hint text, or {@code null} if none set
632      * @attr ref android.R.styleable#SearchView_queryHint
633      */
634     @InspectableProperty
635     @Nullable
getQueryHint()636     public CharSequence getQueryHint() {
637         final CharSequence hint;
638         if (mQueryHint != null) {
639             hint = mQueryHint;
640         } else if (mSearchable != null && mSearchable.getHintId() != 0) {
641             hint = getContext().getText(mSearchable.getHintId());
642         } else {
643             hint = mDefaultQueryHint;
644         }
645         return hint;
646     }
647 
648     /**
649      * Sets the default or resting state of the search field. If true, a single search icon is
650      * shown by default and expands to show the text field and other buttons when pressed. Also,
651      * if the default state is iconified, then it collapses to that state when the close button
652      * is pressed. Changes to this property will take effect immediately.
653      *
654      * <p>The default value is true.</p>
655      *
656      * @param iconified whether the search field should be iconified by default
657      *
658      * @attr ref android.R.styleable#SearchView_iconifiedByDefault
659      */
setIconifiedByDefault(boolean iconified)660     public void setIconifiedByDefault(boolean iconified) {
661         if (mIconifiedByDefault == iconified) return;
662         mIconifiedByDefault = iconified;
663         updateViewsVisibility(iconified);
664         updateQueryHint();
665     }
666 
667     /**
668      * Returns the default iconified state of the search field.
669      * @return
670      *
671      * @deprecated use {@link #isIconifiedByDefault()}
672      * @attr ref android.R.styleable#SearchView_iconifiedByDefault
673      */
674     @Deprecated
isIconfiedByDefault()675     public boolean isIconfiedByDefault() {
676         return mIconifiedByDefault;
677     }
678 
679     /**
680      * Returns the default iconified state of the search field.
681      *
682      * @attr ref android.R.styleable#SearchView_iconifiedByDefault
683      */
684     @InspectableProperty
isIconifiedByDefault()685     public boolean isIconifiedByDefault() {
686         return mIconifiedByDefault;
687     }
688 
689     /**
690      * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
691      * a temporary state and does not override the default iconified state set by
692      * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
693      * a false here will only be valid until the user closes the field. And if the default
694      * state is expanded, then a true here will only clear the text field and not close it.
695      *
696      * @param iconify a true value will collapse the SearchView to an icon, while a false will
697      * expand it.
698      */
setIconified(boolean iconify)699     public void setIconified(boolean iconify) {
700         if (iconify) {
701             onCloseClicked();
702         } else {
703             onSearchClicked();
704         }
705     }
706 
707     /**
708      * Returns the current iconified state of the SearchView.
709      *
710      * @return true if the SearchView is currently iconified, false if the search field is
711      * fully visible.
712      */
713     @InspectableProperty(hasAttributeId = false)
isIconified()714     public boolean isIconified() {
715         return mIconified;
716     }
717 
718     /**
719      * Enables showing a submit button when the query is non-empty. In cases where the SearchView
720      * is being used to filter the contents of the current activity and doesn't launch a separate
721      * results activity, then the submit button should be disabled.
722      *
723      * @param enabled true to show a submit button for submitting queries, false if a submit
724      * button is not required.
725      */
setSubmitButtonEnabled(boolean enabled)726     public void setSubmitButtonEnabled(boolean enabled) {
727         mSubmitButtonEnabled = enabled;
728         updateViewsVisibility(isIconified());
729     }
730 
731     /**
732      * Returns whether the submit button is enabled when necessary or never displayed.
733      *
734      * @return whether the submit button is enabled automatically when necessary
735      */
isSubmitButtonEnabled()736     public boolean isSubmitButtonEnabled() {
737         return mSubmitButtonEnabled;
738     }
739 
740     /**
741      * Specifies if a query refinement button should be displayed alongside each suggestion
742      * or if it should depend on the flags set in the individual items retrieved from the
743      * suggestions provider. Clicking on the query refinement button will replace the text
744      * in the query text field with the text from the suggestion. This flag only takes effect
745      * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
746      * and not when using a custom adapter.
747      *
748      * @param enable true if all items should have a query refinement button, false if only
749      * those items that have a query refinement flag set should have the button.
750      *
751      * @see SearchManager#SUGGEST_COLUMN_FLAGS
752      * @see SearchManager#FLAG_QUERY_REFINEMENT
753      */
setQueryRefinementEnabled(boolean enable)754     public void setQueryRefinementEnabled(boolean enable) {
755         mQueryRefinement = enable;
756         if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
757             ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
758                     enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
759         }
760     }
761 
762     /**
763      * Returns whether query refinement is enabled for all items or only specific ones.
764      * @return true if enabled for all items, false otherwise.
765      */
isQueryRefinementEnabled()766     public boolean isQueryRefinementEnabled() {
767         return mQueryRefinement;
768     }
769 
770     /**
771      * You can set a custom adapter if you wish. Otherwise the default adapter is used to
772      * display the suggestions from the suggestions provider associated with the SearchableInfo.
773      *
774      * @see #setSearchableInfo(SearchableInfo)
775      */
setSuggestionsAdapter(CursorAdapter adapter)776     public void setSuggestionsAdapter(CursorAdapter adapter) {
777         mSuggestionsAdapter = adapter;
778 
779         mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
780     }
781 
782     /**
783      * Returns the adapter used for suggestions, if any.
784      * @return the suggestions adapter
785      */
getSuggestionsAdapter()786     public CursorAdapter getSuggestionsAdapter() {
787         return mSuggestionsAdapter;
788     }
789 
790     /**
791      * Makes the view at most this many pixels wide
792      *
793      * @attr ref android.R.styleable#SearchView_maxWidth
794      */
setMaxWidth(int maxpixels)795     public void setMaxWidth(int maxpixels) {
796         mMaxWidth = maxpixels;
797 
798         requestLayout();
799     }
800 
801     /**
802      * Gets the specified maximum width in pixels, if set. Returns zero if
803      * no maximum width was specified.
804      * @return the maximum width of the view
805      *
806      * @attr ref android.R.styleable#SearchView_maxWidth
807      */
808     @InspectableProperty
getMaxWidth()809     public int getMaxWidth() {
810         return mMaxWidth;
811     }
812 
813     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)814     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
815         // Let the standard measurements take effect in iconified state.
816         if (isIconified()) {
817             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
818             return;
819         }
820 
821         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
822         int width = MeasureSpec.getSize(widthMeasureSpec);
823 
824         switch (widthMode) {
825         case MeasureSpec.AT_MOST:
826             // If there is an upper limit, don't exceed maximum width (explicit or implicit)
827             if (mMaxWidth > 0) {
828                 width = Math.min(mMaxWidth, width);
829             } else {
830                 width = Math.min(getPreferredWidth(), width);
831             }
832             break;
833         case MeasureSpec.EXACTLY:
834             // If an exact width is specified, still don't exceed any specified maximum width
835             if (mMaxWidth > 0) {
836                 width = Math.min(mMaxWidth, width);
837             }
838             break;
839         case MeasureSpec.UNSPECIFIED:
840             // Use maximum width, if specified, else preferred width
841             width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
842             break;
843         }
844         widthMode = MeasureSpec.EXACTLY;
845 
846         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
847         int height = MeasureSpec.getSize(heightMeasureSpec);
848 
849         switch (heightMode) {
850             case MeasureSpec.AT_MOST:
851                 height = Math.min(getPreferredHeight(), height);
852                 break;
853             case MeasureSpec.UNSPECIFIED:
854                 height = getPreferredHeight();
855                 break;
856         }
857         heightMode = MeasureSpec.EXACTLY;
858 
859         super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode),
860                 MeasureSpec.makeMeasureSpec(height, heightMode));
861     }
862 
863     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)864     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
865         super.onLayout(changed, left, top, right, bottom);
866 
867         if (changed) {
868             // Expand mSearchSrcTextView touch target to be the height of the parent in order to
869             // allow it to be up to 48dp.
870             getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds);
871             mSearchSrtTextViewBoundsExpanded.set(
872                     mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top);
873             if (mTouchDelegate == null) {
874                 mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded,
875                         mSearchSrcTextViewBounds, mSearchSrcTextView);
876                 setTouchDelegate(mTouchDelegate);
877             } else {
878                 mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds);
879             }
880         }
881     }
882 
getChildBoundsWithinSearchView(View view, Rect rect)883     private void getChildBoundsWithinSearchView(View view, Rect rect) {
884         view.getLocationInWindow(mTemp);
885         getLocationInWindow(mTemp2);
886         final int top = mTemp[1] - mTemp2[1];
887         final int left = mTemp[0] - mTemp2[0];
888         rect.set(left , top, left + view.getWidth(), top + view.getHeight());
889     }
890 
getPreferredWidth()891     private int getPreferredWidth() {
892         return getContext().getResources()
893                 .getDimensionPixelSize(R.dimen.search_view_preferred_width);
894     }
895 
getPreferredHeight()896     private int getPreferredHeight() {
897         return getContext().getResources()
898                 .getDimensionPixelSize(R.dimen.search_view_preferred_height);
899     }
900 
901     @UnsupportedAppUsage
updateViewsVisibility(final boolean collapsed)902     private void updateViewsVisibility(final boolean collapsed) {
903         mIconified = collapsed;
904         // Visibility of views that are visible when collapsed
905         final int visCollapsed = collapsed ? VISIBLE : GONE;
906         // Is there text in the query
907         final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
908 
909         mSearchButton.setVisibility(visCollapsed);
910         updateSubmitButton(hasText);
911         mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
912 
913         final int iconVisibility;
914         if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) {
915             iconVisibility = GONE;
916         } else {
917             iconVisibility = VISIBLE;
918         }
919         mCollapsedIcon.setVisibility(iconVisibility);
920 
921         updateCloseButton();
922         updateVoiceButton(!hasText);
923         updateSubmitArea();
924     }
925 
hasVoiceSearch()926     private boolean hasVoiceSearch() {
927         if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
928             Intent testIntent = null;
929             if (mSearchable.getVoiceSearchLaunchWebSearch()) {
930                 testIntent = mVoiceWebSearchIntent;
931             } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
932                 testIntent = mVoiceAppSearchIntent;
933             }
934             if (testIntent != null) {
935                 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
936                         PackageManager.MATCH_DEFAULT_ONLY);
937                 return ri != null;
938             }
939         }
940         return false;
941     }
942 
isSubmitAreaEnabled()943     private boolean isSubmitAreaEnabled() {
944         return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
945     }
946 
947     @UnsupportedAppUsage
updateSubmitButton(boolean hasText)948     private void updateSubmitButton(boolean hasText) {
949         int visibility = GONE;
950         if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
951                 && (hasText || !mVoiceButtonEnabled)) {
952             visibility = VISIBLE;
953         }
954         mGoButton.setVisibility(visibility);
955     }
956 
957     @UnsupportedAppUsage
updateSubmitArea()958     private void updateSubmitArea() {
959         int visibility = GONE;
960         if (isSubmitAreaEnabled()
961                 && (mGoButton.getVisibility() == VISIBLE
962                         || mVoiceButton.getVisibility() == VISIBLE)) {
963             visibility = VISIBLE;
964         }
965         mSubmitArea.setVisibility(visibility);
966     }
967 
updateCloseButton()968     private void updateCloseButton() {
969         final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
970         // Should we show the close button? It is not shown if there's no focus,
971         // field is not iconified by default and there is no text in it.
972         final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
973         mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
974         final Drawable closeButtonImg = mCloseButton.getDrawable();
975         if (closeButtonImg != null){
976             closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
977         }
978     }
979 
postUpdateFocusedState()980     private void postUpdateFocusedState() {
981         post(mUpdateDrawableStateRunnable);
982     }
983 
updateFocusedState()984     private void updateFocusedState() {
985         final boolean focused = mSearchSrcTextView.hasFocus();
986         final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET;
987         final Drawable searchPlateBg = mSearchPlate.getBackground();
988         if (searchPlateBg != null) {
989             searchPlateBg.setState(stateSet);
990         }
991         final Drawable submitAreaBg = mSubmitArea.getBackground();
992         if (submitAreaBg != null) {
993             submitAreaBg.setState(stateSet);
994         }
995         invalidate();
996     }
997 
998     @Override
onDetachedFromWindow()999     protected void onDetachedFromWindow() {
1000         removeCallbacks(mUpdateDrawableStateRunnable);
1001         post(mReleaseCursorRunnable);
1002         super.onDetachedFromWindow();
1003     }
1004 
1005     /**
1006      * Called by the SuggestionsAdapter
1007      * @hide
1008      */
onQueryRefine(CharSequence queryText)1009     /* package */void onQueryRefine(CharSequence queryText) {
1010         setQuery(queryText);
1011     }
1012 
1013     @UnsupportedAppUsage
1014     private final OnClickListener mOnClickListener = new OnClickListener() {
1015 
1016         public void onClick(View v) {
1017             if (v == mSearchButton) {
1018                 onSearchClicked();
1019             } else if (v == mCloseButton) {
1020                 onCloseClicked();
1021             } else if (v == mGoButton) {
1022                 onSubmitQuery();
1023             } else if (v == mVoiceButton) {
1024                 onVoiceClicked();
1025             } else if (v == mSearchSrcTextView) {
1026                 forceSuggestionQuery();
1027             }
1028         }
1029     };
1030 
1031     /**
1032      * Handles the key down event for dealing with action keys.
1033      *
1034      * @param keyCode This is the keycode of the typed key, and is the same value as
1035      *        found in the KeyEvent parameter.
1036      * @param event The complete event record for the typed key
1037      *
1038      * @return true if the event was handled here, or false if not.
1039      */
1040     @Override
onKeyDown(int keyCode, KeyEvent event)1041     public boolean onKeyDown(int keyCode, KeyEvent event) {
1042         if (mSearchable == null) {
1043             return false;
1044         }
1045 
1046         // if it's an action specified by the searchable activity, launch the
1047         // entered query with the action key
1048         SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1049         if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
1050             launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText()
1051                     .toString());
1052             return true;
1053         }
1054 
1055         return super.onKeyDown(keyCode, event);
1056     }
1057 
1058     /**
1059      * React to the user typing "enter" or other hardwired keys while typing in
1060      * the search box. This handles these special keys while the edit box has
1061      * focus.
1062      */
1063     View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
1064         public boolean onKey(View v, int keyCode, KeyEvent event) {
1065             // guard against possible race conditions
1066             if (mSearchable == null) {
1067                 return false;
1068             }
1069 
1070             if (DBG) {
1071                 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
1072                         + mSearchSrcTextView.getListSelection());
1073             }
1074 
1075             // If a suggestion is selected, handle enter, search key, and action keys
1076             // as presses on the selected suggestion
1077             if (mSearchSrcTextView.isPopupShowing()
1078                     && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) {
1079                 return onSuggestionsKey(v, keyCode, event);
1080             }
1081 
1082             // If there is text in the query box, handle enter, and action keys
1083             // The search key is handled by the dialog's onKeyDown().
1084             if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) {
1085                 if (event.getAction() == KeyEvent.ACTION_UP) {
1086                     if (keyCode == KeyEvent.KEYCODE_ENTER
1087                             || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER) {
1088                         v.cancelLongPress();
1089 
1090                         // Launch as a regular search.
1091                         launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText()
1092                                 .toString());
1093                         return true;
1094                     }
1095                 }
1096                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1097                     SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1098                     if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
1099                         launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView
1100                                 .getText().toString());
1101                         return true;
1102                     }
1103                 }
1104             }
1105             return false;
1106         }
1107     };
1108 
1109     /**
1110      * React to the user typing while in the suggestions list. First, check for
1111      * action keys. If not handled, try refocusing regular characters into the
1112      * EditText.
1113      */
onSuggestionsKey(View v, int keyCode, KeyEvent event)1114     private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1115         // guard against possible race conditions (late arrival after dismiss)
1116         if (mSearchable == null) {
1117             return false;
1118         }
1119         if (mSuggestionsAdapter == null) {
1120             return false;
1121         }
1122         if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
1123             // First, check for enter or search (both of which we'll treat as a
1124             // "click")
1125             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
1126                     || keyCode == KeyEvent.KEYCODE_TAB) {
1127                 int position = mSearchSrcTextView.getListSelection();
1128                 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
1129             }
1130 
1131             // Next, check for left/right moves, which we use to "return" the
1132             // user to the edit view
1133             if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
1134                 // give "focus" to text editor, with cursor at the beginning if
1135                 // left key, at end if right key
1136                 // TODO: Reverse left/right for right-to-left languages, e.g.
1137                 // Arabic
1138                 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView
1139                         .length();
1140                 mSearchSrcTextView.setSelection(selPoint);
1141                 mSearchSrcTextView.setListSelection(0);
1142                 mSearchSrcTextView.clearListSelection();
1143                 mSearchSrcTextView.ensureImeVisible(true);
1144 
1145                 return true;
1146             }
1147 
1148             // Next, check for an "up and out" move
1149             if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) {
1150                 // TODO: restoreUserQuery();
1151                 // let ACTV complete the move
1152                 return false;
1153             }
1154 
1155             // Next, check for an "action key"
1156             SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1157             if ((actionKey != null)
1158                     && ((actionKey.getSuggestActionMsg() != null) || (actionKey
1159                             .getSuggestActionMsgColumn() != null))) {
1160                 // launch suggestion using action key column
1161                 int position = mSearchSrcTextView.getListSelection();
1162                 if (position != ListView.INVALID_POSITION) {
1163                     Cursor c = mSuggestionsAdapter.getCursor();
1164                     if (c.moveToPosition(position)) {
1165                         final String actionMsg = getActionKeyMessage(c, actionKey);
1166                         if (actionMsg != null && (actionMsg.length() > 0)) {
1167                             return onItemClicked(position, keyCode, actionMsg);
1168                         }
1169                     }
1170                 }
1171             }
1172         }
1173         return false;
1174     }
1175 
1176     /**
1177      * For a given suggestion and a given cursor row, get the action message. If
1178      * not provided by the specific row/column, also check for a single
1179      * definition (for the action key).
1180      *
1181      * @param c The cursor providing suggestions
1182      * @param actionKey The actionkey record being examined
1183      *
1184      * @return Returns a string, or null if no action key message for this
1185      *         suggestion
1186      */
getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey)1187     private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
1188         String result = null;
1189         // check first in the cursor data, for a suggestion-specific message
1190         final String column = actionKey.getSuggestActionMsgColumn();
1191         if (column != null) {
1192             result = SuggestionsAdapter.getColumnString(c, column);
1193         }
1194         // If the cursor didn't give us a message, see if there's a single
1195         // message defined
1196         // for the actionkey (for all suggestions)
1197         if (result == null) {
1198             result = actionKey.getSuggestActionMsg();
1199         }
1200         return result;
1201     }
1202 
getDecoratedHint(CharSequence hintText)1203     private CharSequence getDecoratedHint(CharSequence hintText) {
1204         // If the field is always expanded or we don't have a search hint icon,
1205         // then don't add the search icon to the hint.
1206         if (!mIconifiedByDefault || mSearchHintIcon == null) {
1207             return hintText;
1208         }
1209 
1210         final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25);
1211         mSearchHintIcon.setBounds(0, 0, textSize, textSize);
1212 
1213         final SpannableStringBuilder ssb = new SpannableStringBuilder("   ");
1214         ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1215         ssb.append(hintText);
1216         return ssb;
1217     }
1218 
updateQueryHint()1219     private void updateQueryHint() {
1220         final CharSequence hint = getQueryHint();
1221         mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint));
1222     }
1223 
1224     /**
1225      * Updates the auto-complete text view.
1226      */
updateSearchAutoComplete()1227     private void updateSearchAutoComplete() {
1228         mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation
1229         mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold());
1230         mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions());
1231         int inputType = mSearchable.getInputType();
1232         // We only touch this if the input type is set up for text (which it almost certainly
1233         // should be, in the case of search!)
1234         if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
1235             // The existence of a suggestions authority is the proxy for "suggestions
1236             // are available here"
1237             inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1238             if (mSearchable.getSuggestAuthority() != null) {
1239                 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1240                 // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
1241                 // auto-completion based on its own semantics, which it will present to the user
1242                 // as they type. This generally means that the input method should not show its
1243                 // own candidates, and the spell checker should not be in action. The text editor
1244                 // supplies its candidates by calling InputMethodManager.displayCompletions(),
1245                 // which in turn will call InputMethodSession.displayCompletions().
1246                 inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
1247             }
1248         }
1249         mSearchSrcTextView.setInputType(inputType);
1250         if (mSuggestionsAdapter != null) {
1251             mSuggestionsAdapter.changeCursor(null);
1252         }
1253         // attach the suggestions adapter, if suggestions are available
1254         // The existence of a suggestions authority is the proxy for "suggestions available here"
1255         if (mSearchable.getSuggestAuthority() != null) {
1256             mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
1257                     this, mSearchable, mOutsideDrawablesCache);
1258             mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
1259             ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
1260                     mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
1261                     : SuggestionsAdapter.REFINE_BY_ENTRY);
1262         }
1263     }
1264 
1265     /**
1266      * Update the visibility of the voice button.  There are actually two voice search modes,
1267      * either of which will activate the button.
1268      * @param empty whether the search query text field is empty. If it is, then the other
1269      * criteria apply to make the voice button visible.
1270      */
updateVoiceButton(boolean empty)1271     private void updateVoiceButton(boolean empty) {
1272         int visibility = GONE;
1273         if (mVoiceButtonEnabled && !isIconified() && empty) {
1274             visibility = VISIBLE;
1275             mGoButton.setVisibility(GONE);
1276         }
1277         mVoiceButton.setVisibility(visibility);
1278     }
1279 
1280     private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
1281 
1282         /**
1283          * Called when the input method default action key is pressed.
1284          */
1285         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
1286             onSubmitQuery();
1287             return true;
1288         }
1289     };
1290 
onTextChanged(CharSequence newText)1291     private void onTextChanged(CharSequence newText) {
1292         CharSequence text = mSearchSrcTextView.getText();
1293         mUserQuery = text;
1294         boolean hasText = !TextUtils.isEmpty(text);
1295         updateSubmitButton(hasText);
1296         updateVoiceButton(!hasText);
1297         updateCloseButton();
1298         updateSubmitArea();
1299         if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
1300             mOnQueryChangeListener.onQueryTextChange(newText.toString());
1301         }
1302         mOldQueryText = newText.toString();
1303     }
1304 
onSubmitQuery()1305     private void onSubmitQuery() {
1306         CharSequence query = mSearchSrcTextView.getText();
1307         if (query != null && TextUtils.getTrimmedLength(query) > 0) {
1308             if (mOnQueryChangeListener == null
1309                     || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
1310                 if (mSearchable != null) {
1311                     launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
1312                 }
1313                 mSearchSrcTextView.setImeVisibility(false);
1314                 dismissSuggestions();
1315             }
1316         }
1317     }
1318 
dismissSuggestions()1319     private void dismissSuggestions() {
1320         mSearchSrcTextView.dismissDropDown();
1321     }
1322 
1323     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
onCloseClicked()1324     private void onCloseClicked() {
1325         CharSequence text = mSearchSrcTextView.getText();
1326         if (TextUtils.isEmpty(text)) {
1327             if (mIconifiedByDefault) {
1328                 // If the app doesn't override the close behavior
1329                 if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
1330                     // hide the keyboard and remove focus
1331                     clearFocus();
1332                     // collapse the search field
1333                     updateViewsVisibility(true);
1334                 }
1335             }
1336         } else {
1337             mSearchSrcTextView.setText("");
1338             mSearchSrcTextView.requestFocus();
1339             mSearchSrcTextView.setImeVisibility(true);
1340         }
1341 
1342     }
1343 
onSearchClicked()1344     private void onSearchClicked() {
1345         updateViewsVisibility(false);
1346         mSearchSrcTextView.requestFocus();
1347         mSearchSrcTextView.setImeVisibility(true);
1348         if (mOnSearchClickListener != null) {
1349             mOnSearchClickListener.onClick(this);
1350         }
1351     }
1352 
onVoiceClicked()1353     private void onVoiceClicked() {
1354         // guard against possible race conditions
1355         if (mSearchable == null) {
1356             return;
1357         }
1358         SearchableInfo searchable = mSearchable;
1359         try {
1360             if (searchable.getVoiceSearchLaunchWebSearch()) {
1361                 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
1362                         searchable);
1363                 getContext().startActivity(webSearchIntent);
1364             } else if (searchable.getVoiceSearchLaunchRecognizer()) {
1365                 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
1366                         searchable);
1367                 getContext().startActivity(appSearchIntent);
1368             }
1369         } catch (ActivityNotFoundException e) {
1370             // Should not happen, since we check the availability of
1371             // voice search before showing the button. But just in case...
1372             Log.w(LOG_TAG, "Could not find voice search activity");
1373         }
1374     }
1375 
onTextFocusChanged()1376     void onTextFocusChanged() {
1377         updateViewsVisibility(isIconified());
1378         // Delayed update to make sure that the focus has settled down and window focus changes
1379         // don't affect it. A synchronous update was not working.
1380         postUpdateFocusedState();
1381         if (mSearchSrcTextView.hasFocus()) {
1382             forceSuggestionQuery();
1383         }
1384     }
1385 
1386     @Override
onWindowFocusChanged(boolean hasWindowFocus)1387     public void onWindowFocusChanged(boolean hasWindowFocus) {
1388         super.onWindowFocusChanged(hasWindowFocus);
1389 
1390         postUpdateFocusedState();
1391     }
1392 
1393     /**
1394      * {@inheritDoc}
1395      */
1396     @Override
onActionViewCollapsed()1397     public void onActionViewCollapsed() {
1398         setQuery("", false);
1399         clearFocus();
1400         updateViewsVisibility(true);
1401         mSearchSrcTextView.setImeOptions(mCollapsedImeOptions);
1402         mExpandedInActionView = false;
1403     }
1404 
1405     /**
1406      * {@inheritDoc}
1407      */
1408     @Override
onActionViewExpanded()1409     public void onActionViewExpanded() {
1410         if (mExpandedInActionView) return;
1411 
1412         mExpandedInActionView = true;
1413         mCollapsedImeOptions = mSearchSrcTextView.getImeOptions();
1414         mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
1415         mSearchSrcTextView.setText("");
1416         setIconified(false);
1417     }
1418 
1419     static class SavedState extends BaseSavedState {
1420         boolean isIconified;
1421 
SavedState(Parcelable superState)1422         SavedState(Parcelable superState) {
1423             super(superState);
1424         }
1425 
SavedState(Parcel source)1426         public SavedState(Parcel source) {
1427             super(source);
1428             isIconified = (Boolean) source.readValue(null);
1429         }
1430 
1431         @Override
writeToParcel(Parcel dest, int flags)1432         public void writeToParcel(Parcel dest, int flags) {
1433             super.writeToParcel(dest, flags);
1434             dest.writeValue(isIconified);
1435         }
1436 
1437         @Override
toString()1438         public String toString() {
1439             return "SearchView.SavedState{"
1440                     + Integer.toHexString(System.identityHashCode(this))
1441                     + " isIconified=" + isIconified + "}";
1442         }
1443 
1444         public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR =
1445                 new Parcelable.Creator<SavedState>() {
1446                     public SavedState createFromParcel(Parcel in) {
1447                         return new SavedState(in);
1448                     }
1449 
1450                     public SavedState[] newArray(int size) {
1451                         return new SavedState[size];
1452                     }
1453                 };
1454     }
1455 
1456     @Override
onSaveInstanceState()1457     protected Parcelable onSaveInstanceState() {
1458         Parcelable superState = super.onSaveInstanceState();
1459         SavedState ss = new SavedState(superState);
1460         ss.isIconified = isIconified();
1461         return ss;
1462     }
1463 
1464     @Override
onRestoreInstanceState(Parcelable state)1465     protected void onRestoreInstanceState(Parcelable state) {
1466         SavedState ss = (SavedState) state;
1467         super.onRestoreInstanceState(ss.getSuperState());
1468         updateViewsVisibility(ss.isIconified);
1469         requestLayout();
1470     }
1471 
1472     @Override
getAccessibilityClassName()1473     public CharSequence getAccessibilityClassName() {
1474         return SearchView.class.getName();
1475     }
1476 
adjustDropDownSizeAndPosition()1477     private void adjustDropDownSizeAndPosition() {
1478         if (mDropDownAnchor.getWidth() > 1) {
1479             Resources res = getContext().getResources();
1480             int anchorPadding = mSearchPlate.getPaddingLeft();
1481             Rect dropDownPadding = new Rect();
1482             final boolean isLayoutRtl = isLayoutRtl();
1483             int iconOffset = mIconifiedByDefault
1484                     ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
1485                     + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
1486                     : 0;
1487             mSearchSrcTextView.getDropDownBackground().getPadding(dropDownPadding);
1488             int offset;
1489             if (isLayoutRtl) {
1490                 offset = - dropDownPadding.left;
1491             } else {
1492                 offset = anchorPadding - (dropDownPadding.left + iconOffset);
1493             }
1494             mSearchSrcTextView.setDropDownHorizontalOffset(offset);
1495             final int width = mDropDownAnchor.getWidth() + dropDownPadding.left
1496                     + dropDownPadding.right + iconOffset - anchorPadding;
1497             mSearchSrcTextView.setDropDownWidth(width);
1498         }
1499     }
1500 
onItemClicked(int position, int actionKey, String actionMsg)1501     private boolean onItemClicked(int position, int actionKey, String actionMsg) {
1502         if (mOnSuggestionListener == null
1503                 || !mOnSuggestionListener.onSuggestionClick(position)) {
1504             launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1505             mSearchSrcTextView.setImeVisibility(false);
1506             dismissSuggestions();
1507             return true;
1508         }
1509         return false;
1510     }
1511 
onItemSelected(int position)1512     private boolean onItemSelected(int position) {
1513         if (mOnSuggestionListener == null
1514                 || !mOnSuggestionListener.onSuggestionSelect(position)) {
1515             rewriteQueryFromSuggestion(position);
1516             return true;
1517         }
1518         return false;
1519     }
1520 
1521     @UnsupportedAppUsage
1522     private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
1523 
1524         /**
1525          * Implements OnItemClickListener
1526          */
1527         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1528             if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1529             onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
1530         }
1531     };
1532 
1533     private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
1534 
1535         /**
1536          * Implements OnItemSelectedListener
1537          */
1538         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
1539             if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1540             SearchView.this.onItemSelected(position);
1541         }
1542 
1543         /**
1544          * Implements OnItemSelectedListener
1545          */
1546         public void onNothingSelected(AdapterView<?> parent) {
1547             if (DBG)
1548                 Log.d(LOG_TAG, "onNothingSelected()");
1549         }
1550     };
1551 
1552     /**
1553      * Query rewriting.
1554      */
rewriteQueryFromSuggestion(int position)1555     private void rewriteQueryFromSuggestion(int position) {
1556         CharSequence oldQuery = mSearchSrcTextView.getText();
1557         Cursor c = mSuggestionsAdapter.getCursor();
1558         if (c == null) {
1559             return;
1560         }
1561         if (c.moveToPosition(position)) {
1562             // Get the new query from the suggestion.
1563             CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1564             if (newQuery != null) {
1565                 // The suggestion rewrites the query.
1566                 // Update the text field, without getting new suggestions.
1567                 setQuery(newQuery);
1568             } else {
1569                 // The suggestion does not rewrite the query, restore the user's query.
1570                 setQuery(oldQuery);
1571             }
1572         } else {
1573             // We got a bad position, restore the user's query.
1574             setQuery(oldQuery);
1575         }
1576     }
1577 
1578     /**
1579      * Launches an intent based on a suggestion.
1580      *
1581      * @param position The index of the suggestion to create the intent from.
1582      * @param actionKey The key code of the action key that was pressed,
1583      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1584      * @param actionMsg The message for the action key that was pressed,
1585      *        or <code>null</code> if none.
1586      * @return true if a successful launch, false if could not (e.g. bad position).
1587      */
launchSuggestion(int position, int actionKey, String actionMsg)1588     private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1589         Cursor c = mSuggestionsAdapter.getCursor();
1590         if ((c != null) && c.moveToPosition(position)) {
1591 
1592             Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1593 
1594             // launch the intent
1595             launchIntent(intent);
1596 
1597             return true;
1598         }
1599         return false;
1600     }
1601 
1602     /**
1603      * Launches an intent, including any special intent handling.
1604      */
launchIntent(Intent intent)1605     private void launchIntent(Intent intent) {
1606         if (intent == null) {
1607             return;
1608         }
1609         try {
1610             // If the intent was created from a suggestion, it will always have an explicit
1611             // component here.
1612             getContext().startActivity(intent);
1613         } catch (RuntimeException ex) {
1614             Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1615         }
1616     }
1617 
1618     /**
1619      * Sets the text in the query box, without updating the suggestions.
1620      */
1621     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
setQuery(CharSequence query)1622     private void setQuery(CharSequence query) {
1623         mSearchSrcTextView.setText(query, true);
1624         // Move the cursor to the end
1625         mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
1626     }
1627 
launchQuerySearch(int actionKey, String actionMsg, String query)1628     private void launchQuerySearch(int actionKey, String actionMsg, String query) {
1629         String action = Intent.ACTION_SEARCH;
1630         Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
1631         getContext().startActivity(intent);
1632     }
1633 
1634     /**
1635      * Constructs an intent from the given information and the search dialog state.
1636      *
1637      * @param action Intent action.
1638      * @param data Intent data, or <code>null</code>.
1639      * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1640      * @param query Intent query, or <code>null</code>.
1641      * @param actionKey The key code of the action key that was pressed,
1642      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1643      * @param actionMsg The message for the action key that was pressed,
1644      *        or <code>null</code> if none.
1645      * @param mode The search mode, one of the acceptable values for
1646      *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
1647      * @return The intent.
1648      */
createIntent(String action, Uri data, String extraData, String query, int actionKey, String actionMsg)1649     private Intent createIntent(String action, Uri data, String extraData, String query,
1650             int actionKey, String actionMsg) {
1651         // Now build the Intent
1652         Intent intent = new Intent(action);
1653         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1654         // We need CLEAR_TOP to avoid reusing an old task that has other activities
1655         // on top of the one we want. We don't want to do this in in-app search though,
1656         // as it can be destructive to the activity stack.
1657         if (data != null) {
1658             intent.setData(data);
1659         }
1660         intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
1661         if (query != null) {
1662             intent.putExtra(SearchManager.QUERY, query);
1663         }
1664         if (extraData != null) {
1665             intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1666         }
1667         if (mAppSearchData != null) {
1668             intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1669         }
1670         if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1671             intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1672             intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1673         }
1674         intent.setComponent(mSearchable.getSearchActivity());
1675         return intent;
1676     }
1677 
1678     /**
1679      * Create and return an Intent that can launch the voice search activity for web search.
1680      */
createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable)1681     private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1682         Intent voiceIntent = new Intent(baseIntent);
1683         ComponentName searchActivity = searchable.getSearchActivity();
1684         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1685                 : searchActivity.flattenToShortString());
1686         return voiceIntent;
1687     }
1688 
1689     /**
1690      * Create and return an Intent that can launch the voice search activity, perform a specific
1691      * voice transcription, and forward the results to the searchable activity.
1692      *
1693      * @param baseIntent The voice app search intent to start from
1694      * @return A completely-configured intent ready to send to the voice search activity
1695      */
createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable)1696     private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1697         ComponentName searchActivity = searchable.getSearchActivity();
1698 
1699         // create the necessary intent to set up a search-and-forward operation
1700         // in the voice search system.   We have to keep the bundle separate,
1701         // because it becomes immutable once it enters the PendingIntent
1702         Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
1703         queryIntent.setComponent(searchActivity);
1704         PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
1705                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE);
1706 
1707         // Now set up the bundle that will be inserted into the pending intent
1708         // when it's time to do the search.  We always build it here (even if empty)
1709         // because the voice search activity will always need to insert "QUERY" into
1710         // it anyway.
1711         Bundle queryExtras = new Bundle();
1712         if (mAppSearchData != null) {
1713             queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
1714         }
1715 
1716         // Now build the intent to launch the voice search.  Add all necessary
1717         // extras to launch the voice recognizer, and then all the necessary extras
1718         // to forward the results to the searchable activity
1719         Intent voiceIntent = new Intent(baseIntent);
1720 
1721         // Add all of the configuration options supplied by the searchable's metadata
1722         String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
1723         String prompt = null;
1724         String language = null;
1725         int maxResults = 1;
1726 
1727         Resources resources = getResources();
1728         if (searchable.getVoiceLanguageModeId() != 0) {
1729             languageModel = resources.getString(searchable.getVoiceLanguageModeId());
1730         }
1731         if (searchable.getVoicePromptTextId() != 0) {
1732             prompt = resources.getString(searchable.getVoicePromptTextId());
1733         }
1734         if (searchable.getVoiceLanguageId() != 0) {
1735             language = resources.getString(searchable.getVoiceLanguageId());
1736         }
1737         if (searchable.getVoiceMaxResults() != 0) {
1738             maxResults = searchable.getVoiceMaxResults();
1739         }
1740         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
1741         voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
1742         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
1743         voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
1744         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1745                 : searchActivity.flattenToShortString());
1746 
1747         // Add the values that configure forwarding the results
1748         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
1749         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
1750 
1751         return voiceIntent;
1752     }
1753 
1754     /**
1755      * When a particular suggestion has been selected, perform the various lookups required
1756      * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
1757      * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
1758      * the suggestion includes a data id.
1759      *
1760      * @param c The suggestions cursor, moved to the row of the user's selection
1761      * @param actionKey The key code of the action key that was pressed,
1762      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1763      * @param actionMsg The message for the action key that was pressed,
1764      *        or <code>null</code> if none.
1765      * @return An intent for the suggestion at the cursor's position.
1766      */
createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg)1767     private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1768         try {
1769             // use specific action if supplied, or default action if supplied, or fixed default
1770             String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1771 
1772             if (action == null) {
1773                 action = mSearchable.getSuggestIntentAction();
1774             }
1775             if (action == null) {
1776                 action = Intent.ACTION_SEARCH;
1777             }
1778 
1779             // use specific data if supplied, or default data if supplied
1780             String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1781             if (data == null) {
1782                 data = mSearchable.getSuggestIntentData();
1783             }
1784             // then, if an ID was provided, append it.
1785             if (data != null) {
1786                 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1787                 if (id != null) {
1788                     data = data + "/" + Uri.encode(id);
1789                 }
1790             }
1791             Uri dataUri = (data == null) ? null : Uri.parse(data);
1792 
1793             String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1794             String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1795 
1796             return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
1797         } catch (RuntimeException e ) {
1798             int rowNum;
1799             try {                       // be really defensive now
1800                 rowNum = c.getPosition();
1801             } catch (RuntimeException e2 ) {
1802                 rowNum = -1;
1803             }
1804             Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
1805                             " returned exception.", e);
1806             return null;
1807         }
1808     }
1809 
forceSuggestionQuery()1810     private void forceSuggestionQuery() {
1811         mSearchSrcTextView.doBeforeTextChanged();
1812         mSearchSrcTextView.doAfterTextChanged();
1813     }
1814 
isLandscapeMode(Context context)1815     static boolean isLandscapeMode(Context context) {
1816         return context.getResources().getConfiguration().orientation
1817                 == Configuration.ORIENTATION_LANDSCAPE;
1818     }
1819 
1820     /**
1821      * Callback to watch the text field for empty/non-empty
1822      */
1823     private TextWatcher mTextWatcher = new TextWatcher() {
1824 
1825         public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
1826 
1827         public void onTextChanged(CharSequence s, int start,
1828                 int before, int after) {
1829             SearchView.this.onTextChanged(s);
1830         }
1831 
1832         public void afterTextChanged(Editable s) {
1833         }
1834     };
1835 
1836     private static class UpdatableTouchDelegate extends TouchDelegate {
1837         /**
1838          * View that should receive forwarded touch events
1839          */
1840         private final View mDelegateView;
1841 
1842         /**
1843          * Bounds in local coordinates of the containing view that should be mapped to the delegate
1844          * view. This rect is used for initial hit testing.
1845          */
1846         private final Rect mTargetBounds;
1847 
1848         /**
1849          * Bounds in local coordinates of the containing view that are actual bounds of the delegate
1850          * view. This rect is used for event coordinate mapping.
1851          */
1852         private final Rect mActualBounds;
1853 
1854         /**
1855          * mTargetBounds inflated to include some slop. This rect is to track whether the motion events
1856          * should be considered to be be within the delegate view.
1857          */
1858         private final Rect mSlopBounds;
1859 
1860         private final int mSlop;
1861 
1862         /**
1863          * True if the delegate had been targeted on a down event (intersected mTargetBounds).
1864          */
1865         private boolean mDelegateTargeted;
1866 
UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView)1867         public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) {
1868             super(targetBounds, delegateView);
1869             mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
1870             mTargetBounds = new Rect();
1871             mSlopBounds = new Rect();
1872             mActualBounds = new Rect();
1873             setBounds(targetBounds, actualBounds);
1874             mDelegateView = delegateView;
1875         }
1876 
setBounds(Rect desiredBounds, Rect actualBounds)1877         public void setBounds(Rect desiredBounds, Rect actualBounds) {
1878             mTargetBounds.set(desiredBounds);
1879             mSlopBounds.set(desiredBounds);
1880             mSlopBounds.inset(-mSlop, -mSlop);
1881             mActualBounds.set(actualBounds);
1882         }
1883 
1884         @Override
onTouchEvent(MotionEvent event)1885         public boolean onTouchEvent(MotionEvent event) {
1886             final int x = (int) event.getX();
1887             final int y = (int) event.getY();
1888             boolean sendToDelegate = false;
1889             boolean hit = true;
1890             boolean handled = false;
1891 
1892             switch (event.getAction()) {
1893                 case MotionEvent.ACTION_DOWN:
1894                     if (mTargetBounds.contains(x, y)) {
1895                         mDelegateTargeted = true;
1896                         sendToDelegate = true;
1897                     }
1898                     break;
1899                 case MotionEvent.ACTION_UP:
1900                 case MotionEvent.ACTION_MOVE:
1901                     sendToDelegate = mDelegateTargeted;
1902                     if (sendToDelegate) {
1903                         if (!mSlopBounds.contains(x, y)) {
1904                             hit = false;
1905                         }
1906                     }
1907                     break;
1908                 case MotionEvent.ACTION_CANCEL:
1909                     sendToDelegate = mDelegateTargeted;
1910                     mDelegateTargeted = false;
1911                     break;
1912             }
1913             if (sendToDelegate) {
1914                 if (hit && !mActualBounds.contains(x, y)) {
1915                     // Offset event coordinates to be in the center of the target view since we
1916                     // are within the targetBounds, but not inside the actual bounds of
1917                     // mDelegateView
1918                     event.setLocation(mDelegateView.getWidth() / 2,
1919                             mDelegateView.getHeight() / 2);
1920                 } else {
1921                     // Offset event coordinates to the target view coordinates.
1922                     event.setLocation(x - mActualBounds.left, y - mActualBounds.top);
1923                 }
1924 
1925                 handled = mDelegateView.dispatchTouchEvent(event);
1926             }
1927             return handled;
1928         }
1929     }
1930 
1931     /**
1932      * Local subclass for AutoCompleteTextView.
1933      * @hide
1934      */
1935     public static class SearchAutoComplete extends AutoCompleteTextView {
1936 
1937         private int mThreshold;
1938         private SearchView mSearchView;
1939 
1940         private boolean mHasPendingShowSoftInputRequest;
1941         final Runnable mRunShowSoftInputIfNecessary = () -> showSoftInputIfNecessary();
1942 
SearchAutoComplete(Context context)1943         public SearchAutoComplete(Context context) {
1944             super(context);
1945             mThreshold = getThreshold();
1946         }
1947 
1948         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
SearchAutoComplete(Context context, AttributeSet attrs)1949         public SearchAutoComplete(Context context, AttributeSet attrs) {
1950             super(context, attrs);
1951             mThreshold = getThreshold();
1952         }
1953 
SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs)1954         public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) {
1955             super(context, attrs, defStyleAttrs);
1956             mThreshold = getThreshold();
1957         }
1958 
SearchAutoComplete( Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes)1959         public SearchAutoComplete(
1960                 Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
1961             super(context, attrs, defStyleAttrs, defStyleRes);
1962             mThreshold = getThreshold();
1963         }
1964 
1965         @Override
onFinishInflate()1966         protected void onFinishInflate() {
1967             super.onFinishInflate();
1968             DisplayMetrics metrics = getResources().getDisplayMetrics();
1969             setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
1970                     getSearchViewTextMinWidthDp(), metrics));
1971         }
1972 
setSearchView(SearchView searchView)1973         void setSearchView(SearchView searchView) {
1974             mSearchView = searchView;
1975         }
1976 
1977         @Override
setThreshold(int threshold)1978         public void setThreshold(int threshold) {
1979             super.setThreshold(threshold);
1980             mThreshold = threshold;
1981         }
1982 
1983         /**
1984          * Returns true if the text field is empty, or contains only whitespace.
1985          */
isEmpty()1986         private boolean isEmpty() {
1987             return TextUtils.getTrimmedLength(getText()) == 0;
1988         }
1989 
1990         /**
1991          * We override this method to avoid replacing the query box text when a
1992          * suggestion is clicked.
1993          */
1994         @Override
replaceText(CharSequence text)1995         protected void replaceText(CharSequence text) {
1996         }
1997 
1998         /**
1999          * We override this method to avoid an extra onItemClick being called on
2000          * the drop-down's OnItemClickListener by
2001          * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
2002          * clicked with the trackball.
2003          */
2004         @Override
performCompletion()2005         public void performCompletion() {
2006         }
2007 
2008         /**
2009          * We override this method to be sure and show the soft keyboard if
2010          * appropriate when the TextView has focus.
2011          */
2012         @Override
onWindowFocusChanged(boolean hasWindowFocus)2013         public void onWindowFocusChanged(boolean hasWindowFocus) {
2014             super.onWindowFocusChanged(hasWindowFocus);
2015 
2016             if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
2017                 // Since InputMethodManager#onPostWindowFocus() will be called after this callback,
2018                 // it is a bit too early to call InputMethodManager#showSoftInput() here. We still
2019                 // need to wait until the system calls back onCreateInputConnection() to call
2020                 // InputMethodManager#showSoftInput().
2021                 mHasPendingShowSoftInputRequest = true;
2022 
2023                 // If in landscape mode, then make sure that the ime is in front of the dropdown.
2024                 if (isLandscapeMode(getContext())) {
2025                     ensureImeVisible(true);
2026                 }
2027             }
2028         }
2029 
2030         @Override
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)2031         protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
2032             super.onFocusChanged(focused, direction, previouslyFocusedRect);
2033             mSearchView.onTextFocusChanged();
2034         }
2035 
2036         /**
2037          * We override this method so that we can allow a threshold of zero,
2038          * which ACTV does not.
2039          */
2040         @Override
enoughToFilter()2041         public boolean enoughToFilter() {
2042             return mThreshold <= 0 || super.enoughToFilter();
2043         }
2044 
2045         @Override
onKeyPreIme(int keyCode, KeyEvent event)2046         public boolean onKeyPreIme(int keyCode, KeyEvent event) {
2047             final boolean consume = super.onKeyPreIme(keyCode, event);
2048             if (consume && keyCode == KeyEvent.KEYCODE_BACK
2049                     && event.getAction() == KeyEvent.ACTION_UP) {
2050                 // If AutoCompleteTextView closed its pop-up, it will return true, in which case
2051                 // we should also close the IME. Otherwise, the popup is already closed and we can
2052                 // leave the BACK event alone.
2053                 setImeVisibility(false);
2054             }
2055             return consume;
2056         }
2057 
2058         /**
2059          * Get minimum width of the search view text entry area.
2060          */
getSearchViewTextMinWidthDp()2061         private int getSearchViewTextMinWidthDp() {
2062             final Configuration configuration = getResources().getConfiguration();
2063             final int width = configuration.screenWidthDp;
2064             final int height = configuration.screenHeightDp;
2065             final int orientation = configuration.orientation;
2066             if (width >= 960 && height >= 720
2067                     && orientation == Configuration.ORIENTATION_LANDSCAPE) {
2068                 return 256;
2069             } else if (width >= 600 || (width >= 640 && height >= 480)) {
2070                 return 192;
2071             };
2072             return 160;
2073         }
2074 
2075         /**
2076          * We override {@link View#onCreateInputConnection(EditorInfo)} as a signal to schedule a
2077          * pending {@link InputMethodManager#showSoftInput(View, int)} request (if any).
2078          */
2079         @Override
onCreateInputConnection(EditorInfo editorInfo)2080         public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
2081             final InputConnection ic = super.onCreateInputConnection(editorInfo);
2082             if (mHasPendingShowSoftInputRequest) {
2083                 removeCallbacks(mRunShowSoftInputIfNecessary);
2084                 post(mRunShowSoftInputIfNecessary);
2085             }
2086             return ic;
2087         }
2088 
2089         @Override
checkInputConnectionProxy(View view)2090         public boolean checkInputConnectionProxy(View view) {
2091             return view == mSearchView;
2092         }
2093 
showSoftInputIfNecessary()2094         private void showSoftInputIfNecessary() {
2095             if (mHasPendingShowSoftInputRequest) {
2096                 final InputMethodManager imm =
2097                         getContext().getSystemService(InputMethodManager.class);
2098                 imm.showSoftInput(this, 0);
2099                 mHasPendingShowSoftInputRequest = false;
2100             }
2101         }
2102 
setImeVisibility(final boolean visible)2103         private void setImeVisibility(final boolean visible) {
2104             final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
2105             if (!visible) {
2106                 mHasPendingShowSoftInputRequest = false;
2107                 removeCallbacks(mRunShowSoftInputIfNecessary);
2108                 imm.hideSoftInputFromWindow(getWindowToken(), 0);
2109                 return;
2110             }
2111 
2112             if (imm.isActive(this)) {
2113                 // This means that SearchAutoComplete is already connected to the IME.
2114                 // InputMethodManager#showSoftInput() is guaranteed to pass client-side focus check.
2115                 mHasPendingShowSoftInputRequest = false;
2116                 removeCallbacks(mRunShowSoftInputIfNecessary);
2117                 imm.showSoftInput(this, 0);
2118                 return;
2119             }
2120 
2121             // Otherwise, InputMethodManager#showSoftInput() should be deferred after
2122             // onCreateInputConnection().
2123             mHasPendingShowSoftInputRequest = true;
2124         }
2125     }
2126 }
2127