SearchView.java revision 1b01ce23fa533d467ca2ccbc65c980a59662aa8d
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     * @attr ref android.R.styleable#SearchView_imeOptions
437     */
438    public void setImeOptions(int imeOptions) {
439        mQueryTextView.setImeOptions(imeOptions);
440    }
441
442    /**
443     * Returns the IME options set on the query text field.
444     * @return the ime options
445     * @see TextView#setImeOptions(int)
446     *
447     * @attr ref android.R.styleable#SearchView_imeOptions
448     */
449    public int getImeOptions() {
450        return mQueryTextView.getImeOptions();
451    }
452
453    /**
454     * Sets the input type on the query text field.
455     *
456     * @see TextView#setInputType(int)
457     * @param inputType the input type to set on the query text field
458     *
459     * @attr ref android.R.styleable#SearchView_inputType
460     */
461    public void setInputType(int inputType) {
462        mQueryTextView.setInputType(inputType);
463    }
464
465    /**
466     * Returns the input type set on the query text field.
467     * @return the input type
468     *
469     * @attr ref android.R.styleable#SearchView_inputType
470     */
471    public int getInputType() {
472        return mQueryTextView.getInputType();
473    }
474
475    /** @hide */
476    @Override
477    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
478        // Don't accept focus if in the middle of clearing focus
479        if (mClearingFocus) return false;
480        // Check if SearchView is focusable.
481        if (!isFocusable()) return false;
482        // If it is not iconified, then give the focus to the text field
483        if (!isIconified()) {
484            boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect);
485            if (result) {
486                updateViewsVisibility(false);
487            }
488            return result;
489        } else {
490            return super.requestFocus(direction, previouslyFocusedRect);
491        }
492    }
493
494    /** @hide */
495    @Override
496    public void clearFocus() {
497        mClearingFocus = true;
498        setImeVisibility(false);
499        super.clearFocus();
500        mQueryTextView.clearFocus();
501        mClearingFocus = false;
502    }
503
504    /**
505     * Sets a listener for user actions within the SearchView.
506     *
507     * @param listener the listener object that receives callbacks when the user performs
508     * actions in the SearchView such as clicking on buttons or typing a query.
509     */
510    public void setOnQueryTextListener(OnQueryTextListener listener) {
511        mOnQueryChangeListener = listener;
512    }
513
514    /**
515     * Sets a listener to inform when the user closes the SearchView.
516     *
517     * @param listener the listener to call when the user closes the SearchView.
518     */
519    public void setOnCloseListener(OnCloseListener listener) {
520        mOnCloseListener = listener;
521    }
522
523    /**
524     * Sets a listener to inform when the focus of the query text field changes.
525     *
526     * @param listener the listener to inform of focus changes.
527     */
528    public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
529        mOnQueryTextFocusChangeListener = listener;
530    }
531
532    /**
533     * Sets a listener to inform when a suggestion is focused or clicked.
534     *
535     * @param listener the listener to inform of suggestion selection events.
536     */
537    public void setOnSuggestionListener(OnSuggestionListener listener) {
538        mOnSuggestionListener = listener;
539    }
540
541    /**
542     * Sets a listener to inform when the search button is pressed. This is only
543     * relevant when the text field is not visible by default. Calling {@link #setIconified
544     * setIconified(false)} can also cause this listener to be informed.
545     *
546     * @param listener the listener to inform when the search button is clicked or
547     * the text field is programmatically de-iconified.
548     */
549    public void setOnSearchClickListener(OnClickListener listener) {
550        mOnSearchClickListener = listener;
551    }
552
553    /**
554     * Returns the query string currently in the text field.
555     *
556     * @return the query string
557     */
558    public CharSequence getQuery() {
559        return mQueryTextView.getText();
560    }
561
562    /**
563     * Sets a query string in the text field and optionally submits the query as well.
564     *
565     * @param query the query string. This replaces any query text already present in the
566     * text field.
567     * @param submit whether to submit the query right now or only update the contents of
568     * text field.
569     */
570    public void setQuery(CharSequence query, boolean submit) {
571        mQueryTextView.setText(query);
572        if (query != null) {
573            mQueryTextView.setSelection(mQueryTextView.length());
574            mUserQuery = query;
575        }
576
577        // If the query is not empty and submit is requested, submit the query
578        if (submit && !TextUtils.isEmpty(query)) {
579            onSubmitQuery();
580        }
581    }
582
583    /**
584     * Sets the hint text to display in the query text field. This overrides any hint specified
585     * in the SearchableInfo.
586     *
587     * @param hint the hint text to display
588     *
589     * @attr ref android.R.styleable#SearchView_queryHint
590     */
591    public void setQueryHint(CharSequence hint) {
592        mQueryHint = hint;
593        updateQueryHint();
594    }
595
596    /**
597     * Gets the hint text to display in the query text field.
598     * @return the query hint text, if specified, null otherwise.
599     *
600     * @attr ref android.R.styleable#SearchView_queryHint
601     */
602    public CharSequence getQueryHint() {
603        if (mQueryHint != null) {
604            return mQueryHint;
605        } else if (IS_AT_LEAST_FROYO && mSearchable != null) {
606            CharSequence hint = null;
607            int hintId = mSearchable.getHintId();
608            if (hintId != 0) {
609                hint = getContext().getString(hintId);
610            }
611            return hint;
612        }
613        return null;
614    }
615
616    /**
617     * Sets the default or resting state of the search field. If true, a single search icon is
618     * shown by default and expands to show the text field and other buttons when pressed. Also,
619     * if the default state is iconified, then it collapses to that state when the close button
620     * is pressed. Changes to this property will take effect immediately.
621     *
622     * <p>The default value is true.</p>
623     *
624     * @param iconified whether the search field should be iconified by default
625     *
626     * @attr ref android.R.styleable#SearchView_iconifiedByDefault
627     */
628    public void setIconifiedByDefault(boolean iconified) {
629        if (mIconifiedByDefault == iconified) return;
630        mIconifiedByDefault = iconified;
631        updateViewsVisibility(iconified);
632        updateQueryHint();
633    }
634
635    /**
636     * Returns the default iconified state of the search field.
637     * @return
638     *
639     * @attr ref android.R.styleable#SearchView_iconifiedByDefault
640     */
641    public boolean isIconfiedByDefault() {
642        return mIconifiedByDefault;
643    }
644
645    /**
646     * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
647     * a temporary state and does not override the default iconified state set by
648     * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
649     * a false here will only be valid until the user closes the field. And if the default
650     * state is expanded, then a true here will only clear the text field and not close it.
651     *
652     * @param iconify a true value will collapse the SearchView to an icon, while a false will
653     * expand it.
654     */
655    public void setIconified(boolean iconify) {
656        if (iconify) {
657            onCloseClicked();
658        } else {
659            onSearchClicked();
660        }
661    }
662
663    /**
664     * Returns the current iconified state of the SearchView.
665     *
666     * @return true if the SearchView is currently iconified, false if the search field is
667     * fully visible.
668     */
669    public boolean isIconified() {
670        return mIconified;
671    }
672
673    /**
674     * Enables showing a submit button when the query is non-empty. In cases where the SearchView
675     * is being used to filter the contents of the current activity and doesn't launch a separate
676     * results activity, then the submit button should be disabled.
677     *
678     * @param enabled true to show a submit button for submitting queries, false if a submit
679     * button is not required.
680     */
681    public void setSubmitButtonEnabled(boolean enabled) {
682        mSubmitButtonEnabled = enabled;
683        updateViewsVisibility(isIconified());
684    }
685
686    /**
687     * Returns whether the submit button is enabled when necessary or never displayed.
688     *
689     * @return whether the submit button is enabled automatically when necessary
690     */
691    public boolean isSubmitButtonEnabled() {
692        return mSubmitButtonEnabled;
693    }
694
695    /**
696     * Specifies if a query refinement button should be displayed alongside each suggestion
697     * or if it should depend on the flags set in the individual items retrieved from the
698     * suggestions provider. Clicking on the query refinement button will replace the text
699     * in the query text field with the text from the suggestion. This flag only takes effect
700     * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
701     * and not when using a custom adapter.
702     *
703     * @param enable true if all items should have a query refinement button, false if only
704     * those items that have a query refinement flag set should have the button.
705     *
706     * @see SearchManager#SUGGEST_COLUMN_FLAGS
707     * @see SearchManager#FLAG_QUERY_REFINEMENT
708     */
709    public void setQueryRefinementEnabled(boolean enable) {
710        mQueryRefinement = enable;
711        if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
712            ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
713                    enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
714        }
715    }
716
717    /**
718     * Returns whether query refinement is enabled for all items or only specific ones.
719     * @return true if enabled for all items, false otherwise.
720     */
721    public boolean isQueryRefinementEnabled() {
722        return mQueryRefinement;
723    }
724
725    /**
726     * You can set a custom adapter if you wish. Otherwise the default adapter is used to
727     * display the suggestions from the suggestions provider associated with the SearchableInfo.
728     *
729     * @see #setSearchableInfo(SearchableInfo)
730     */
731    public void setSuggestionsAdapter(CursorAdapter adapter) {
732        mSuggestionsAdapter = adapter;
733
734        mQueryTextView.setAdapter(mSuggestionsAdapter);
735    }
736
737    /**
738     * Returns the adapter used for suggestions, if any.
739     * @return the suggestions adapter
740     */
741    public CursorAdapter getSuggestionsAdapter() {
742        return mSuggestionsAdapter;
743    }
744
745    /**
746     * Makes the view at most this many pixels wide
747     *
748     * @attr ref android.R.styleable#SearchView_maxWidth
749     */
750    public void setMaxWidth(int maxpixels) {
751        mMaxWidth = maxpixels;
752
753        requestLayout();
754    }
755
756    /**
757     * Gets the specified maximum width in pixels, if set. Returns zero if
758     * no maximum width was specified.
759     * @return the maximum width of the view
760     *
761     * @attr ref android.R.styleable#SearchView_maxWidth
762     */
763    public int getMaxWidth() {
764        return mMaxWidth;
765    }
766
767    @Override
768    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
769        // Let the standard measurements take effect in iconified state.
770        if (isIconified()) {
771            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
772            return;
773        }
774
775        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
776        int width = MeasureSpec.getSize(widthMeasureSpec);
777
778        switch (widthMode) {
779            case MeasureSpec.AT_MOST:
780                // If there is an upper limit, don't exceed maximum width (explicit or implicit)
781                if (mMaxWidth > 0) {
782                    width = Math.min(mMaxWidth, width);
783                } else {
784                    width = Math.min(getPreferredWidth(), width);
785                }
786                break;
787            case MeasureSpec.EXACTLY:
788                // If an exact width is specified, still don't exceed any specified maximum width
789                if (mMaxWidth > 0) {
790                    width = Math.min(mMaxWidth, width);
791                }
792                break;
793            case MeasureSpec.UNSPECIFIED:
794                // Use maximum width, if specified, else preferred width
795                width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
796                break;
797        }
798        widthMode = MeasureSpec.EXACTLY;
799        super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), heightMeasureSpec);
800    }
801
802    private int getPreferredWidth() {
803        return getContext().getResources()
804                .getDimensionPixelSize(R.dimen.abc_search_view_preferred_width);
805    }
806
807    private void updateViewsVisibility(final boolean collapsed) {
808        mIconified = collapsed;
809        // Visibility of views that are visible when collapsed
810        final int visCollapsed = collapsed ? VISIBLE : GONE;
811        // Is there text in the query
812        final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
813
814        mSearchButton.setVisibility(visCollapsed);
815        updateSubmitButton(hasText);
816        mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
817        mSearchHintIcon.setVisibility(mIconifiedByDefault ? GONE : VISIBLE);
818        updateCloseButton();
819        updateVoiceButton(!hasText);
820        updateSubmitArea();
821    }
822
823    @TargetApi(Build.VERSION_CODES.FROYO)
824    private boolean hasVoiceSearch() {
825        if (mSearchable != null &&
826                mSearchable.getVoiceSearchEnabled()) {
827            Intent testIntent = null;
828            if (mSearchable.getVoiceSearchLaunchWebSearch()) {
829                testIntent = mVoiceWebSearchIntent;
830            } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
831                testIntent = mVoiceAppSearchIntent;
832            }
833            if (testIntent != null) {
834                ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
835                        PackageManager.MATCH_DEFAULT_ONLY);
836                return ri != null;
837            }
838        }
839        return false;
840    }
841
842    private boolean isSubmitAreaEnabled() {
843        return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
844    }
845
846    private void updateSubmitButton(boolean hasText) {
847        int visibility = GONE;
848        if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
849                && (hasText || !mVoiceButtonEnabled)) {
850            visibility = VISIBLE;
851        }
852        mSubmitButton.setVisibility(visibility);
853    }
854
855    private void updateSubmitArea() {
856        int visibility = GONE;
857        if (isSubmitAreaEnabled()
858                && (mSubmitButton.getVisibility() == VISIBLE
859                || mVoiceButton.getVisibility() == VISIBLE)) {
860            visibility = VISIBLE;
861        }
862        mSubmitArea.setVisibility(visibility);
863    }
864
865    private void updateCloseButton() {
866        final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
867        // Should we show the close button? It is not shown if there's no focus,
868        // field is not iconified by default and there is no text in it.
869        final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
870        mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
871        mCloseButton.getDrawable().setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
872    }
873
874    private void postUpdateFocusedState() {
875        post(mUpdateDrawableStateRunnable);
876    }
877
878    private void updateFocusedState() {
879        boolean focused = mQueryTextView.hasFocus();
880        mSearchPlate.getBackground().setState(focused ? ENABLED_FOCUSED_STATE_SET : EMPTY_STATE_SET);
881        mSubmitArea.getBackground().setState(focused ? ENABLED_FOCUSED_STATE_SET : EMPTY_STATE_SET);
882        invalidate();
883    }
884
885    @Override
886    protected void onDetachedFromWindow() {
887        removeCallbacks(mUpdateDrawableStateRunnable);
888        post(mReleaseCursorRunnable);
889        super.onDetachedFromWindow();
890    }
891
892    private void setImeVisibility(final boolean visible) {
893        if (visible) {
894            post(mShowImeRunnable);
895        } else {
896            removeCallbacks(mShowImeRunnable);
897            InputMethodManager imm = (InputMethodManager)
898                    getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
899
900            if (imm != null) {
901                imm.hideSoftInputFromWindow(getWindowToken(), 0);
902            }
903        }
904    }
905
906    /**
907     * Called by the SuggestionsAdapter
908     * @hide
909     */
910    /* package */void onQueryRefine(CharSequence queryText) {
911        setQuery(queryText);
912    }
913
914    private final OnClickListener mOnClickListener = new OnClickListener() {
915
916        public void onClick(View v) {
917            if (v == mSearchButton) {
918                onSearchClicked();
919            } else if (v == mCloseButton) {
920                onCloseClicked();
921            } else if (v == mSubmitButton) {
922                onSubmitQuery();
923            } else if (v == mVoiceButton) {
924                if (IS_AT_LEAST_FROYO) {
925                    onVoiceClicked();
926                }
927            } else if (v == mQueryTextView) {
928                forceSuggestionQuery();
929            }
930        }
931    };
932
933    /**
934     * React to the user typing "enter" or other hardwired keys while typing in
935     * the search box. This handles these special keys while the edit box has
936     * focus.
937     */
938    View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
939        public boolean onKey(View v, int keyCode, KeyEvent event) {
940            // guard against possible race conditions
941            if (mSearchable == null) {
942                return false;
943            }
944
945            if (DBG) {
946                Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
947                        + mQueryTextView.getListSelection());
948            }
949
950            // If a suggestion is selected, handle enter, search key, and action keys
951            // as presses on the selected suggestion
952            if (mQueryTextView.isPopupShowing()
953                    && mQueryTextView.getListSelection() != ListView.INVALID_POSITION) {
954                return onSuggestionsKey(v, keyCode, event);
955            }
956
957            // If there is text in the query box, handle enter, and action keys
958            // The search key is handled by the dialog's onKeyDown().
959            if (!mQueryTextView.isEmpty() && KeyEventCompat.hasNoModifiers(event)) {
960                if (event.getAction() == KeyEvent.ACTION_UP) {
961                    if (keyCode == KeyEvent.KEYCODE_ENTER) {
962                        v.cancelLongPress();
963
964                        // Launch as a regular search.
965                        launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
966                                .toString());
967                        return true;
968                    }
969                }
970            }
971            return false;
972        }
973    };
974
975    /**
976     * React to the user typing while in the suggestions list. First, check for
977     * action keys. If not handled, try refocusing regular characters into the
978     * EditText.
979     */
980    private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
981        // guard against possible race conditions (late arrival after dismiss)
982        if (mSearchable == null) {
983            return false;
984        }
985        if (mSuggestionsAdapter == null) {
986            return false;
987        }
988        if (event.getAction() == KeyEvent.ACTION_DOWN && KeyEventCompat.hasNoModifiers(event)) {
989            // First, check for enter or search (both of which we'll treat as a
990            // "click")
991            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
992                    || keyCode == KeyEvent.KEYCODE_TAB) {
993                int position = mQueryTextView.getListSelection();
994                return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
995            }
996
997            // Next, check for left/right moves, which we use to "return" the
998            // user to the edit view
999            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
1000                // give "focus" to text editor, with cursor at the beginning if
1001                // left key, at end if right key
1002                // TODO: Reverse left/right for right-to-left languages, e.g.
1003                // Arabic
1004                int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView
1005                        .length();
1006                mQueryTextView.setSelection(selPoint);
1007                mQueryTextView.setListSelection(0);
1008                mQueryTextView.clearListSelection();
1009                HIDDEN_METHOD_INVOKER.ensureImeVisible(mQueryTextView, true);
1010
1011                return true;
1012            }
1013
1014            // Next, check for an "up and out" move
1015            if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mQueryTextView.getListSelection()) {
1016                // TODO: restoreUserQuery();
1017                // let ACTV complete the move
1018                return false;
1019            }
1020        }
1021        return false;
1022    }
1023
1024    private CharSequence getDecoratedHint(CharSequence hintText) {
1025        // If the field is always expanded, then don't add the search icon to the hint
1026        if (!mIconifiedByDefault) {
1027            return hintText;
1028        }
1029
1030        final Drawable searchIcon = getResources().getDrawable(mSearchIconResId);
1031        final int textSize = (int) (mQueryTextView.getTextSize() * 1.25);
1032        searchIcon.setBounds(0, 0, textSize, textSize);
1033
1034        final SpannableStringBuilder ssb = new SpannableStringBuilder("   "); // for the icon
1035        ssb.append(hintText);
1036        ssb.setSpan(new ImageSpan(searchIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1037        return ssb;
1038    }
1039
1040    private void updateQueryHint() {
1041        if (mQueryHint != null) {
1042            mQueryTextView.setHint(getDecoratedHint(mQueryHint));
1043        } else if (IS_AT_LEAST_FROYO && mSearchable != null) {
1044            CharSequence hint = null;
1045            int hintId = mSearchable.getHintId();
1046            if (hintId != 0) {
1047                hint = getContext().getString(hintId);
1048            }
1049            if (hint != null) {
1050                mQueryTextView.setHint(getDecoratedHint(hint));
1051            }
1052        } else {
1053            mQueryTextView.setHint(getDecoratedHint(""));
1054        }
1055    }
1056
1057    /**
1058     * Updates the auto-complete text view.
1059     */
1060    @TargetApi(Build.VERSION_CODES.FROYO)
1061    private void updateSearchAutoComplete() {
1062        mQueryTextView.setThreshold(mSearchable.getSuggestThreshold());
1063        mQueryTextView.setImeOptions(mSearchable.getImeOptions());
1064        int inputType = mSearchable.getInputType();
1065        // We only touch this if the input type is set up for text (which it almost certainly
1066        // should be, in the case of search!)
1067        if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
1068            // The existence of a suggestions authority is the proxy for "suggestions
1069            // are available here"
1070            inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1071            if (mSearchable.getSuggestAuthority() != null) {
1072                inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1073                // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
1074                // auto-completion based on its own semantics, which it will present to the user
1075                // as they type. This generally means that the input method should not show its
1076                // own candidates, and the spell checker should not be in action. The text editor
1077                // supplies its candidates by calling InputMethodManager.displayCompletions(),
1078                // which in turn will call InputMethodSession.displayCompletions().
1079                inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
1080            }
1081        }
1082        mQueryTextView.setInputType(inputType);
1083        if (mSuggestionsAdapter != null) {
1084            mSuggestionsAdapter.changeCursor(null);
1085        }
1086        // attach the suggestions adapter, if suggestions are available
1087        // The existence of a suggestions authority is the proxy for "suggestions available here"
1088        if (mSearchable.getSuggestAuthority() != null) {
1089            mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
1090                    this, mSearchable, mOutsideDrawablesCache);
1091            mQueryTextView.setAdapter(mSuggestionsAdapter);
1092            ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
1093                    mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
1094                            : SuggestionsAdapter.REFINE_BY_ENTRY);
1095        }
1096    }
1097
1098    /**
1099     * Update the visibility of the voice button.  There are actually two voice search modes,
1100     * either of which will activate the button.
1101     * @param empty whether the search query text field is empty. If it is, then the other
1102     * criteria apply to make the voice button visible.
1103     */
1104    private void updateVoiceButton(boolean empty) {
1105        int visibility = GONE;
1106        if (mVoiceButtonEnabled && !isIconified() && empty) {
1107            visibility = VISIBLE;
1108            mSubmitButton.setVisibility(GONE);
1109        }
1110        mVoiceButton.setVisibility(visibility);
1111    }
1112
1113    private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
1114
1115        /**
1116         * Called when the input method default action key is pressed.
1117         */
1118        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
1119            onSubmitQuery();
1120            return true;
1121        }
1122    };
1123
1124    private void onTextChanged(CharSequence newText) {
1125        CharSequence text = mQueryTextView.getText();
1126        mUserQuery = text;
1127        boolean hasText = !TextUtils.isEmpty(text);
1128        updateSubmitButton(hasText);
1129        updateVoiceButton(!hasText);
1130        updateCloseButton();
1131        updateSubmitArea();
1132        if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
1133            mOnQueryChangeListener.onQueryTextChange(newText.toString());
1134        }
1135        mOldQueryText = newText.toString();
1136    }
1137
1138    private void onSubmitQuery() {
1139        CharSequence query = mQueryTextView.getText();
1140        if (query != null && TextUtils.getTrimmedLength(query) > 0) {
1141            if (mOnQueryChangeListener == null
1142                    || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
1143                if (mSearchable != null) {
1144                    launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
1145                }
1146                setImeVisibility(false);
1147                dismissSuggestions();
1148            }
1149        }
1150    }
1151
1152    private void dismissSuggestions() {
1153        mQueryTextView.dismissDropDown();
1154    }
1155
1156    private void onCloseClicked() {
1157        CharSequence text = mQueryTextView.getText();
1158        if (TextUtils.isEmpty(text)) {
1159            if (mIconifiedByDefault) {
1160                // If the app doesn't override the close behavior
1161                if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
1162                    // hide the keyboard and remove focus
1163                    clearFocus();
1164                    // collapse the search field
1165                    updateViewsVisibility(true);
1166                }
1167            }
1168        } else {
1169            mQueryTextView.setText("");
1170            mQueryTextView.requestFocus();
1171            setImeVisibility(true);
1172        }
1173
1174    }
1175
1176    private void onSearchClicked() {
1177        updateViewsVisibility(false);
1178        mQueryTextView.requestFocus();
1179        setImeVisibility(true);
1180        if (mOnSearchClickListener != null) {
1181            mOnSearchClickListener.onClick(this);
1182        }
1183    }
1184
1185    @TargetApi(Build.VERSION_CODES.FROYO)
1186    private void onVoiceClicked() {
1187        // guard against possible race conditions
1188        if (mSearchable == null) {
1189            return;
1190        }
1191        SearchableInfo searchable = mSearchable;
1192        try {
1193            if (searchable.getVoiceSearchLaunchWebSearch()) {
1194                Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
1195                        searchable);
1196                getContext().startActivity(webSearchIntent);
1197            } else if (searchable.getVoiceSearchLaunchRecognizer()) {
1198                Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
1199                        searchable);
1200                getContext().startActivity(appSearchIntent);
1201            }
1202        } catch (ActivityNotFoundException e) {
1203            // Should not happen, since we check the availability of
1204            // voice search before showing the button. But just in case...
1205            Log.w(LOG_TAG, "Could not find voice search activity");
1206        }
1207    }
1208
1209    void onTextFocusChanged() {
1210        updateViewsVisibility(isIconified());
1211        // Delayed update to make sure that the focus has settled down and window focus changes
1212        // don't affect it. A synchronous update was not working.
1213        postUpdateFocusedState();
1214        if (mQueryTextView.hasFocus()) {
1215            forceSuggestionQuery();
1216        }
1217    }
1218
1219    @Override
1220    public void onWindowFocusChanged(boolean hasWindowFocus) {
1221        super.onWindowFocusChanged(hasWindowFocus);
1222
1223        postUpdateFocusedState();
1224    }
1225
1226    /**
1227     * {@inheritDoc}
1228     */
1229    @Override
1230    public void onActionViewCollapsed() {
1231        setQuery("", false);
1232        clearFocus();
1233        updateViewsVisibility(true);
1234        mQueryTextView.setImeOptions(mCollapsedImeOptions);
1235        mExpandedInActionView = false;
1236    }
1237
1238    /**
1239     * {@inheritDoc}
1240     */
1241    @Override
1242    public void onActionViewExpanded() {
1243        if (mExpandedInActionView) return;
1244
1245        mExpandedInActionView = true;
1246        mCollapsedImeOptions = mQueryTextView.getImeOptions();
1247        mQueryTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
1248        mQueryTextView.setText("");
1249        setIconified(false);
1250    }
1251
1252
1253    private void adjustDropDownSizeAndPosition() {
1254        if (mDropDownAnchor.getWidth() > 1) {
1255            Resources res = getContext().getResources();
1256            int anchorPadding = mSearchPlate.getPaddingLeft();
1257            Rect dropDownPadding = new Rect();
1258            final boolean isLayoutRtl = ViewUtils.isLayoutRtl(this);
1259            int iconOffset = mIconifiedByDefault
1260                    ? res.getDimensionPixelSize(R.dimen.abc_dropdownitem_icon_width)
1261                    + res.getDimensionPixelSize(R.dimen.abc_dropdownitem_text_padding_left)
1262                    : 0;
1263            mQueryTextView.getDropDownBackground().getPadding(dropDownPadding);
1264            int offset;
1265            if (isLayoutRtl) {
1266                offset = - dropDownPadding.left;
1267            } else {
1268                offset = anchorPadding - (dropDownPadding.left + iconOffset);
1269            }
1270            mQueryTextView.setDropDownHorizontalOffset(offset);
1271            final int width = mDropDownAnchor.getWidth() + dropDownPadding.left
1272                    + dropDownPadding.right + iconOffset - anchorPadding;
1273            mQueryTextView.setDropDownWidth(width);
1274        }
1275    }
1276
1277    private boolean onItemClicked(int position, int actionKey, String actionMsg) {
1278        if (mOnSuggestionListener == null
1279                || !mOnSuggestionListener.onSuggestionClick(position)) {
1280            launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1281            setImeVisibility(false);
1282            dismissSuggestions();
1283            return true;
1284        }
1285        return false;
1286    }
1287
1288    private boolean onItemSelected(int position) {
1289        if (mOnSuggestionListener == null
1290                || !mOnSuggestionListener.onSuggestionSelect(position)) {
1291            rewriteQueryFromSuggestion(position);
1292            return true;
1293        }
1294        return false;
1295    }
1296
1297    private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
1298
1299        /**
1300         * Implements OnItemClickListener
1301         */
1302        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1303            if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1304            onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
1305        }
1306    };
1307
1308    private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
1309
1310        /**
1311         * Implements OnItemSelectedListener
1312         */
1313        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
1314            if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1315            SearchView.this.onItemSelected(position);
1316        }
1317
1318        /**
1319         * Implements OnItemSelectedListener
1320         */
1321        public void onNothingSelected(AdapterView<?> parent) {
1322            if (DBG)
1323                Log.d(LOG_TAG, "onNothingSelected()");
1324        }
1325    };
1326
1327    /**
1328     * Query rewriting.
1329     */
1330    private void rewriteQueryFromSuggestion(int position) {
1331        CharSequence oldQuery = mQueryTextView.getText();
1332        Cursor c = mSuggestionsAdapter.getCursor();
1333        if (c == null) {
1334            return;
1335        }
1336        if (c.moveToPosition(position)) {
1337            // Get the new query from the suggestion.
1338            CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1339            if (newQuery != null) {
1340                // The suggestion rewrites the query.
1341                // Update the text field, without getting new suggestions.
1342                setQuery(newQuery);
1343            } else {
1344                // The suggestion does not rewrite the query, restore the user's query.
1345                setQuery(oldQuery);
1346            }
1347        } else {
1348            // We got a bad position, restore the user's query.
1349            setQuery(oldQuery);
1350        }
1351    }
1352
1353    /**
1354     * Launches an intent based on a suggestion.
1355     *
1356     * @param position The index of the suggestion to create the intent from.
1357     * @param actionKey The key code of the action key that was pressed,
1358     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1359     * @param actionMsg The message for the action key that was pressed,
1360     *        or <code>null</code> if none.
1361     * @return true if a successful launch, false if could not (e.g. bad position).
1362     */
1363    private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1364        Cursor c = mSuggestionsAdapter.getCursor();
1365        if ((c != null) && c.moveToPosition(position)) {
1366
1367            Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1368
1369            // launch the intent
1370            launchIntent(intent);
1371
1372            return true;
1373        }
1374        return false;
1375    }
1376
1377    /**
1378     * Launches an intent, including any special intent handling.
1379     */
1380    private void launchIntent(Intent intent) {
1381        if (intent == null) {
1382            return;
1383        }
1384        try {
1385            // If the intent was created from a suggestion, it will always have an explicit
1386            // component here.
1387            getContext().startActivity(intent);
1388        } catch (RuntimeException ex) {
1389            Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1390        }
1391    }
1392
1393    /**
1394     * Sets the text in the query box, without updating the suggestions.
1395     */
1396    private void setQuery(CharSequence query) {
1397        mQueryTextView.setText(query);
1398        // Move the cursor to the end
1399        mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
1400    }
1401
1402    private void launchQuerySearch(int actionKey, String actionMsg, String query) {
1403        String action = Intent.ACTION_SEARCH;
1404        Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
1405        getContext().startActivity(intent);
1406    }
1407
1408    /**
1409     * Constructs an intent from the given information and the search dialog state.
1410     *
1411     * @param action Intent action.
1412     * @param data Intent data, or <code>null</code>.
1413     * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1414     * @param query Intent query, or <code>null</code>.
1415     * @param actionKey The key code of the action key that was pressed,
1416     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1417     * @param actionMsg The message for the action key that was pressed,
1418     *        or <code>null</code> if none.
1419     * @return The intent.
1420     */
1421    private Intent createIntent(String action, Uri data, String extraData, String query,
1422            int actionKey, String actionMsg) {
1423        // Now build the Intent
1424        Intent intent = new Intent(action);
1425        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1426        // We need CLEAR_TOP to avoid reusing an old task that has other activities
1427        // on top of the one we want. We don't want to do this in in-app search though,
1428        // as it can be destructive to the activity stack.
1429        if (data != null) {
1430            intent.setData(data);
1431        }
1432        intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
1433        if (query != null) {
1434            intent.putExtra(SearchManager.QUERY, query);
1435        }
1436        if (extraData != null) {
1437            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1438        }
1439        if (mAppSearchData != null) {
1440            intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1441        }
1442        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1443            intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1444            intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1445        }
1446        if (IS_AT_LEAST_FROYO) {
1447            intent.setComponent(mSearchable.getSearchActivity());
1448        }
1449        return intent;
1450    }
1451
1452    /**
1453     * Create and return an Intent that can launch the voice search activity for web search.
1454     */
1455    @TargetApi(Build.VERSION_CODES.FROYO)
1456    private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1457        Intent voiceIntent = new Intent(baseIntent);
1458        ComponentName searchActivity = searchable.getSearchActivity();
1459        voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1460                : searchActivity.flattenToShortString());
1461        return voiceIntent;
1462    }
1463
1464    /**
1465     * Create and return an Intent that can launch the voice search activity, perform a specific
1466     * voice transcription, and forward the results to the searchable activity.
1467     *
1468     * @param baseIntent The voice app search intent to start from
1469     * @return A completely-configured intent ready to send to the voice search activity
1470     */
1471    @TargetApi(Build.VERSION_CODES.FROYO)
1472    private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1473        ComponentName searchActivity = searchable.getSearchActivity();
1474
1475        // create the necessary intent to set up a search-and-forward operation
1476        // in the voice search system.   We have to keep the bundle separate,
1477        // because it becomes immutable once it enters the PendingIntent
1478        Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
1479        queryIntent.setComponent(searchActivity);
1480        PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
1481                PendingIntent.FLAG_ONE_SHOT);
1482
1483        // Now set up the bundle that will be inserted into the pending intent
1484        // when it's time to do the search.  We always build it here (even if empty)
1485        // because the voice search activity will always need to insert "QUERY" into
1486        // it anyway.
1487        Bundle queryExtras = new Bundle();
1488        if (mAppSearchData != null) {
1489            queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
1490        }
1491
1492        // Now build the intent to launch the voice search.  Add all necessary
1493        // extras to launch the voice recognizer, and then all the necessary extras
1494        // to forward the results to the searchable activity
1495        Intent voiceIntent = new Intent(baseIntent);
1496
1497        // Add all of the configuration options supplied by the searchable's metadata
1498        String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
1499        String prompt = null;
1500        String language = null;
1501        int maxResults = 1;
1502
1503        if (Build.VERSION.SDK_INT >= 8) {
1504            Resources resources = getResources();
1505            if (searchable.getVoiceLanguageModeId() != 0) {
1506                languageModel = resources.getString(searchable.getVoiceLanguageModeId());
1507            }
1508            if (searchable.getVoicePromptTextId() != 0) {
1509                prompt = resources.getString(searchable.getVoicePromptTextId());
1510            }
1511            if (searchable.getVoiceLanguageId() != 0) {
1512                language = resources.getString(searchable.getVoiceLanguageId());
1513            }
1514            if (searchable.getVoiceMaxResults() != 0) {
1515                maxResults = searchable.getVoiceMaxResults();
1516            }
1517        }
1518        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
1519        voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
1520        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
1521        voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
1522        voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1523                : searchActivity.flattenToShortString());
1524
1525        // Add the values that configure forwarding the results
1526        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
1527        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
1528
1529        return voiceIntent;
1530    }
1531
1532    /**
1533     * When a particular suggestion has been selected, perform the various lookups required
1534     * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
1535     * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
1536     * the suggestion includes a data id.
1537     *
1538     * @param c The suggestions cursor, moved to the row of the user's selection
1539     * @param actionKey The key code of the action key that was pressed,
1540     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1541     * @param actionMsg The message for the action key that was pressed,
1542     *        or <code>null</code> if none.
1543     * @return An intent for the suggestion at the cursor's position.
1544     */
1545    private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1546        try {
1547            // use specific action if supplied, or default action if supplied, or fixed default
1548            String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1549
1550            if (action == null && Build.VERSION.SDK_INT >= 8) {
1551                action = mSearchable.getSuggestIntentAction();
1552            }
1553            if (action == null) {
1554                action = Intent.ACTION_SEARCH;
1555            }
1556
1557            // use specific data if supplied, or default data if supplied
1558            String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1559            if (IS_AT_LEAST_FROYO && data == null) {
1560                data = mSearchable.getSuggestIntentData();
1561            }
1562            // then, if an ID was provided, append it.
1563            if (data != null) {
1564                String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1565                if (id != null) {
1566                    data = data + "/" + Uri.encode(id);
1567                }
1568            }
1569            Uri dataUri = (data == null) ? null : Uri.parse(data);
1570
1571            String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1572            String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1573
1574            return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
1575        } catch (RuntimeException e ) {
1576            int rowNum;
1577            try {                       // be really paranoid now
1578                rowNum = c.getPosition();
1579            } catch (RuntimeException e2 ) {
1580                rowNum = -1;
1581            }
1582            Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
1583                    " returned exception.", e);
1584            return null;
1585        }
1586    }
1587
1588    private void forceSuggestionQuery() {
1589        HIDDEN_METHOD_INVOKER.doBeforeTextChanged(mQueryTextView);
1590        HIDDEN_METHOD_INVOKER.doAfterTextChanged(mQueryTextView);
1591    }
1592
1593    static boolean isLandscapeMode(Context context) {
1594        return context.getResources().getConfiguration().orientation
1595                == Configuration.ORIENTATION_LANDSCAPE;
1596    }
1597
1598    /**
1599     * Callback to watch the text field for empty/non-empty
1600     */
1601    private TextWatcher mTextWatcher = new TextWatcher() {
1602
1603        public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
1604
1605        public void onTextChanged(CharSequence s, int start,
1606                int before, int after) {
1607            SearchView.this.onTextChanged(s);
1608        }
1609
1610        public void afterTextChanged(Editable s) {
1611        }
1612    };
1613
1614    /**
1615     * Local subclass for AutoCompleteTextView.
1616     * @hide
1617     */
1618    public static class SearchAutoComplete extends AutoCompleteTextView {
1619
1620        private int mThreshold;
1621        private SearchView mSearchView;
1622
1623        public SearchAutoComplete(Context context) {
1624            super(context);
1625            mThreshold = getThreshold();
1626        }
1627
1628        public SearchAutoComplete(Context context, AttributeSet attrs) {
1629            super(context, attrs);
1630            mThreshold = getThreshold();
1631        }
1632
1633        public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1634            super(context, attrs, defStyle);
1635            mThreshold = getThreshold();
1636        }
1637
1638        void setSearchView(SearchView searchView) {
1639            mSearchView = searchView;
1640        }
1641
1642        @Override
1643        public void setThreshold(int threshold) {
1644            super.setThreshold(threshold);
1645            mThreshold = threshold;
1646        }
1647
1648        /**
1649         * Returns true if the text field is empty, or contains only whitespace.
1650         */
1651        private boolean isEmpty() {
1652            return TextUtils.getTrimmedLength(getText()) == 0;
1653        }
1654
1655        /**
1656         * We override this method to avoid replacing the query box text when a
1657         * suggestion is clicked.
1658         */
1659        @Override
1660        protected void replaceText(CharSequence text) {
1661        }
1662
1663        /**
1664         * We override this method to avoid an extra onItemClick being called on
1665         * the drop-down's OnItemClickListener by
1666         * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
1667         * clicked with the trackball.
1668         */
1669        @Override
1670        public void performCompletion() {
1671        }
1672
1673        /**
1674         * We override this method to be sure and show the soft keyboard if
1675         * appropriate when the TextView has focus.
1676         */
1677        @Override
1678        public void onWindowFocusChanged(boolean hasWindowFocus) {
1679            super.onWindowFocusChanged(hasWindowFocus);
1680
1681            if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
1682                InputMethodManager inputManager = (InputMethodManager) getContext()
1683                        .getSystemService(Context.INPUT_METHOD_SERVICE);
1684                inputManager.showSoftInput(this, 0);
1685                // If in landscape mode, then make sure that
1686                // the ime is in front of the dropdown.
1687                if (isLandscapeMode(getContext())) {
1688                    HIDDEN_METHOD_INVOKER.ensureImeVisible(this, true);
1689                }
1690            }
1691        }
1692
1693        @Override
1694        protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1695            super.onFocusChanged(focused, direction, previouslyFocusedRect);
1696            mSearchView.onTextFocusChanged();
1697        }
1698
1699        /**
1700         * We override this method so that we can allow a threshold of zero,
1701         * which ACTV does not.
1702         */
1703        @Override
1704        public boolean enoughToFilter() {
1705            return mThreshold <= 0 || super.enoughToFilter();
1706        }
1707
1708        @Override
1709        public boolean onKeyPreIme(int keyCode, KeyEvent event) {
1710            if (keyCode == KeyEvent.KEYCODE_BACK) {
1711                // special case for the back key, we do not even try to send it
1712                // to the drop down list but instead, consume it immediately
1713                if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
1714                    KeyEvent.DispatcherState state = getKeyDispatcherState();
1715                    if (state != null) {
1716                        state.startTracking(event, this);
1717                    }
1718                    return true;
1719                } else if (event.getAction() == KeyEvent.ACTION_UP) {
1720                    KeyEvent.DispatcherState state = getKeyDispatcherState();
1721                    if (state != null) {
1722                        state.handleUpEvent(event);
1723                    }
1724                    if (event.isTracking() && !event.isCanceled()) {
1725                        mSearchView.clearFocus();
1726                        mSearchView.setImeVisibility(false);
1727                        return true;
1728                    }
1729                }
1730            }
1731            return super.onKeyPreIme(keyCode, event);
1732        }
1733    }
1734
1735    private static class AutoCompleteTextViewReflector {
1736        private Method doBeforeTextChanged, doAfterTextChanged;
1737        private Method ensureImeVisible;
1738        private Method showSoftInputUnchecked;
1739
1740        AutoCompleteTextViewReflector() {
1741            try {
1742                doBeforeTextChanged = AutoCompleteTextView.class
1743                        .getDeclaredMethod("doBeforeTextChanged");
1744                doBeforeTextChanged.setAccessible(true);
1745            } catch (NoSuchMethodException e) {
1746                // Ah well.
1747            }
1748            try {
1749                doAfterTextChanged = AutoCompleteTextView.class
1750                        .getDeclaredMethod("doAfterTextChanged");
1751                doAfterTextChanged.setAccessible(true);
1752            } catch (NoSuchMethodException e) {
1753                // Ah well.
1754            }
1755            try {
1756                ensureImeVisible = AutoCompleteTextView.class
1757                        .getMethod("ensureImeVisible", boolean.class);
1758                ensureImeVisible.setAccessible(true);
1759            } catch (NoSuchMethodException e) {
1760                // Ah well.
1761            }
1762            try {
1763                showSoftInputUnchecked = InputMethodManager.class.getMethod(
1764                        "showSoftInputUnchecked", int.class, ResultReceiver.class);
1765                showSoftInputUnchecked.setAccessible(true);
1766            } catch (NoSuchMethodException e) {
1767                // Ah well.
1768            }
1769        }
1770
1771        void doBeforeTextChanged(AutoCompleteTextView view) {
1772            if (doBeforeTextChanged != null) {
1773                try {
1774                    doBeforeTextChanged.invoke(view);
1775                } catch (Exception e) {
1776                }
1777            }
1778        }
1779
1780        void doAfterTextChanged(AutoCompleteTextView view) {
1781            if (doAfterTextChanged != null) {
1782                try {
1783                    doAfterTextChanged.invoke(view);
1784                } catch (Exception e) {
1785                }
1786            }
1787        }
1788
1789        void ensureImeVisible(AutoCompleteTextView view, boolean visible) {
1790            if (ensureImeVisible != null) {
1791                try {
1792                    ensureImeVisible.invoke(view, visible);
1793                } catch (Exception e) {
1794                }
1795            }
1796        }
1797
1798        void showSoftInputUnchecked(InputMethodManager imm, View view, int flags) {
1799            if (showSoftInputUnchecked != null) {
1800                try {
1801                    showSoftInputUnchecked.invoke(imm, flags, null);
1802                    return;
1803                } catch (Exception e) {
1804                }
1805            }
1806
1807            // Hidden method failed, call public version instead
1808            imm.showSoftInput(view, flags);
1809        }
1810    }
1811}