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