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