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