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