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