SearchView.java revision c0171d5e8ed1aaeaa658aa0d603860f7ada6807a
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.Configuration;
33import android.content.res.Resources;
34import android.content.res.TypedArray;
35import android.database.Cursor;
36import android.graphics.Rect;
37import android.graphics.drawable.Drawable;
38import android.net.Uri;
39import android.os.Bundle;
40import android.speech.RecognizerIntent;
41import android.text.Editable;
42import android.text.TextUtils;
43import android.text.TextWatcher;
44import android.util.AttributeSet;
45import android.util.Log;
46import android.view.KeyEvent;
47import android.view.LayoutInflater;
48import android.view.View;
49import android.view.inputmethod.InputMethodManager;
50import android.widget.AdapterView.OnItemClickListener;
51import android.widget.AdapterView.OnItemSelectedListener;
52import android.widget.TextView.OnEditorActionListener;
53
54import java.util.WeakHashMap;
55
56/**
57 * Provides the user interface elements for the user to enter a search query and submit a
58 * request to a search provider. Shows a list of query suggestions or results, if
59 * available and allows the user to pick a suggestion or result to launch into.
60 *
61 * <p>
62 * <b>XML attributes</b>
63 * <p>
64 * See {@link android.R.styleable#SearchView SearchView Attributes},
65 * {@link android.R.styleable#View View Attributes}
66 *
67 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
68 * @attr ref android.R.styleable#SearchView_maxWidth
69 */
70public class SearchView extends LinearLayout {
71
72    private static final boolean DBG = false;
73    private static final String LOG_TAG = "SearchView";
74
75    private OnQueryChangeListener mOnQueryChangeListener;
76    private OnCloseListener mOnCloseListener;
77    private OnFocusChangeListener mOnQueryTextFocusChangeListener;
78    private OnSuggestionSelectionListener mOnSuggestionListener;
79    private OnClickListener mOnSearchClickListener;
80
81    private boolean mIconifiedByDefault;
82    private boolean mIconified;
83    private CursorAdapter mSuggestionsAdapter;
84    private View mSearchButton;
85    private View mSubmitButton;
86    private ImageView mCloseButton;
87    private View mSearchEditFrame;
88    private View mVoiceButton;
89    private SearchAutoComplete mQueryTextView;
90    private boolean mSubmitButtonEnabled;
91    private CharSequence mQueryHint;
92    private boolean mQueryRefinement;
93    private boolean mClearingFocus;
94    private int mMaxWidth;
95
96    private SearchableInfo mSearchable;
97
98    // For voice searching
99    private final Intent mVoiceWebSearchIntent;
100    private final Intent mVoiceAppSearchIntent;
101
102    // A weak map of drawables we've gotten from other packages, so we don't load them
103    // more than once.
104    private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
105            new WeakHashMap<String, Drawable.ConstantState>();
106
107    /**
108     * Callbacks for changes to the query text.
109     */
110    public interface OnQueryChangeListener {
111
112        /**
113         * Called when the user submits the query. This could be due to a key press on the
114         * keyboard or due to pressing a submit button.
115         * The listener can override the standard behavior by returning true
116         * to indicate that it has handled the submit request. Otherwise return false to
117         * let the SearchView handle the submission by launching any associated intent.
118         *
119         * @param query the query text that is to be submitted
120         *
121         * @return true if the query has been handled by the listener, false to let the
122         * SearchView perform the default action.
123         */
124        boolean onSubmitQuery(String query);
125
126        /**
127         * Called when the query text is changed by the user.
128         *
129         * @param newText the new content of the query text field.
130         *
131         * @return false if the SearchView should perform the default action of showing any
132         * suggestions if available, true if the action was handled by the listener.
133         */
134        boolean onQueryTextChanged(String newText);
135    }
136
137    public interface OnCloseListener {
138
139        /**
140         * The user is attempting to close the SearchView.
141         *
142         * @return true if the listener wants to override the default behavior of clearing the
143         * text field and dismissing it, false otherwise.
144         */
145        boolean onClose();
146    }
147
148    /**
149     * Callback interface for selection events on suggestions. These callbacks
150     * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
151     */
152    public interface OnSuggestionSelectionListener {
153
154        /**
155         * Called when a suggestion was selected by navigating to it.
156         * @param position the absolute position in the list of suggestions.
157         *
158         * @return true if the listener handles the event and wants to override the default
159         * behavior of possibly rewriting the query based on the selected item, false otherwise.
160         */
161        boolean onSuggestionSelected(int position);
162
163        /**
164         * Called when a suggestion was clicked.
165         * @param position the absolute position of the clicked item in the list of suggestions.
166         *
167         * @return true if the listener handles the event and wants to override the default
168         * behavior of launching any intent or submitting a search query specified on that item.
169         * Return false otherwise.
170         */
171        boolean onSuggestionClicked(int position);
172    }
173
174    public SearchView(Context context) {
175        this(context, null);
176    }
177
178    public SearchView(Context context, AttributeSet attrs) {
179        super(context, attrs);
180
181        LayoutInflater inflater = (LayoutInflater) context
182                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
183        inflater.inflate(R.layout.search_view, this, true);
184
185        mSearchButton = findViewById(R.id.search_button);
186        mQueryTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
187        mQueryTextView.setSearchView(this);
188
189        mSearchEditFrame = findViewById(R.id.search_edit_frame);
190        mSubmitButton = findViewById(R.id.search_go_btn);
191        mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
192        mVoiceButton = findViewById(R.id.search_voice_btn);
193
194        mSearchButton.setOnClickListener(mOnClickListener);
195        mCloseButton.setOnClickListener(mOnClickListener);
196        mSubmitButton.setOnClickListener(mOnClickListener);
197        mVoiceButton.setOnClickListener(mOnClickListener);
198
199        mQueryTextView.addTextChangedListener(mTextWatcher);
200        mQueryTextView.setOnEditorActionListener(mOnEditorActionListener);
201        mQueryTextView.setOnItemClickListener(mOnItemClickListener);
202        mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener);
203        mQueryTextView.setOnKeyListener(mTextKeyListener);
204        // Inform any listener of focus changes
205        mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
206
207            public void onFocusChange(View v, boolean hasFocus) {
208                if (mOnQueryTextFocusChangeListener != null) {
209                    mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
210                }
211            }
212        });
213
214        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0);
215        setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
216        int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
217        if (maxWidth != -1) {
218            setMaxWidth(maxWidth);
219        }
220        CharSequence queryHint = a.getText(R.styleable.SearchView_queryHint);
221        if (!TextUtils.isEmpty(queryHint)) {
222            setQueryHint(queryHint);
223        }
224        a.recycle();
225
226        // Save voice intent for later queries/launching
227        mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
228        mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
229        mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
230                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
231
232        mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
233        mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
234
235        updateViewsVisibility(mIconifiedByDefault);
236    }
237
238    /**
239     * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
240     * to display labels, hints, suggestions, create intents for launching search results screens
241     * and controlling other affordances such as a voice button.
242     *
243     * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
244     * activity or a global search provider.
245     */
246    public void setSearchableInfo(SearchableInfo searchable) {
247        mSearchable = searchable;
248        if (mSearchable != null) {
249            updateSearchAutoComplete();
250        }
251        updateViewsVisibility(mIconifiedByDefault);
252    }
253
254    /** @hide */
255    @Override
256    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
257        if (mClearingFocus || isIconified()) return false;
258        boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect);
259        if (result) updateViewsVisibility(mIconifiedByDefault);
260        return result;
261    }
262
263    /** @hide */
264    @Override
265    public void clearFocus() {
266        mClearingFocus = true;
267        super.clearFocus();
268        mQueryTextView.clearFocus();
269        setImeVisibility(false);
270        mClearingFocus = false;
271        updateViewsVisibility(mIconifiedByDefault);
272    }
273
274    /**
275     * Sets a listener for user actions within the SearchView.
276     *
277     * @param listener the listener object that receives callbacks when the user performs
278     * actions in the SearchView such as clicking on buttons or typing a query.
279     */
280    public void setOnQueryChangeListener(OnQueryChangeListener listener) {
281        mOnQueryChangeListener = listener;
282    }
283
284    /**
285     * Sets a listener to inform when the user closes the SearchView.
286     *
287     * @param listener the listener to call when the user closes the SearchView.
288     */
289    public void setOnCloseListener(OnCloseListener listener) {
290        mOnCloseListener = listener;
291    }
292
293    /**
294     * Sets a listener to inform when the focus of the query text field changes.
295     *
296     * @param listener the listener to inform of focus changes.
297     */
298    public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
299        mOnQueryTextFocusChangeListener = listener;
300    }
301
302    /**
303     * Sets a listener to inform when a suggestion is focused or clicked.
304     *
305     * @param listener the listener to inform of suggestion selection events.
306     */
307    public void setOnSuggestionSelectionListener(OnSuggestionSelectionListener listener) {
308        mOnSuggestionListener = listener;
309    }
310
311    /**
312     * Sets a listener to inform when the search button is pressed. This is only
313     * relevant when the text field is not visible by default. Calling #setIconified(false)
314     * can also cause this listener to be informed.
315     *
316     * @param listener the listener to inform when the search button is clicked or
317     * the text field is programmatically de-iconified.
318     */
319    public void setOnSearchClickListener(OnClickListener listener) {
320        mOnSearchClickListener = listener;
321    }
322
323    /**
324     * Returns the query string currently in the text field.
325     *
326     * @return the query string
327     */
328    public CharSequence getQuery() {
329        return mQueryTextView.getText();
330    }
331
332    /**
333     * Sets a query string in the text field and optionally submits the query as well.
334     *
335     * @param query the query string. This replaces any query text already present in the
336     * text field.
337     * @param submit whether to submit the query right now or only update the contents of
338     * text field.
339     */
340    public void setQuery(CharSequence query, boolean submit) {
341        mQueryTextView.setText(query);
342        if (query != null) {
343            mQueryTextView.setSelection(query.length());
344        }
345
346        // If the query is not empty and submit is requested, submit the query
347        if (submit && !TextUtils.isEmpty(query)) {
348            onSubmitQuery();
349        }
350    }
351
352    /**
353     * Sets the hint text to display in the query text field. This overrides any hint specified
354     * in the SearchableInfo.
355     *
356     * @param hint the hint text to display
357     */
358    public void setQueryHint(CharSequence hint) {
359        mQueryHint = hint;
360        updateQueryHint();
361    }
362
363    /**
364     * Sets the default or resting state of the search field. If true, a single search icon is
365     * shown by default and expands to show the text field and other buttons when pressed. Also,
366     * if the default state is iconified, then it collapses to that state when the close button
367     * is pressed. Changes to this property will take effect immediately.
368     *
369     * <p>The default value is false.</p>
370     *
371     * @param iconified whether the search field should be iconified by default
372     */
373    public void setIconifiedByDefault(boolean iconified) {
374        if (mIconifiedByDefault == iconified) return;
375        mIconifiedByDefault = iconified;
376        updateViewsVisibility(iconified);
377        setImeVisibility(!iconified);
378    }
379
380    /**
381     * Returns the default iconified state of the search field.
382     * @return
383     */
384    public boolean isIconfiedByDefault() {
385        return mIconifiedByDefault;
386    }
387
388    /**
389     * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
390     * a temporary state and does not override the default iconified state set by
391     * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
392     * a false here will only be valid until the user closes the field. And if the default
393     * state is expanded, then a true here will only clear the text field and not close it.
394     *
395     * @param iconify a true value will collapse the SearchView to an icon, while a false will
396     * expand it.
397     */
398    public void setIconified(boolean iconify) {
399        if (iconify) {
400            onCloseClicked();
401        } else {
402            onSearchClicked();
403        }
404    }
405
406    /**
407     * Returns the current iconified state of the SearchView.
408     *
409     * @return true if the SearchView is currently iconified, false if the search field is
410     * fully visible.
411     */
412    public boolean isIconified() {
413        return mIconified;
414    }
415
416    /**
417     * Enables showing a submit button when the query is non-empty. In cases where the SearchView
418     * is being used to filter the contents of the current activity and doesn't launch a separate
419     * results activity, then the submit button should be disabled.
420     *
421     * @param enabled true to show a submit button for submitting queries, false if a submit
422     * button is not required.
423     */
424    public void setSubmitButtonEnabled(boolean enabled) {
425        mSubmitButtonEnabled = enabled;
426        updateViewsVisibility(isIconified());
427    }
428
429    /**
430     * Returns whether the submit button is enabled when necessary or never displayed.
431     *
432     * @return whether the submit button is enabled automatically when necessary
433     */
434    public boolean isSubmitButtonEnabled() {
435        return mSubmitButtonEnabled;
436    }
437
438    /**
439     * Specifies if a query refinement button should be displayed alongside each suggestion
440     * or if it should depend on the flags set in the individual items retrieved from the
441     * suggestions provider. Clicking on the query refinement button will replace the text
442     * in the query text field with the text from the suggestion. This flag only takes effect
443     * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
444     * and not when using a custom adapter.
445     *
446     * @param enable true if all items should have a query refinement button, false if only
447     * those items that have a query refinement flag set should have the button.
448     *
449     * @see SearchManager#SUGGEST_COLUMN_FLAGS
450     * @see SearchManager#FLAG_QUERY_REFINEMENT
451     */
452    public void setQueryRefinementEnabled(boolean enable) {
453        mQueryRefinement = enable;
454        if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
455            ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
456                    enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
457        }
458    }
459
460    /**
461     * Returns whether query refinement is enabled for all items or only specific ones.
462     * @return true if enabled for all items, false otherwise.
463     */
464    public boolean isQueryRefinementEnabled() {
465        return mQueryRefinement;
466    }
467
468    /**
469     * You can set a custom adapter if you wish. Otherwise the default adapter is used to
470     * display the suggestions from the suggestions provider associated with the SearchableInfo.
471     *
472     * @see #setSearchableInfo(SearchableInfo)
473     */
474    public void setSuggestionsAdapter(CursorAdapter adapter) {
475        mSuggestionsAdapter = adapter;
476
477        mQueryTextView.setAdapter(mSuggestionsAdapter);
478    }
479
480    /**
481     * Returns the adapter used for suggestions, if any.
482     * @return the suggestions adapter
483     */
484    public CursorAdapter getSuggestionsAdapter() {
485        return mSuggestionsAdapter;
486    }
487
488    /**
489     * Makes the view at most this many pixels wide
490     *
491     * @attr ref android.R.styleable#SearchView_maxWidth
492     */
493    public void setMaxWidth(int maxpixels) {
494        mMaxWidth = maxpixels;
495
496        requestLayout();
497    }
498
499    @Override
500    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
501        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
502        int width = MeasureSpec.getSize(widthMeasureSpec);
503
504        if ((widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.EXACTLY) && mMaxWidth > 0
505                && width > mMaxWidth) {
506            super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxWidth, widthMode), heightMeasureSpec);
507        } else {
508            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
509        }
510    }
511
512    private void updateViewsVisibility(final boolean collapsed) {
513        mIconified = collapsed;
514        // Visibility of views that are visible when collapsed
515        final int visCollapsed = collapsed ? VISIBLE : GONE;
516        // Visibility of views that are visible when expanded
517        final int visExpanded = collapsed ? GONE : VISIBLE;
518        // Is there text in the query
519        final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
520
521        mSearchButton.setVisibility(visCollapsed);
522        mSubmitButton.setVisibility(mSubmitButtonEnabled && hasText ? visExpanded : GONE);
523        mSearchEditFrame.setVisibility(visExpanded);
524        updateCloseButton();
525        updateVoiceButton(!hasText);
526        requestLayout();
527        invalidate();
528    }
529
530    private void updateCloseButton() {
531        final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
532        // Should we show the close button? It is not shown if there's no focus,
533        // field is not iconified by default and there is no text in it.
534        final boolean showClose = hasText || mIconifiedByDefault || mQueryTextView.hasFocus();
535        mCloseButton.setVisibility(showClose ? VISIBLE : INVISIBLE);
536        mCloseButton.getDrawable().setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
537    }
538
539    private void setImeVisibility(boolean visible) {
540        InputMethodManager imm = (InputMethodManager)
541        getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
542
543        // We made sure the IME was displayed, so also make sure it is closed
544        // when we go away.
545        if (imm != null) {
546            if (visible) {
547                imm.showSoftInputUnchecked(0, null);
548            } else {
549                imm.hideSoftInputFromWindow(getWindowToken(), 0);
550            }
551        }
552    }
553
554    /**
555     * Called by the SuggestionsAdapter
556     * @hide
557     */
558    /* package */void onQueryRefine(CharSequence queryText) {
559        setQuery(queryText);
560    }
561
562    private final OnClickListener mOnClickListener = new OnClickListener() {
563
564        public void onClick(View v) {
565            if (v == mSearchButton) {
566                onSearchClicked();
567            } else if (v == mCloseButton) {
568                onCloseClicked();
569            } else if (v == mSubmitButton) {
570                onSubmitQuery();
571            } else if (v == mVoiceButton) {
572                onVoiceClicked();
573            }
574        }
575    };
576
577    /**
578     * Handles the key down event for dealing with action keys.
579     *
580     * @param keyCode This is the keycode of the typed key, and is the same value as
581     *        found in the KeyEvent parameter.
582     * @param event The complete event record for the typed key
583     *
584     * @return true if the event was handled here, or false if not.
585     */
586    @Override
587    public boolean onKeyDown(int keyCode, KeyEvent event) {
588        if (mSearchable == null) {
589            return false;
590        }
591
592        // if it's an action specified by the searchable activity, launch the
593        // entered query with the action key
594        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
595        if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
596            launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText()
597                    .toString());
598            return true;
599        }
600
601        return super.onKeyDown(keyCode, event);
602    }
603
604    /**
605     * React to the user typing "enter" or other hardwired keys while typing in
606     * the search box. This handles these special keys while the edit box has
607     * focus.
608     */
609    View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
610        public boolean onKey(View v, int keyCode, KeyEvent event) {
611            // guard against possible race conditions
612            if (mSearchable == null) {
613                return false;
614            }
615
616            if (DBG) {
617                Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
618                        + mQueryTextView.getListSelection());
619            }
620
621            // If a suggestion is selected, handle enter, search key, and action keys
622            // as presses on the selected suggestion
623            if (mQueryTextView.isPopupShowing()
624                    && mQueryTextView.getListSelection() != ListView.INVALID_POSITION) {
625                return onSuggestionsKey(v, keyCode, event);
626            }
627
628            // If there is text in the query box, handle enter, and action keys
629            // The search key is handled by the dialog's onKeyDown().
630            if (!mQueryTextView.isEmpty() && event.hasNoModifiers()) {
631                if (event.getAction() == KeyEvent.ACTION_UP) {
632                    if (keyCode == KeyEvent.KEYCODE_ENTER) {
633                        v.cancelLongPress();
634
635                        // Launch as a regular search.
636                        launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
637                                .toString());
638                        return true;
639                    }
640                }
641                if (event.getAction() == KeyEvent.ACTION_DOWN) {
642                    SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
643                    if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
644                        launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView
645                                .getText().toString());
646                        return true;
647                    }
648                }
649            }
650            return false;
651        }
652    };
653
654    /**
655     * React to the user typing while in the suggestions list. First, check for
656     * action keys. If not handled, try refocusing regular characters into the
657     * EditText.
658     */
659    private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
660        // guard against possible race conditions (late arrival after dismiss)
661        if (mSearchable == null) {
662            return false;
663        }
664        if (mSuggestionsAdapter == null) {
665            return false;
666        }
667        if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
668            // First, check for enter or search (both of which we'll treat as a
669            // "click")
670            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
671                    || keyCode == KeyEvent.KEYCODE_TAB) {
672                int position = mQueryTextView.getListSelection();
673                return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
674            }
675
676            // Next, check for left/right moves, which we use to "return" the
677            // user to the edit view
678            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
679                // give "focus" to text editor, with cursor at the beginning if
680                // left key, at end if right key
681                // TODO: Reverse left/right for right-to-left languages, e.g.
682                // Arabic
683                int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView
684                        .length();
685                mQueryTextView.setSelection(selPoint);
686                mQueryTextView.setListSelection(0);
687                mQueryTextView.clearListSelection();
688                mQueryTextView.ensureImeVisible(true);
689
690                return true;
691            }
692
693            // Next, check for an "up and out" move
694            if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mQueryTextView.getListSelection()) {
695                // TODO: restoreUserQuery();
696                // let ACTV complete the move
697                return false;
698            }
699
700            // Next, check for an "action key"
701            SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
702            if ((actionKey != null)
703                    && ((actionKey.getSuggestActionMsg() != null) || (actionKey
704                            .getSuggestActionMsgColumn() != null))) {
705                // launch suggestion using action key column
706                int position = mQueryTextView.getListSelection();
707                if (position != ListView.INVALID_POSITION) {
708                    Cursor c = mSuggestionsAdapter.getCursor();
709                    if (c.moveToPosition(position)) {
710                        final String actionMsg = getActionKeyMessage(c, actionKey);
711                        if (actionMsg != null && (actionMsg.length() > 0)) {
712                            return onItemClicked(position, keyCode, actionMsg);
713                        }
714                    }
715                }
716            }
717        }
718        return false;
719    }
720
721    /**
722     * For a given suggestion and a given cursor row, get the action message. If
723     * not provided by the specific row/column, also check for a single
724     * definition (for the action key).
725     *
726     * @param c The cursor providing suggestions
727     * @param actionKey The actionkey record being examined
728     *
729     * @return Returns a string, or null if no action key message for this
730     *         suggestion
731     */
732    private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
733        String result = null;
734        // check first in the cursor data, for a suggestion-specific message
735        final String column = actionKey.getSuggestActionMsgColumn();
736        if (column != null) {
737            result = SuggestionsAdapter.getColumnString(c, column);
738        }
739        // If the cursor didn't give us a message, see if there's a single
740        // message defined
741        // for the actionkey (for all suggestions)
742        if (result == null) {
743            result = actionKey.getSuggestActionMsg();
744        }
745        return result;
746    }
747
748    private void updateQueryHint() {
749        if (mQueryHint != null) {
750            mQueryTextView.setHint(mQueryHint);
751        } else if (mSearchable != null) {
752            CharSequence hint = null;
753            int hintId = mSearchable.getHintId();
754            if (hintId != 0) {
755                hint = getContext().getString(hintId);
756            }
757            if (hint != null) {
758                mQueryTextView.setHint(hint);
759            }
760        }
761    }
762
763    /**
764     * Updates the auto-complete text view.
765     */
766    private void updateSearchAutoComplete() {
767        // close any existing suggestions adapter
768        //closeSuggestionsAdapter();
769
770        mQueryTextView.setDropDownAnimationStyle(0); // no animation
771        mQueryTextView.setThreshold(mSearchable.getSuggestThreshold());
772
773        // attach the suggestions adapter, if suggestions are available
774        // The existence of a suggestions authority is the proxy for "suggestions available here"
775        if (mSearchable.getSuggestAuthority() != null) {
776            mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
777                    this, mSearchable, mOutsideDrawablesCache);
778            mQueryTextView.setAdapter(mSuggestionsAdapter);
779            ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
780                    mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
781                    : SuggestionsAdapter.REFINE_BY_ENTRY);
782        }
783    }
784
785    /**
786     * Update the visibility of the voice button.  There are actually two voice search modes,
787     * either of which will activate the button.
788     * @param empty whether the search query text field is empty. If it is, then the other
789     * criteria apply to make the voice button visible. Otherwise the voice button will not
790     * be visible - i.e., if the user has typed a query, remove the voice button.
791     */
792    private void updateVoiceButton(boolean empty) {
793        int visibility = View.GONE;
794        if (mSearchable != null && mSearchable.getVoiceSearchEnabled() && empty
795                && !isIconified()) {
796            Intent testIntent = null;
797            if (mSearchable.getVoiceSearchLaunchWebSearch()) {
798                testIntent = mVoiceWebSearchIntent;
799            } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
800                testIntent = mVoiceAppSearchIntent;
801            }
802            if (testIntent != null) {
803                ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
804                        PackageManager.MATCH_DEFAULT_ONLY);
805                if (ri != null) {
806                    visibility = View.VISIBLE;
807                }
808            }
809        }
810        mVoiceButton.setVisibility(visibility);
811    }
812
813    private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
814
815        /**
816         * Called when the input method default action key is pressed.
817         */
818        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
819            onSubmitQuery();
820            return true;
821        }
822    };
823
824    private void onTextChanged(CharSequence newText) {
825        CharSequence text = mQueryTextView.getText();
826        boolean hasText = !TextUtils.isEmpty(text);
827        if (isSubmitButtonEnabled()) {
828            mSubmitButton.setVisibility(hasText ? VISIBLE : GONE);
829            requestLayout();
830            invalidate();
831        }
832        updateVoiceButton(!hasText);
833        updateCloseButton();
834        if (mOnQueryChangeListener != null) {
835            mOnQueryChangeListener.onQueryTextChanged(newText.toString());
836        }
837    }
838
839    private void onSubmitQuery() {
840        CharSequence query = mQueryTextView.getText();
841        if (!TextUtils.isEmpty(query)) {
842            if (mOnQueryChangeListener == null
843                    || !mOnQueryChangeListener.onSubmitQuery(query.toString())) {
844                if (mSearchable != null) {
845                    launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
846                    setImeVisibility(false);
847                }
848                dismissSuggestions();
849            }
850        }
851    }
852
853    private void dismissSuggestions() {
854        mQueryTextView.dismissDropDown();
855    }
856
857    private void onCloseClicked() {
858        if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
859            CharSequence text = mQueryTextView.getText();
860            if (TextUtils.isEmpty(text)) {
861                // query field already empty, hide the keyboard and remove focus
862                clearFocus();
863                setImeVisibility(false);
864            } else {
865                mQueryTextView.setText("");
866            }
867            updateViewsVisibility(mIconifiedByDefault);
868            if (mIconifiedByDefault) setImeVisibility(false);
869        }
870    }
871
872    private void onSearchClicked() {
873        mQueryTextView.requestFocus();
874        updateViewsVisibility(false);
875        setImeVisibility(true);
876        if (mOnSearchClickListener != null) {
877            mOnSearchClickListener.onClick(this);
878        }
879    }
880
881    private void onVoiceClicked() {
882        // guard against possible race conditions
883        if (mSearchable == null) {
884            return;
885        }
886        SearchableInfo searchable = mSearchable;
887        try {
888            if (searchable.getVoiceSearchLaunchWebSearch()) {
889                Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
890                        searchable);
891                getContext().startActivity(webSearchIntent);
892            } else if (searchable.getVoiceSearchLaunchRecognizer()) {
893                Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
894                        searchable);
895                getContext().startActivity(appSearchIntent);
896            }
897        } catch (ActivityNotFoundException e) {
898            // Should not happen, since we check the availability of
899            // voice search before showing the button. But just in case...
900            Log.w(LOG_TAG, "Could not find voice search activity");
901        }
902    }
903
904    void onTextFocusChanged() {
905        updateCloseButton();
906    }
907
908    private boolean onItemClicked(int position, int actionKey, String actionMsg) {
909        if (mOnSuggestionListener == null
910                || !mOnSuggestionListener.onSuggestionClicked(position)) {
911            launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
912            setImeVisibility(false);
913            dismissSuggestions();
914            return true;
915        }
916        return false;
917    }
918
919    private boolean onItemSelected(int position) {
920        if (mOnSuggestionListener == null
921                || !mOnSuggestionListener.onSuggestionSelected(position)) {
922            rewriteQueryFromSuggestion(position);
923            return true;
924        }
925        return false;
926    }
927
928    private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
929
930        /**
931         * Implements OnItemClickListener
932         */
933        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
934            if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
935            onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
936        }
937    };
938
939    private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
940
941        /**
942         * Implements OnItemSelectedListener
943         */
944        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
945            if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
946            SearchView.this.onItemSelected(position);
947        }
948
949        /**
950         * Implements OnItemSelectedListener
951         */
952        public void onNothingSelected(AdapterView<?> parent) {
953            if (DBG)
954                Log.d(LOG_TAG, "onNothingSelected()");
955        }
956    };
957
958    /**
959     * Query rewriting.
960     */
961    private void rewriteQueryFromSuggestion(int position) {
962        CharSequence oldQuery = mQueryTextView.getText();
963        Cursor c = mSuggestionsAdapter.getCursor();
964        if (c == null) {
965            return;
966        }
967        if (c.moveToPosition(position)) {
968            // Get the new query from the suggestion.
969            CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
970            if (newQuery != null) {
971                // The suggestion rewrites the query.
972                // Update the text field, without getting new suggestions.
973                setQuery(newQuery);
974            } else {
975                // The suggestion does not rewrite the query, restore the user's query.
976                setQuery(oldQuery);
977            }
978        } else {
979            // We got a bad position, restore the user's query.
980            setQuery(oldQuery);
981        }
982    }
983
984    /**
985     * Launches an intent based on a suggestion.
986     *
987     * @param position The index of the suggestion to create the intent from.
988     * @param actionKey The key code of the action key that was pressed,
989     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
990     * @param actionMsg The message for the action key that was pressed,
991     *        or <code>null</code> if none.
992     * @return true if a successful launch, false if could not (e.g. bad position).
993     */
994    private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
995        Cursor c = mSuggestionsAdapter.getCursor();
996        if ((c != null) && c.moveToPosition(position)) {
997
998            Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
999
1000            // launch the intent
1001            launchIntent(intent);
1002
1003            return true;
1004        }
1005        return false;
1006    }
1007
1008    /**
1009     * Launches an intent, including any special intent handling.
1010     */
1011    private void launchIntent(Intent intent) {
1012        if (intent == null) {
1013            return;
1014        }
1015        try {
1016            // If the intent was created from a suggestion, it will always have an explicit
1017            // component here.
1018            getContext().startActivity(intent);
1019        } catch (RuntimeException ex) {
1020            Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1021        }
1022    }
1023
1024    /**
1025     * Sets the text in the query box, without updating the suggestions.
1026     */
1027    private void setQuery(CharSequence query) {
1028        mQueryTextView.setText(query, true);
1029        // Move the cursor to the end
1030        mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
1031    }
1032
1033    private void launchQuerySearch(int actionKey, String actionMsg, String query) {
1034        String action = Intent.ACTION_SEARCH;
1035        Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
1036        getContext().startActivity(intent);
1037    }
1038
1039    /**
1040     * Constructs an intent from the given information and the search dialog state.
1041     *
1042     * @param action Intent action.
1043     * @param data Intent data, or <code>null</code>.
1044     * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1045     * @param query Intent query, or <code>null</code>.
1046     * @param actionKey The key code of the action key that was pressed,
1047     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1048     * @param actionMsg The message for the action key that was pressed,
1049     *        or <code>null</code> if none.
1050     * @param mode The search mode, one of the acceptable values for
1051     *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
1052     * @return The intent.
1053     */
1054    private Intent createIntent(String action, Uri data, String extraData, String query,
1055            int actionKey, String actionMsg) {
1056        // Now build the Intent
1057        Intent intent = new Intent(action);
1058        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1059        // We need CLEAR_TOP to avoid reusing an old task that has other activities
1060        // on top of the one we want. We don't want to do this in in-app search though,
1061        // as it can be destructive to the activity stack.
1062        if (data != null) {
1063            intent.setData(data);
1064        }
1065        intent.putExtra(SearchManager.USER_QUERY, query);
1066        if (query != null) {
1067            intent.putExtra(SearchManager.QUERY, query);
1068        }
1069        if (extraData != null) {
1070            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1071        }
1072        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1073            intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1074            intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1075        }
1076        intent.setComponent(mSearchable.getSearchActivity());
1077        return intent;
1078    }
1079
1080    /**
1081     * Create and return an Intent that can launch the voice search activity for web search.
1082     */
1083    private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1084        Intent voiceIntent = new Intent(baseIntent);
1085        ComponentName searchActivity = searchable.getSearchActivity();
1086        voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1087                : searchActivity.flattenToShortString());
1088        return voiceIntent;
1089    }
1090
1091    /**
1092     * Create and return an Intent that can launch the voice search activity, perform a specific
1093     * voice transcription, and forward the results to the searchable activity.
1094     *
1095     * @param baseIntent The voice app search intent to start from
1096     * @return A completely-configured intent ready to send to the voice search activity
1097     */
1098    private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1099        ComponentName searchActivity = searchable.getSearchActivity();
1100
1101        // create the necessary intent to set up a search-and-forward operation
1102        // in the voice search system.   We have to keep the bundle separate,
1103        // because it becomes immutable once it enters the PendingIntent
1104        Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
1105        queryIntent.setComponent(searchActivity);
1106        PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
1107                PendingIntent.FLAG_ONE_SHOT);
1108
1109        // Now set up the bundle that will be inserted into the pending intent
1110        // when it's time to do the search.  We always build it here (even if empty)
1111        // because the voice search activity will always need to insert "QUERY" into
1112        // it anyway.
1113        Bundle queryExtras = new Bundle();
1114
1115        // Now build the intent to launch the voice search.  Add all necessary
1116        // extras to launch the voice recognizer, and then all the necessary extras
1117        // to forward the results to the searchable activity
1118        Intent voiceIntent = new Intent(baseIntent);
1119
1120        // Add all of the configuration options supplied by the searchable's metadata
1121        String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
1122        String prompt = null;
1123        String language = null;
1124        int maxResults = 1;
1125
1126        Resources resources = getResources();
1127        if (searchable.getVoiceLanguageModeId() != 0) {
1128            languageModel = resources.getString(searchable.getVoiceLanguageModeId());
1129        }
1130        if (searchable.getVoicePromptTextId() != 0) {
1131            prompt = resources.getString(searchable.getVoicePromptTextId());
1132        }
1133        if (searchable.getVoiceLanguageId() != 0) {
1134            language = resources.getString(searchable.getVoiceLanguageId());
1135        }
1136        if (searchable.getVoiceMaxResults() != 0) {
1137            maxResults = searchable.getVoiceMaxResults();
1138        }
1139        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
1140        voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
1141        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
1142        voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
1143        voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1144                : searchActivity.flattenToShortString());
1145
1146        // Add the values that configure forwarding the results
1147        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
1148        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
1149
1150        return voiceIntent;
1151    }
1152
1153    /**
1154     * When a particular suggestion has been selected, perform the various lookups required
1155     * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
1156     * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
1157     * the suggestion includes a data id.
1158     *
1159     * @param c The suggestions cursor, moved to the row of the user's selection
1160     * @param actionKey The key code of the action key that was pressed,
1161     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1162     * @param actionMsg The message for the action key that was pressed,
1163     *        or <code>null</code> if none.
1164     * @return An intent for the suggestion at the cursor's position.
1165     */
1166    private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1167        try {
1168            // use specific action if supplied, or default action if supplied, or fixed default
1169            String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1170
1171            if (action == null) {
1172                action = mSearchable.getSuggestIntentAction();
1173            }
1174            if (action == null) {
1175                action = Intent.ACTION_SEARCH;
1176            }
1177
1178            // use specific data if supplied, or default data if supplied
1179            String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1180            if (data == null) {
1181                data = mSearchable.getSuggestIntentData();
1182            }
1183            // then, if an ID was provided, append it.
1184            if (data != null) {
1185                String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1186                if (id != null) {
1187                    data = data + "/" + Uri.encode(id);
1188                }
1189            }
1190            Uri dataUri = (data == null) ? null : Uri.parse(data);
1191
1192            String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1193            String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1194
1195            return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
1196        } catch (RuntimeException e ) {
1197            int rowNum;
1198            try {                       // be really paranoid now
1199                rowNum = c.getPosition();
1200            } catch (RuntimeException e2 ) {
1201                rowNum = -1;
1202            }
1203            Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
1204                            " returned exception" + e.toString());
1205            return null;
1206        }
1207    }
1208
1209    static boolean isLandscapeMode(Context context) {
1210        return context.getResources().getConfiguration().orientation
1211                == Configuration.ORIENTATION_LANDSCAPE;
1212    }
1213
1214    /**
1215     * Callback to watch the text field for empty/non-empty
1216     */
1217    private TextWatcher mTextWatcher = new TextWatcher() {
1218
1219        public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
1220
1221        public void onTextChanged(CharSequence s, int start,
1222                int before, int after) {
1223            SearchView.this.onTextChanged(s);
1224        }
1225
1226        public void afterTextChanged(Editable s) {
1227        }
1228    };
1229
1230    /**
1231     * Local subclass for AutoCompleteTextView.
1232     * @hide
1233     */
1234    public static class SearchAutoComplete extends AutoCompleteTextView {
1235
1236        private int mThreshold;
1237        private SearchView mSearchView;
1238
1239        public SearchAutoComplete(Context context) {
1240            super(context);
1241            mThreshold = getThreshold();
1242        }
1243
1244        public SearchAutoComplete(Context context, AttributeSet attrs) {
1245            super(context, attrs);
1246            mThreshold = getThreshold();
1247        }
1248
1249        public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1250            super(context, attrs, defStyle);
1251            mThreshold = getThreshold();
1252        }
1253
1254        void setSearchView(SearchView searchView) {
1255            mSearchView = searchView;
1256        }
1257
1258        @Override
1259        public void setThreshold(int threshold) {
1260            super.setThreshold(threshold);
1261            mThreshold = threshold;
1262        }
1263
1264        /**
1265         * Returns true if the text field is empty, or contains only whitespace.
1266         */
1267        private boolean isEmpty() {
1268            return TextUtils.getTrimmedLength(getText()) == 0;
1269        }
1270
1271        /**
1272         * We override this method to avoid replacing the query box text when a
1273         * suggestion is clicked.
1274         */
1275        @Override
1276        protected void replaceText(CharSequence text) {
1277        }
1278
1279        /**
1280         * We override this method to avoid an extra onItemClick being called on
1281         * the drop-down's OnItemClickListener by
1282         * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
1283         * clicked with the trackball.
1284         */
1285        @Override
1286        public void performCompletion() {
1287        }
1288
1289        /**
1290         * We override this method to be sure and show the soft keyboard if
1291         * appropriate when the TextView has focus.
1292         */
1293        @Override
1294        public void onWindowFocusChanged(boolean hasWindowFocus) {
1295            super.onWindowFocusChanged(hasWindowFocus);
1296
1297            if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
1298                InputMethodManager inputManager = (InputMethodManager) getContext()
1299                        .getSystemService(Context.INPUT_METHOD_SERVICE);
1300                inputManager.showSoftInput(this, 0);
1301                // If in landscape mode, then make sure that
1302                // the ime is in front of the dropdown.
1303                if (isLandscapeMode(getContext())) {
1304                    ensureImeVisible(true);
1305                }
1306            }
1307        }
1308
1309        @Override
1310        protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1311            super.onFocusChanged(focused, direction, previouslyFocusedRect);
1312            mSearchView.onTextFocusChanged();
1313        }
1314
1315        /**
1316         * We override this method so that we can allow a threshold of zero,
1317         * which ACTV does not.
1318         */
1319        @Override
1320        public boolean enoughToFilter() {
1321            return mThreshold <= 0 || super.enoughToFilter();
1322        }
1323    }
1324}
1325