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