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