SearchView.java revision 5931b1f415fdb30f429fb39238c63f7533335998
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 com.android.internal.R;
22
23import android.app.PendingIntent;
24import android.app.SearchManager;
25import android.app.SearchableInfo;
26import android.content.ActivityNotFoundException;
27import android.content.ComponentName;
28import android.content.Context;
29import android.content.Intent;
30import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
32import android.content.res.Resources;
33import android.content.res.TypedArray;
34import android.database.Cursor;
35import android.graphics.Rect;
36import android.graphics.drawable.Drawable;
37import android.net.Uri;
38import android.os.Bundle;
39import android.speech.RecognizerIntent;
40import android.text.Editable;
41import android.text.TextUtils;
42import android.text.TextWatcher;
43import android.util.AttributeSet;
44import android.util.Log;
45import android.view.KeyEvent;
46import android.view.LayoutInflater;
47import android.view.View;
48import android.view.inputmethod.InputMethodManager;
49import android.widget.AdapterView.OnItemClickListener;
50import android.widget.AdapterView.OnItemSelectedListener;
51import android.widget.TextView.OnEditorActionListener;
52
53import java.util.WeakHashMap;
54
55/**
56 * Provides the user interface elements for the user to enter a search query and submit a
57 * request to a search provider. Shows a list of query suggestions or results, if
58 * available and allows the user to pick a suggestion or result to launch into.
59 *
60 * <p>
61 * <b>XML attributes</b>
62 * <p>
63 * See {@link android.R.styleable#SearchView SearchView Attributes},
64 * {@link android.R.styleable#View View Attributes}
65 *
66 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
67 * @attr ref android.R.styleable#SearchView_maxWidth
68 */
69public class SearchView extends LinearLayout {
70
71    private static final boolean DBG = false;
72    private static final String LOG_TAG = "SearchView";
73
74    private OnQueryChangeListener mOnQueryChangeListener;
75    private OnCloseListener mOnCloseListener;
76    private OnFocusChangeListener mOnQueryTextFocusChangeListener;
77    private OnSuggestionSelectionListener mOnSuggestionListener;
78
79    private boolean mIconifiedByDefault;
80    private boolean mIconified;
81    private CursorAdapter mSuggestionsAdapter;
82    private View mSearchButton;
83    private View mSubmitButton;
84    private View mCloseButton;
85    private View mSearchEditFrame;
86    private View mVoiceButton;
87    private AutoCompleteTextView mQueryTextView;
88    private boolean mSubmitButtonEnabled;
89    private CharSequence mQueryHint;
90    private boolean mQueryRefinement;
91    private boolean mClearingFocus;
92    private int mMaxWidth;
93
94    private SearchableInfo mSearchable;
95
96    // For voice searching
97    private final Intent mVoiceWebSearchIntent;
98    private final Intent mVoiceAppSearchIntent;
99
100    // A weak map of drawables we've gotten from other packages, so we don't load them
101    // more than once.
102    private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
103            new WeakHashMap<String, Drawable.ConstantState>();
104
105    /**
106     * Callbacks for changes to the query text.
107     */
108    public interface OnQueryChangeListener {
109
110        /**
111         * Called when the user submits the query. This could be due to a key press on the
112         * keyboard or due to pressing a submit button.
113         * The listener can override the standard behavior by returning true
114         * to indicate that it has handled the submit request. Otherwise return false to
115         * let the SearchView handle the submission by launching any associated intent.
116         *
117         * @param query the query text that is to be submitted
118         *
119         * @return true if the query has been handled by the listener, false to let the
120         * SearchView perform the default action.
121         */
122        boolean onSubmitQuery(String query);
123
124        /**
125         * Called when the query text is changed by the user.
126         *
127         * @param newText the new content of the query text field.
128         *
129         * @return false if the SearchView should perform the default action of showing any
130         * suggestions if available, true if the action was handled by the listener.
131         */
132        boolean onQueryTextChanged(String newText);
133    }
134
135    public interface OnCloseListener {
136
137        /**
138         * The user is attempting to close the SearchView.
139         *
140         * @return true if the listener wants to override the default behavior of clearing the
141         * text field and dismissing it, false otherwise.
142         */
143        boolean onClose();
144    }
145
146    /**
147     * Callback interface for selection events on suggestions. These callbacks
148     * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
149     */
150    public interface OnSuggestionSelectionListener {
151
152        /**
153         * Called when a suggestion was selected by navigating to it.
154         * @param position the absolute position in the list of suggestions.
155         *
156         * @return true if the listener handles the event and wants to override the default
157         * behavior of possibly rewriting the query based on the selected item, false otherwise.
158         */
159        boolean onSuggestionSelected(int position);
160
161        /**
162         * Called when a suggestion was clicked.
163         * @param position the absolute position of the clicked item in the list of suggestions.
164         *
165         * @return true if the listener handles the event and wants to override the default
166         * behavior of launching any intent or submitting a search query specified on that item.
167         * Return false otherwise.
168         */
169        boolean onSuggestionClicked(int position);
170    }
171
172    public SearchView(Context context) {
173        this(context, null);
174    }
175
176    public SearchView(Context context, AttributeSet attrs) {
177        super(context, attrs);
178
179        LayoutInflater inflater = (LayoutInflater) context
180                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
181        inflater.inflate(R.layout.search_view, this, true);
182
183        mSearchButton = findViewById(R.id.search_button);
184        mQueryTextView = (AutoCompleteTextView) findViewById(R.id.search_src_text);
185        mSearchEditFrame = findViewById(R.id.search_edit_frame);
186        mSubmitButton = findViewById(R.id.search_go_btn);
187        mCloseButton = findViewById(R.id.search_close_btn);
188        mVoiceButton = findViewById(R.id.search_voice_btn);
189
190        mSearchButton.setOnClickListener(mOnClickListener);
191        mCloseButton.setOnClickListener(mOnClickListener);
192        mSubmitButton.setOnClickListener(mOnClickListener);
193        mVoiceButton.setOnClickListener(mOnClickListener);
194
195        mQueryTextView.addTextChangedListener(mTextWatcher);
196        mQueryTextView.setOnEditorActionListener(mOnEditorActionListener);
197        mQueryTextView.setOnItemClickListener(mOnItemClickListener);
198        mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener);
199        // Inform any listener of focus changes
200        mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
201
202            public void onFocusChange(View v, boolean hasFocus) {
203                if (mOnQueryTextFocusChangeListener != null) {
204                    mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
205                }
206            }
207        });
208
209        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0);
210        setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
211        int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
212        if (maxWidth != -1) {
213            setMaxWidth(maxWidth);
214        }
215        a.recycle();
216
217        // Save voice intent for later queries/launching
218        mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
219        mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
220        mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
221                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
222
223        mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
224        mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
225
226        updateViewsVisibility(mIconifiedByDefault);
227    }
228
229    /**
230     * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
231     * to display labels, hints, suggestions, create intents for launching search results screens
232     * and controlling other affordances such as a voice button.
233     *
234     * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
235     * activity or a global search provider.
236     */
237    public void setSearchableInfo(SearchableInfo searchable) {
238        mSearchable = searchable;
239        if (mSearchable != null) {
240            updateSearchAutoComplete();
241        }
242        updateViewsVisibility(mIconifiedByDefault);
243    }
244
245    /** @hide */
246    @Override
247    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
248        if (mClearingFocus || isIconified()) return false;
249        return mQueryTextView.requestFocus(direction, previouslyFocusedRect);
250    }
251
252    /** @hide */
253    @Override
254    public void clearFocus() {
255        mClearingFocus = true;
256        super.clearFocus();
257        mQueryTextView.clearFocus();
258        setImeVisibility(false);
259        mClearingFocus = false;
260    }
261
262    /**
263     * Sets a listener for user actions within the SearchView.
264     *
265     * @param listener the listener object that receives callbacks when the user performs
266     * actions in the SearchView such as clicking on buttons or typing a query.
267     */
268    public void setOnQueryChangeListener(OnQueryChangeListener listener) {
269        mOnQueryChangeListener = listener;
270    }
271
272    /**
273     * Sets a listener to inform when the user closes the SearchView.
274     *
275     * @param listener the listener to call when the user closes the SearchView.
276     */
277    public void setOnCloseListener(OnCloseListener listener) {
278        mOnCloseListener = listener;
279    }
280
281    /**
282     * Sets a listener to inform when the focus of the query text field changes.
283     *
284     * @param listener the listener to inform of focus changes.
285     */
286    public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
287        mOnQueryTextFocusChangeListener = listener;
288    }
289
290    /**
291     * Sets a listener to inform when a suggestion is focused or clicked.
292     *
293     * @param listener the listener to inform of suggestion selection events.
294     */
295    public void setOnSuggestionSelectionListener(OnSuggestionSelectionListener listener) {
296        mOnSuggestionListener = listener;
297    }
298
299    /**
300     * Sets a query string in the text field and optionally submits the query as well.
301     *
302     * @param query the query string. This replaces any query text already present in the
303     * text field.
304     * @param submit whether to submit the query right now or only update the contents of
305     * text field.
306     */
307    public void setQuery(CharSequence query, boolean submit) {
308        mQueryTextView.setText(query);
309        // If the query is not empty and submit is requested, submit the query
310        if (submit && !TextUtils.isEmpty(query)) {
311            onSubmitQuery();
312        }
313    }
314
315    /**
316     * Sets the hint text to display in the query text field. This overrides any hint specified
317     * in the SearchableInfo.
318     *
319     * @param hint the hint text to display
320     */
321    public void setQueryHint(CharSequence hint) {
322        mQueryHint = hint;
323        updateQueryHint();
324    }
325
326    /**
327     * Sets the default or resting state of the search field. If true, a single search icon is
328     * shown by default and expands to show the text field and other buttons when pressed. Also,
329     * if the default state is iconified, then it collapses to that state when the close button
330     * is pressed. Changes to this property will take effect immediately.
331     *
332     * <p>The default value is false.</p>
333     *
334     * @param iconified whether the search field should be iconified by default
335     */
336    public void setIconifiedByDefault(boolean iconified) {
337        if (mIconifiedByDefault == iconified) return;
338        mIconifiedByDefault = iconified;
339        updateViewsVisibility(iconified);
340        setImeVisibility(!iconified);
341    }
342
343    /**
344     * Returns the default iconified state of the search field.
345     * @return
346     */
347    public boolean isIconfiedByDefault() {
348        return mIconifiedByDefault;
349    }
350
351    /**
352     * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
353     * a temporary state and does not override the default iconified state set by
354     * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
355     * a false here will only be valid until the user closes the field. And if the default
356     * state is expanded, then a true here will only clear the text field and not close it.
357     *
358     * @param iconify a true value will collapse the SearchView to an icon, while a false will
359     * expand it.
360     */
361    public void setIconified(boolean iconify) {
362        if (iconify) {
363            onCloseClicked();
364        } else {
365            onSearchClicked();
366        }
367    }
368
369    /**
370     * Returns the current iconified state of the SearchView.
371     *
372     * @return true if the SearchView is currently iconified, false if the search field is
373     * fully visible.
374     */
375    public boolean isIconified() {
376        return mIconified;
377    }
378
379    /**
380     * Enables showing a submit button when the query is non-empty. In cases where the SearchView
381     * is being used to filter the contents of the current activity and doesn't launch a separate
382     * results activity, then the submit button should be disabled.
383     *
384     * @param enabled true to show a submit button for submitting queries, false if a submit
385     * button is not required.
386     */
387    public void setSubmitButtonEnabled(boolean enabled) {
388        mSubmitButtonEnabled = enabled;
389        updateViewsVisibility(isIconified());
390    }
391
392    /**
393     * Returns whether the submit button is enabled when necessary or never displayed.
394     *
395     * @return whether the submit button is enabled automatically when necessary
396     */
397    public boolean isSubmitButtonEnabled() {
398        return mSubmitButtonEnabled;
399    }
400
401    /**
402     * Specifies if a query refinement button should be displayed alongside each suggestion
403     * or if it should depend on the flags set in the individual items retrieved from the
404     * suggestions provider. Clicking on the query refinement button will replace the text
405     * in the query text field with the text from the suggestion. This flag only takes effect
406     * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
407     * and not when using a custom adapter.
408     *
409     * @param enable true if all items should have a query refinement button, false if only
410     * those items that have a query refinement flag set should have the button.
411     *
412     * @see SearchManager#SUGGEST_COLUMN_FLAGS
413     * @see SearchManager#FLAG_QUERY_REFINEMENT
414     */
415    public void setQueryRefinementEnabled(boolean enable) {
416        mQueryRefinement = enable;
417        if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
418            ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
419                    enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
420        }
421    }
422
423    /**
424     * Returns whether query refinement is enabled for all items or only specific ones.
425     * @return true if enabled for all items, false otherwise.
426     */
427    public boolean isQueryRefinementEnabled() {
428        return mQueryRefinement;
429    }
430
431    /**
432     * You can set a custom adapter if you wish. Otherwise the default adapter is used to
433     * display the suggestions from the suggestions provider associated with the SearchableInfo.
434     *
435     * @see #setSearchableInfo(SearchableInfo)
436     */
437    public void setSuggestionsAdapter(CursorAdapter adapter) {
438        mSuggestionsAdapter = adapter;
439
440        mQueryTextView.setAdapter(mSuggestionsAdapter);
441    }
442
443    /**
444     * Returns the adapter used for suggestions, if any.
445     * @return the suggestions adapter
446     */
447    public CursorAdapter getSuggestionsAdapter() {
448        return mSuggestionsAdapter;
449    }
450
451    /**
452     * Makes the view at most this many pixels wide
453     *
454     * @attr ref android.R.styleable#SearchView_maxWidth
455     */
456    public void setMaxWidth(int maxpixels) {
457        mMaxWidth = maxpixels;
458
459        requestLayout();
460    }
461
462    @Override
463    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
464        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
465        int width = MeasureSpec.getSize(widthMeasureSpec);
466
467        if ((widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.EXACTLY) && mMaxWidth > 0
468                && width > mMaxWidth) {
469            super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxWidth, widthMode), heightMeasureSpec);
470        } else {
471            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
472        }
473    }
474
475    private void updateViewsVisibility(final boolean collapsed) {
476        mIconified = collapsed;
477        // Visibility of views that are visible when collapsed
478        final int visCollapsed = collapsed ? VISIBLE : GONE;
479        // Visibility of views that are visible when expanded
480        final int visExpanded = collapsed ? GONE : VISIBLE;
481        // Is there text in the query
482        final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
483
484        mSearchButton.setVisibility(visCollapsed);
485        mSubmitButton.setVisibility(mSubmitButtonEnabled && hasText ? visExpanded : GONE);
486        mSearchEditFrame.setVisibility(visExpanded);
487        updateVoiceButton(!hasText);
488        requestLayout();
489        invalidate();
490    }
491
492    private void setImeVisibility(boolean visible) {
493        InputMethodManager imm = (InputMethodManager)
494        getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
495
496        // We made sure the IME was displayed, so also make sure it is closed
497        // when we go away.
498        if (imm != null) {
499            if (visible) {
500                imm.showSoftInputUnchecked(0, null);
501            } else {
502                imm.hideSoftInputFromWindow(getWindowToken(), 0);
503            }
504        }
505    }
506
507    /**
508     * Called by the SuggestionsAdapter
509     * @hide
510     */
511    /* package */void onQueryRefine(CharSequence queryText) {
512        setQuery(queryText);
513    }
514
515    private final OnClickListener mOnClickListener = new OnClickListener() {
516
517        public void onClick(View v) {
518            if (v == mSearchButton) {
519                onSearchClicked();
520            } else if (v == mCloseButton) {
521                onCloseClicked();
522            } else if (v == mSubmitButton) {
523                onSubmitQuery();
524            } else if (v == mVoiceButton) {
525                onVoiceClicked();
526            }
527        }
528    };
529
530    /**
531     * Handles the key down event for dealing with action keys.
532     *
533     * @param keyCode This is the keycode of the typed key, and is the same value as
534     *        found in the KeyEvent parameter.
535     * @param event The complete event record for the typed key
536     *
537     * @return true if the event was handled here, or false if not.
538     */
539    @Override
540    public boolean onKeyDown(int keyCode, KeyEvent event) {
541        if (mSearchable == null) {
542            return false;
543        }
544
545        // if it's an action specified by the searchable activity, launch the
546        // entered query with the action key
547        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
548        if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
549            launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText()
550                    .toString());
551            return true;
552        }
553
554        return super.onKeyDown(keyCode, event);
555    }
556
557    private void updateQueryHint() {
558        if (mQueryHint != null) {
559            mQueryTextView.setHint(mQueryHint);
560        } else if (mSearchable != null) {
561            CharSequence hint = null;
562            int hintId = mSearchable.getHintId();
563            if (hintId != 0) {
564                hint = getContext().getString(hintId);
565            }
566            if (hint != null) {
567                mQueryTextView.setHint(hint);
568            }
569        }
570    }
571
572    /**
573     * Updates the auto-complete text view.
574     */
575    private void updateSearchAutoComplete() {
576        // close any existing suggestions adapter
577        //closeSuggestionsAdapter();
578
579        mQueryTextView.setDropDownAnimationStyle(0); // no animation
580        mQueryTextView.setThreshold(mSearchable.getSuggestThreshold());
581
582        // attach the suggestions adapter, if suggestions are available
583        // The existence of a suggestions authority is the proxy for "suggestions available here"
584        if (mSearchable.getSuggestAuthority() != null) {
585            mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
586                    this, mSearchable, mOutsideDrawablesCache);
587            mQueryTextView.setAdapter(mSuggestionsAdapter);
588            ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
589                    mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
590                    : SuggestionsAdapter.REFINE_BY_ENTRY);
591        }
592    }
593
594    /**
595     * Update the visibility of the voice button.  There are actually two voice search modes,
596     * either of which will activate the button.
597     * @param empty whether the search query text field is empty. If it is, then the other
598     * criteria apply to make the voice button visible. Otherwise the voice button will not
599     * be visible - i.e., if the user has typed a query, remove the voice button.
600     */
601    private void updateVoiceButton(boolean empty) {
602        int visibility = View.GONE;
603        if (mSearchable != null && mSearchable.getVoiceSearchEnabled() && empty
604                && !isIconified()) {
605            Intent testIntent = null;
606            if (mSearchable.getVoiceSearchLaunchWebSearch()) {
607                testIntent = mVoiceWebSearchIntent;
608            } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
609                testIntent = mVoiceAppSearchIntent;
610            }
611            if (testIntent != null) {
612                ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
613                        PackageManager.MATCH_DEFAULT_ONLY);
614                if (ri != null) {
615                    visibility = View.VISIBLE;
616                }
617            }
618        }
619        mVoiceButton.setVisibility(visibility);
620    }
621
622    private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
623
624        /**
625         * Called when the input method default action key is pressed.
626         */
627        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
628            onSubmitQuery();
629            return true;
630        }
631    };
632
633    private void onTextChanged(CharSequence newText) {
634        CharSequence text = mQueryTextView.getText();
635        boolean hasText = !TextUtils.isEmpty(text);
636        if (isSubmitButtonEnabled()) {
637            mSubmitButton.setVisibility(hasText ? VISIBLE : GONE);
638            requestLayout();
639            invalidate();
640        }
641        updateVoiceButton(!hasText);
642        if (mOnQueryChangeListener != null) {
643            mOnQueryChangeListener.onQueryTextChanged(newText.toString());
644        }
645    }
646
647    private void onSubmitQuery() {
648        CharSequence query = mQueryTextView.getText();
649        if (!TextUtils.isEmpty(query)) {
650            if (mOnQueryChangeListener == null
651                    || !mOnQueryChangeListener.onSubmitQuery(query.toString())) {
652                if (mSearchable != null) {
653                    launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
654                    setImeVisibility(false);
655                }
656                dismissSuggestions();
657            }
658        }
659    }
660
661    private void dismissSuggestions() {
662        mQueryTextView.dismissDropDown();
663    }
664
665    private void onCloseClicked() {
666        if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
667            CharSequence text = mQueryTextView.getText();
668            if (TextUtils.isEmpty(text)) {
669                // query field already empty, hide the keyboard and remove focus
670                clearFocus();
671                setImeVisibility(false);
672            } else {
673                mQueryTextView.setText("");
674            }
675            updateViewsVisibility(mIconifiedByDefault);
676            if (mIconifiedByDefault) setImeVisibility(false);
677        }
678    }
679
680    private void onSearchClicked() {
681        mQueryTextView.requestFocus();
682        updateViewsVisibility(false);
683        setImeVisibility(true);
684    }
685
686    private void onVoiceClicked() {
687        // guard against possible race conditions
688        if (mSearchable == null) {
689            return;
690        }
691        SearchableInfo searchable = mSearchable;
692        try {
693            if (searchable.getVoiceSearchLaunchWebSearch()) {
694                Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
695                        searchable);
696                getContext().startActivity(webSearchIntent);
697            } else if (searchable.getVoiceSearchLaunchRecognizer()) {
698                Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
699                        searchable);
700                getContext().startActivity(appSearchIntent);
701            }
702        } catch (ActivityNotFoundException e) {
703            // Should not happen, since we check the availability of
704            // voice search before showing the button. But just in case...
705            Log.w(LOG_TAG, "Could not find voice search activity");
706        }
707    }
708
709    private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
710
711        /**
712         * Implements OnItemClickListener
713         */
714        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
715            if (DBG)
716                Log.d(LOG_TAG, "onItemClick() position " + position);
717            if (mOnSuggestionListener == null
718                    || !mOnSuggestionListener.onSuggestionClicked(position)) {
719                launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
720                dismissSuggestions();
721            }
722        }
723    };
724
725    private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
726
727        /**
728         * Implements OnItemSelectedListener
729         */
730        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
731            if (DBG)
732                Log.d(LOG_TAG, "onItemSelected() position " + position);
733            // A suggestion has been selected, rewrite the query if possible,
734            // otherwise the restore the original query.
735            if (mOnSuggestionListener == null
736                    || !mOnSuggestionListener.onSuggestionSelected(position)) {
737                rewriteQueryFromSuggestion(position);
738            }
739        }
740
741        /**
742         * Implements OnItemSelectedListener
743         */
744        public void onNothingSelected(AdapterView<?> parent) {
745            if (DBG)
746                Log.d(LOG_TAG, "onNothingSelected()");
747        }
748    };
749
750    /**
751     * Query rewriting.
752     */
753    private void rewriteQueryFromSuggestion(int position) {
754        CharSequence oldQuery = mQueryTextView.getText();
755        Cursor c = mSuggestionsAdapter.getCursor();
756        if (c == null) {
757            return;
758        }
759        if (c.moveToPosition(position)) {
760            // Get the new query from the suggestion.
761            CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
762            if (newQuery != null) {
763                // The suggestion rewrites the query.
764                // Update the text field, without getting new suggestions.
765                setQuery(newQuery);
766            } else {
767                // The suggestion does not rewrite the query, restore the user's query.
768                setQuery(oldQuery);
769            }
770        } else {
771            // We got a bad position, restore the user's query.
772            setQuery(oldQuery);
773        }
774    }
775
776    /**
777     * Launches an intent based on a suggestion.
778     *
779     * @param position The index of the suggestion to create the intent from.
780     * @param actionKey The key code of the action key that was pressed,
781     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
782     * @param actionMsg The message for the action key that was pressed,
783     *        or <code>null</code> if none.
784     * @return true if a successful launch, false if could not (e.g. bad position).
785     */
786    private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
787        Cursor c = mSuggestionsAdapter.getCursor();
788        if ((c != null) && c.moveToPosition(position)) {
789
790            Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
791
792            // launch the intent
793            launchIntent(intent);
794
795            return true;
796        }
797        return false;
798    }
799
800    /**
801     * Launches an intent, including any special intent handling.
802     */
803    private void launchIntent(Intent intent) {
804        if (intent == null) {
805            return;
806        }
807        try {
808            // If the intent was created from a suggestion, it will always have an explicit
809            // component here.
810            getContext().startActivity(intent);
811        } catch (RuntimeException ex) {
812            Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
813        }
814    }
815
816    /**
817     * Sets the text in the query box, without updating the suggestions.
818     */
819    private void setQuery(CharSequence query) {
820        mQueryTextView.setText(query, true);
821        // Move the cursor to the end
822        mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
823    }
824
825    private void launchQuerySearch(int actionKey, String actionMsg, String query) {
826        String action = Intent.ACTION_SEARCH;
827        Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
828        getContext().startActivity(intent);
829    }
830
831    /**
832     * Constructs an intent from the given information and the search dialog state.
833     *
834     * @param action Intent action.
835     * @param data Intent data, or <code>null</code>.
836     * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
837     * @param query Intent query, or <code>null</code>.
838     * @param actionKey The key code of the action key that was pressed,
839     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
840     * @param actionMsg The message for the action key that was pressed,
841     *        or <code>null</code> if none.
842     * @param mode The search mode, one of the acceptable values for
843     *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
844     * @return The intent.
845     */
846    private Intent createIntent(String action, Uri data, String extraData, String query,
847            int actionKey, String actionMsg) {
848        // Now build the Intent
849        Intent intent = new Intent(action);
850        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
851        // We need CLEAR_TOP to avoid reusing an old task that has other activities
852        // on top of the one we want. We don't want to do this in in-app search though,
853        // as it can be destructive to the activity stack.
854        if (data != null) {
855            intent.setData(data);
856        }
857        intent.putExtra(SearchManager.USER_QUERY, query);
858        if (query != null) {
859            intent.putExtra(SearchManager.QUERY, query);
860        }
861        if (extraData != null) {
862            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
863        }
864        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
865            intent.putExtra(SearchManager.ACTION_KEY, actionKey);
866            intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
867        }
868        intent.setComponent(mSearchable.getSearchActivity());
869        return intent;
870    }
871
872    /**
873     * Create and return an Intent that can launch the voice search activity for web search.
874     */
875    private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
876        Intent voiceIntent = new Intent(baseIntent);
877        ComponentName searchActivity = searchable.getSearchActivity();
878        voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
879                : searchActivity.flattenToShortString());
880        return voiceIntent;
881    }
882
883    /**
884     * Create and return an Intent that can launch the voice search activity, perform a specific
885     * voice transcription, and forward the results to the searchable activity.
886     *
887     * @param baseIntent The voice app search intent to start from
888     * @return A completely-configured intent ready to send to the voice search activity
889     */
890    private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
891        ComponentName searchActivity = searchable.getSearchActivity();
892
893        // create the necessary intent to set up a search-and-forward operation
894        // in the voice search system.   We have to keep the bundle separate,
895        // because it becomes immutable once it enters the PendingIntent
896        Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
897        queryIntent.setComponent(searchActivity);
898        PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
899                PendingIntent.FLAG_ONE_SHOT);
900
901        // Now set up the bundle that will be inserted into the pending intent
902        // when it's time to do the search.  We always build it here (even if empty)
903        // because the voice search activity will always need to insert "QUERY" into
904        // it anyway.
905        Bundle queryExtras = new Bundle();
906
907        // Now build the intent to launch the voice search.  Add all necessary
908        // extras to launch the voice recognizer, and then all the necessary extras
909        // to forward the results to the searchable activity
910        Intent voiceIntent = new Intent(baseIntent);
911
912        // Add all of the configuration options supplied by the searchable's metadata
913        String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
914        String prompt = null;
915        String language = null;
916        int maxResults = 1;
917
918        Resources resources = getResources();
919        if (searchable.getVoiceLanguageModeId() != 0) {
920            languageModel = resources.getString(searchable.getVoiceLanguageModeId());
921        }
922        if (searchable.getVoicePromptTextId() != 0) {
923            prompt = resources.getString(searchable.getVoicePromptTextId());
924        }
925        if (searchable.getVoiceLanguageId() != 0) {
926            language = resources.getString(searchable.getVoiceLanguageId());
927        }
928        if (searchable.getVoiceMaxResults() != 0) {
929            maxResults = searchable.getVoiceMaxResults();
930        }
931        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
932        voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
933        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
934        voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
935        voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
936                : searchActivity.flattenToShortString());
937
938        // Add the values that configure forwarding the results
939        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
940        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
941
942        return voiceIntent;
943    }
944
945    /**
946     * When a particular suggestion has been selected, perform the various lookups required
947     * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
948     * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
949     * the suggestion includes a data id.
950     *
951     * @param c The suggestions cursor, moved to the row of the user's selection
952     * @param actionKey The key code of the action key that was pressed,
953     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
954     * @param actionMsg The message for the action key that was pressed,
955     *        or <code>null</code> if none.
956     * @return An intent for the suggestion at the cursor's position.
957     */
958    private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
959        try {
960            // use specific action if supplied, or default action if supplied, or fixed default
961            String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
962
963            if (action == null) {
964                action = mSearchable.getSuggestIntentAction();
965            }
966            if (action == null) {
967                action = Intent.ACTION_SEARCH;
968            }
969
970            // use specific data if supplied, or default data if supplied
971            String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
972            if (data == null) {
973                data = mSearchable.getSuggestIntentData();
974            }
975            // then, if an ID was provided, append it.
976            if (data != null) {
977                String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
978                if (id != null) {
979                    data = data + "/" + Uri.encode(id);
980                }
981            }
982            Uri dataUri = (data == null) ? null : Uri.parse(data);
983
984            String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
985            String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
986
987            return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
988        } catch (RuntimeException e ) {
989            int rowNum;
990            try {                       // be really paranoid now
991                rowNum = c.getPosition();
992            } catch (RuntimeException e2 ) {
993                rowNum = -1;
994            }
995            Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
996                            " returned exception" + e.toString());
997            return null;
998        }
999    }
1000
1001    /**
1002     * Callback to watch the text field for empty/non-empty
1003     */
1004    private TextWatcher mTextWatcher = new TextWatcher() {
1005
1006        public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
1007
1008        public void onTextChanged(CharSequence s, int start,
1009                int before, int after) {
1010            SearchView.this.onTextChanged(s);
1011        }
1012
1013        public void afterTextChanged(Editable s) {
1014        }
1015    };
1016}
1017