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