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