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