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