SearchDialog.java revision 246529891ee289e8393ad4a486db785ef455c778
1/*
2 * Copyright (C) 2008 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.app;
18
19
20import android.content.BroadcastReceiver;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.pm.ActivityInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.res.Configuration;
29import android.graphics.drawable.Drawable;
30import android.net.Uri;
31import android.os.Bundle;
32import android.speech.RecognizerIntent;
33import android.text.InputType;
34import android.text.TextUtils;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.util.TypedValue;
38import android.view.ActionMode;
39import android.view.Gravity;
40import android.view.KeyEvent;
41import android.view.MotionEvent;
42import android.view.View;
43import android.view.ViewConfiguration;
44import android.view.ViewGroup;
45import android.view.Window;
46import android.view.WindowManager;
47import android.view.inputmethod.InputMethodManager;
48import android.widget.AutoCompleteTextView;
49import android.widget.ImageView;
50import android.widget.LinearLayout;
51import android.widget.SearchView;
52import android.widget.TextView;
53
54/**
55 * Search dialog. This is controlled by the
56 * SearchManager and runs in the current foreground process.
57 *
58 * @hide
59 */
60public class SearchDialog extends Dialog {
61
62    // Debugging support
63    private static final boolean DBG = false;
64    private static final String LOG_TAG = "SearchDialog";
65
66    private static final String INSTANCE_KEY_COMPONENT = "comp";
67    private static final String INSTANCE_KEY_APPDATA = "data";
68    private static final String INSTANCE_KEY_USER_QUERY = "uQry";
69
70    // The string used for privateImeOptions to identify to the IME that it should not show
71    // a microphone button since one already exists in the search dialog.
72    private static final String IME_OPTION_NO_MICROPHONE = "nm";
73
74    private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
75
76    // views & widgets
77    private TextView mBadgeLabel;
78    private ImageView mAppIcon;
79    private AutoCompleteTextView mSearchAutoComplete;
80    private View mSearchPlate;
81    private SearchView mSearchView;
82    private Drawable mWorkingSpinner;
83    private View mCloseSearch;
84
85    // interaction with searchable application
86    private SearchableInfo mSearchable;
87    private ComponentName mLaunchComponent;
88    private Bundle mAppSearchData;
89    private Context mActivityContext;
90
91    // For voice searching
92    private final Intent mVoiceWebSearchIntent;
93    private final Intent mVoiceAppSearchIntent;
94
95    // The query entered by the user. This is not changed when selecting a suggestion
96    // that modifies the contents of the text field. But if the user then edits
97    // the suggestion, the resulting string is saved.
98    private String mUserQuery;
99
100    // Last known IME options value for the search edit text.
101    private int mSearchAutoCompleteImeOptions;
102
103    private BroadcastReceiver mConfChangeListener = new BroadcastReceiver() {
104        @Override
105        public void onReceive(Context context, Intent intent) {
106            if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
107                onConfigurationChanged();
108            }
109        }
110    };
111
112    static int resolveDialogTheme(Context context) {
113        TypedValue outValue = new TypedValue();
114        context.getTheme().resolveAttribute(com.android.internal.R.attr.searchDialogTheme,
115                outValue, true);
116        return outValue.resourceId;
117    }
118
119    /**
120     * Constructor - fires it up and makes it look like the search UI.
121     *
122     * @param context Application Context we can use for system acess
123     */
124    public SearchDialog(Context context, SearchManager searchManager) {
125        super(context, resolveDialogTheme(context));
126
127        // Save voice intent for later queries/launching
128        mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
129        mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
130        mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
131                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
132
133        mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
134        mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
135    }
136
137    /**
138     * Create the search dialog and any resources that are used for the
139     * entire lifetime of the dialog.
140     */
141    @Override
142    protected void onCreate(Bundle savedInstanceState) {
143        super.onCreate(savedInstanceState);
144
145        Window theWindow = getWindow();
146        WindowManager.LayoutParams lp = theWindow.getAttributes();
147        lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
148        // taking up the whole window (even when transparent) is less than ideal,
149        // but necessary to show the popup window until the window manager supports
150        // having windows anchored by their parent but not clipped by them.
151        lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
152        lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
153        lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
154        theWindow.setAttributes(lp);
155
156        // Touching outside of the search dialog will dismiss it
157        setCanceledOnTouchOutside(true);
158    }
159
160    /**
161     * We recreate the dialog view each time it becomes visible so as to limit
162     * the scope of any problems with the contained resources.
163     */
164    private void createContentView() {
165        setContentView(com.android.internal.R.layout.search_bar);
166
167        // get the view elements for local access
168        SearchBar searchBar = (SearchBar) findViewById(com.android.internal.R.id.search_bar);
169        searchBar.setSearchDialog(this);
170        mSearchView = (SearchView) findViewById(com.android.internal.R.id.search_view);
171        mSearchView.setOnCloseListener(mOnCloseListener);
172        mSearchView.setOnQueryTextListener(mOnQueryChangeListener);
173        mSearchView.setOnSuggestionListener(mOnSuggestionSelectionListener);
174
175        mCloseSearch = findViewById(com.android.internal.R.id.closeButton);
176        mCloseSearch.setOnClickListener(new View.OnClickListener() {
177            @Override
178            public void onClick(View v) {
179                dismiss();
180            }
181        });
182
183        // TODO: Move the badge logic to SearchView or move the badge to search_bar.xml
184        mBadgeLabel = (TextView) mSearchView.findViewById(com.android.internal.R.id.search_badge);
185        mSearchAutoComplete = (AutoCompleteTextView)
186                mSearchView.findViewById(com.android.internal.R.id.search_src_text);
187        mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon);
188        mSearchPlate = mSearchView.findViewById(com.android.internal.R.id.search_plate);
189        mWorkingSpinner = getContext().getResources().
190                getDrawable(com.android.internal.R.drawable.search_spinner);
191        mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
192                null, null, mWorkingSpinner, null);
193        setWorking(false);
194
195        // pre-hide all the extraneous elements
196        mBadgeLabel.setVisibility(View.GONE);
197
198        // Additional adjustments to make Dialog work for Search
199        mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
200    }
201
202    /**
203     * Set up the search dialog
204     *
205     * @return true if search dialog launched, false if not
206     */
207    public boolean show(String initialQuery, boolean selectInitialQuery,
208            ComponentName componentName, Bundle appSearchData) {
209        boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData);
210        if (success) {
211            // Display the drop down as soon as possible instead of waiting for the rest of the
212            // pending UI stuff to get done, so that things appear faster to the user.
213            mSearchAutoComplete.showDropDownAfterLayout();
214        }
215        return success;
216    }
217
218    /**
219     * Does the rest of the work required to show the search dialog. Called by
220     * {@link #show(String, boolean, ComponentName, Bundle)} and
221     *
222     * @return true if search dialog showed, false if not
223     */
224    private boolean doShow(String initialQuery, boolean selectInitialQuery,
225            ComponentName componentName, Bundle appSearchData) {
226        // set up the searchable and show the dialog
227        if (!show(componentName, appSearchData)) {
228            return false;
229        }
230
231        // finally, load the user's initial text (which may trigger suggestions)
232        setUserQuery(initialQuery);
233        if (selectInitialQuery) {
234            mSearchAutoComplete.selectAll();
235        }
236
237        return true;
238    }
239
240    /**
241     * Sets up the search dialog and shows it.
242     *
243     * @return <code>true</code> if search dialog launched
244     */
245    private boolean show(ComponentName componentName, Bundle appSearchData) {
246
247        if (DBG) {
248            Log.d(LOG_TAG, "show(" + componentName + ", "
249                    + appSearchData + ")");
250        }
251
252        SearchManager searchManager = (SearchManager)
253                mContext.getSystemService(Context.SEARCH_SERVICE);
254        // Try to get the searchable info for the provided component.
255        mSearchable = searchManager.getSearchableInfo(componentName);
256
257        if (mSearchable == null) {
258            return false;
259        }
260
261        mLaunchComponent = componentName;
262        mAppSearchData = appSearchData;
263        mActivityContext = mSearchable.getActivityContext(getContext());
264
265        // show the dialog. this will call onStart().
266        if (!isShowing()) {
267            // Recreate the search bar view every time the dialog is shown, to get rid
268            // of any bad state in the AutoCompleteTextView etc
269            createContentView();
270            mSearchView.setSearchableInfo(mSearchable);
271            mSearchView.setAppSearchData(mAppSearchData);
272
273            show();
274        }
275        updateUI();
276
277        return true;
278    }
279
280    @Override
281    public void onStart() {
282        super.onStart();
283
284        // Register a listener for configuration change events.
285        IntentFilter filter = new IntentFilter();
286        filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
287        getContext().registerReceiver(mConfChangeListener, filter);
288    }
289
290    /**
291     * The search dialog is being dismissed, so handle all of the local shutdown operations.
292     *
293     * This function is designed to be idempotent so that dismiss() can be safely called at any time
294     * (even if already closed) and more likely to really dump any memory.  No leaks!
295     */
296    @Override
297    public void onStop() {
298        super.onStop();
299
300        getContext().unregisterReceiver(mConfChangeListener);
301
302        // dump extra memory we're hanging on to
303        mLaunchComponent = null;
304        mAppSearchData = null;
305        mSearchable = null;
306        mUserQuery = null;
307    }
308
309    /**
310     * Sets the search dialog to the 'working' state, which shows a working spinner in the
311     * right hand size of the text field.
312     *
313     * @param working true to show spinner, false to hide spinner
314     */
315    public void setWorking(boolean working) {
316        mWorkingSpinner.setAlpha(working ? 255 : 0);
317        mWorkingSpinner.setVisible(working, false);
318        mWorkingSpinner.invalidateSelf();
319    }
320
321    /**
322     * Save the minimal set of data necessary to recreate the search
323     *
324     * @return A bundle with the state of the dialog, or {@code null} if the search
325     *         dialog is not showing.
326     */
327    @Override
328    public Bundle onSaveInstanceState() {
329        if (!isShowing()) return null;
330
331        Bundle bundle = new Bundle();
332
333        // setup info so I can recreate this particular search
334        bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
335        bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
336        bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
337
338        return bundle;
339    }
340
341    /**
342     * Restore the state of the dialog from a previously saved bundle.
343     *
344     * @param savedInstanceState The state of the dialog previously saved by
345     *     {@link #onSaveInstanceState()}.
346     */
347    @Override
348    public void onRestoreInstanceState(Bundle savedInstanceState) {
349        if (savedInstanceState == null) return;
350
351        ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
352        Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
353        String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
354
355        // show the dialog.
356        if (!doShow(userQuery, false, launchComponent, appSearchData)) {
357            // for some reason, we couldn't re-instantiate
358            return;
359        }
360    }
361
362    /**
363     * Called after resources have changed, e.g. after screen rotation or locale change.
364     */
365    public void onConfigurationChanged() {
366        if (mSearchable != null && isShowing()) {
367            // Redraw (resources may have changed)
368            updateSearchAppIcon();
369            updateSearchBadge();
370            if (isLandscapeMode(getContext())) {
371                mSearchAutoComplete.ensureImeVisible(true);
372            }
373        }
374    }
375
376    static boolean isLandscapeMode(Context context) {
377        return context.getResources().getConfiguration().orientation
378                == Configuration.ORIENTATION_LANDSCAPE;
379    }
380
381    /**
382     * Update the UI according to the info in the current value of {@link #mSearchable}.
383     */
384    private void updateUI() {
385        if (mSearchable != null) {
386            mDecor.setVisibility(View.VISIBLE);
387            updateSearchAutoComplete();
388            updateSearchAppIcon();
389            updateSearchBadge();
390
391            // In order to properly configure the input method (if one is being used), we
392            // need to let it know if we'll be providing suggestions.  Although it would be
393            // difficult/expensive to know if every last detail has been configured properly, we
394            // can at least see if a suggestions provider has been configured, and use that
395            // as our trigger.
396            int inputType = mSearchable.getInputType();
397            // We only touch this if the input type is set up for text (which it almost certainly
398            // should be, in the case of search!)
399            if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
400                // The existence of a suggestions authority is the proxy for "suggestions
401                // are available here"
402                inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
403                if (mSearchable.getSuggestAuthority() != null) {
404                    inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
405                }
406            }
407            mSearchAutoComplete.setInputType(inputType);
408            mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
409            mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
410
411            // If the search dialog is going to show a voice search button, then don't let
412            // the soft keyboard display a microphone button if it would have otherwise.
413            if (mSearchable.getVoiceSearchEnabled()) {
414                mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
415            } else {
416                mSearchAutoComplete.setPrivateImeOptions(null);
417            }
418        }
419    }
420
421    /**
422     * Updates the auto-complete text view.
423     */
424    private void updateSearchAutoComplete() {
425        // we dismiss the entire dialog instead
426        mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
427        mSearchAutoComplete.setForceIgnoreOutsideTouch(false);
428    }
429
430    private void updateSearchAppIcon() {
431        PackageManager pm = getContext().getPackageManager();
432        Drawable icon;
433        try {
434            ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
435            icon = pm.getApplicationIcon(info.applicationInfo);
436            if (DBG)
437                Log.d(LOG_TAG, "Using app-specific icon");
438        } catch (NameNotFoundException e) {
439            icon = pm.getDefaultActivityIcon();
440            Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
441        }
442        mAppIcon.setImageDrawable(icon);
443        mAppIcon.setVisibility(View.VISIBLE);
444        mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, mSearchPlate.getPaddingTop(), mSearchPlate.getPaddingRight(), mSearchPlate.getPaddingBottom());
445    }
446
447    /**
448     * Setup the search "Badge" if requested by mode flags.
449     */
450    private void updateSearchBadge() {
451        // assume both hidden
452        int visibility = View.GONE;
453        Drawable icon = null;
454        CharSequence text = null;
455
456        // optionally show one or the other.
457        if (mSearchable.useBadgeIcon()) {
458            icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
459            visibility = View.VISIBLE;
460            if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
461        } else if (mSearchable.useBadgeLabel()) {
462            text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
463            visibility = View.VISIBLE;
464            if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
465        }
466
467        mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
468        mBadgeLabel.setText(text);
469        mBadgeLabel.setVisibility(visibility);
470    }
471
472    /*
473     * Listeners of various types
474     */
475
476    /**
477     * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
478     * touch is outside the window. But the window includes space for the drop-down,
479     * so we also cancel on taps outside the search bar when the drop-down is not showing.
480     */
481    @Override
482    public boolean onTouchEvent(MotionEvent event) {
483        // cancel if the drop-down is not showing and the touch event was outside the search plate
484        if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
485            if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
486            cancel();
487            return true;
488        }
489        // Let Dialog handle events outside the window while the pop-up is showing.
490        return super.onTouchEvent(event);
491    }
492
493    private boolean isOutOfBounds(View v, MotionEvent event) {
494        final int x = (int) event.getX();
495        final int y = (int) event.getY();
496        final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
497        return (x < -slop) || (y < -slop)
498                || (x > (v.getWidth()+slop))
499                || (y > (v.getHeight()+slop));
500    }
501
502    @Override
503    public void hide() {
504        if (!isShowing()) return;
505
506        // We made sure the IME was displayed, so also make sure it is closed
507        // when we go away.
508        InputMethodManager imm = (InputMethodManager)getContext()
509                .getSystemService(Context.INPUT_METHOD_SERVICE);
510        if (imm != null) {
511            imm.hideSoftInputFromWindow(
512                    getWindow().getDecorView().getWindowToken(), 0);
513        }
514
515        super.hide();
516    }
517
518    /**
519     * Launch a search for the text in the query text field.
520     */
521    public void launchQuerySearch() {
522        launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
523    }
524
525    /**
526     * Launch a search for the text in the query text field.
527     *
528     * @param actionKey The key code of the action key that was pressed,
529     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
530     * @param actionMsg The message for the action key that was pressed,
531     *        or <code>null</code> if none.
532     */
533    protected void launchQuerySearch(int actionKey, String actionMsg) {
534        String query = mSearchAutoComplete.getText().toString();
535        String action = Intent.ACTION_SEARCH;
536        Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
537        launchIntent(intent);
538    }
539
540    /**
541     * Launches an intent, including any special intent handling.
542     */
543    private void launchIntent(Intent intent) {
544        if (intent == null) {
545            return;
546        }
547        Log.d(LOG_TAG, "launching " + intent);
548        try {
549            // If the intent was created from a suggestion, it will always have an explicit
550            // component here.
551            Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toUri(0));
552            getContext().startActivity(intent);
553            // If the search switches to a different activity,
554            // SearchDialogWrapper#performActivityResuming
555            // will handle hiding the dialog when the next activity starts, but for
556            // real in-app search, we still need to dismiss the dialog.
557            dismiss();
558        } catch (RuntimeException ex) {
559            Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
560        }
561    }
562
563    /**
564     * Sets the list item selection in the AutoCompleteTextView's ListView.
565     */
566    public void setListSelection(int index) {
567        mSearchAutoComplete.setListSelection(index);
568    }
569
570    /**
571     * Constructs an intent from the given information and the search dialog state.
572     *
573     * @param action Intent action.
574     * @param data Intent data, or <code>null</code>.
575     * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
576     * @param query Intent query, or <code>null</code>.
577     * @param actionKey The key code of the action key that was pressed,
578     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
579     * @param actionMsg The message for the action key that was pressed,
580     *        or <code>null</code> if none.
581     * @param mode The search mode, one of the acceptable values for
582     *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
583     * @return The intent.
584     */
585    private Intent createIntent(String action, Uri data, String extraData, String query,
586            int actionKey, String actionMsg) {
587        // Now build the Intent
588        Intent intent = new Intent(action);
589        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
590        // We need CLEAR_TOP to avoid reusing an old task that has other activities
591        // on top of the one we want. We don't want to do this in in-app search though,
592        // as it can be destructive to the activity stack.
593        if (data != null) {
594            intent.setData(data);
595        }
596        intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
597        if (query != null) {
598            intent.putExtra(SearchManager.QUERY, query);
599        }
600        if (extraData != null) {
601            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
602        }
603        if (mAppSearchData != null) {
604            intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
605        }
606        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
607            intent.putExtra(SearchManager.ACTION_KEY, actionKey);
608            intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
609        }
610        intent.setComponent(mSearchable.getSearchActivity());
611        return intent;
612    }
613
614    /**
615     * The root element in the search bar layout. This is a custom view just to override
616     * the handling of the back button.
617     */
618    public static class SearchBar extends LinearLayout {
619
620        private SearchDialog mSearchDialog;
621
622        public SearchBar(Context context, AttributeSet attrs) {
623            super(context, attrs);
624        }
625
626        public SearchBar(Context context) {
627            super(context);
628        }
629
630        public void setSearchDialog(SearchDialog searchDialog) {
631            mSearchDialog = searchDialog;
632        }
633
634        /**
635         * Overrides the handling of the back key to move back to the previous
636         * sources or dismiss the search dialog, instead of dismissing the input
637         * method.
638         */
639        @Override
640        public boolean dispatchKeyEventPreIme(KeyEvent event) {
641            if (DBG)
642                Log.d(LOG_TAG, "onKeyPreIme(" + event + ")");
643            if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
644                KeyEvent.DispatcherState state = getKeyDispatcherState();
645                if (state != null) {
646                    if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
647                        state.startTracking(event, this);
648                        return true;
649                    } else if (event.getAction() == KeyEvent.ACTION_UP && !event.isCanceled()
650                            && state.isTracking(event)) {
651                        mSearchDialog.onBackPressed();
652                        return true;
653                    }
654                }
655            }
656            return super.dispatchKeyEventPreIme(event);
657        }
658
659        /**
660         * Don't allow action modes in a SearchBar, it looks silly.
661         */
662        @Override
663        public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) {
664            return null;
665        }
666    }
667
668    private boolean isEmpty(AutoCompleteTextView actv) {
669        return TextUtils.getTrimmedLength(actv.getText()) == 0;
670    }
671
672    @Override
673    public void onBackPressed() {
674        // If the input method is covering the search dialog completely,
675        // e.g. in landscape mode with no hard keyboard, dismiss just the input method
676        InputMethodManager imm = (InputMethodManager)getContext()
677                .getSystemService(Context.INPUT_METHOD_SERVICE);
678        if (imm != null && imm.isFullscreenMode() &&
679                imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) {
680            return;
681        }
682        // Close search dialog
683        cancel();
684    }
685
686    private boolean onClosePressed() {
687        // Dismiss the dialog if close button is pressed when there's no query text
688        if (isEmpty(mSearchAutoComplete)) {
689            dismiss();
690            return true;
691        }
692
693        return false;
694    }
695
696    private final SearchView.OnCloseListener mOnCloseListener = new SearchView.OnCloseListener() {
697
698        public boolean onClose() {
699            return onClosePressed();
700        }
701    };
702
703    private final SearchView.OnQueryTextListener mOnQueryChangeListener =
704            new SearchView.OnQueryTextListener() {
705
706        public boolean onQueryTextSubmit(String query) {
707            dismiss();
708            return false;
709        }
710
711        public boolean onQueryTextChange(String newText) {
712            return false;
713        }
714    };
715
716    private final SearchView.OnSuggestionListener mOnSuggestionSelectionListener =
717            new SearchView.OnSuggestionListener() {
718
719        public boolean onSuggestionSelect(int position) {
720            return false;
721        }
722
723        public boolean onSuggestionClick(int position) {
724            dismiss();
725            return false;
726        }
727    };
728
729    /**
730     * Sets the text in the query box, updating the suggestions.
731     */
732    private void setUserQuery(String query) {
733        if (query == null) {
734            query = "";
735        }
736        mUserQuery = query;
737        mSearchAutoComplete.setText(query);
738        mSearchAutoComplete.setSelection(query.length());
739    }
740}
741