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