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