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