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