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