SearchDialog.java revision 725d8ff6c5b4e27c8b7f2969ce1ddf6ec715ea6d
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
19import static android.app.SuggestionsAdapter.getColumnString;
20
21import android.content.ActivityNotFoundException;
22import android.content.ComponentName;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.content.pm.ActivityInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.content.pm.PackageManager.NameNotFoundException;
31import android.content.res.Resources;
32import android.database.Cursor;
33import android.graphics.drawable.Animatable;
34import android.graphics.drawable.Drawable;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.IBinder;
38import android.os.RemoteException;
39import android.os.SystemClock;
40import android.provider.Browser;
41import android.server.search.SearchableInfo;
42import android.speech.RecognizerIntent;
43import android.text.Editable;
44import android.text.InputType;
45import android.text.TextUtils;
46import android.text.TextWatcher;
47import android.text.util.Regex;
48import android.util.AndroidRuntimeException;
49import android.util.AttributeSet;
50import android.util.Log;
51import android.view.ContextThemeWrapper;
52import android.view.Gravity;
53import android.view.KeyEvent;
54import android.view.Menu;
55import android.view.MotionEvent;
56import android.view.View;
57import android.view.ViewConfiguration;
58import android.view.ViewGroup;
59import android.view.Window;
60import android.view.WindowManager;
61import android.view.inputmethod.EditorInfo;
62import android.view.inputmethod.InputMethodManager;
63import android.widget.AdapterView;
64import android.widget.AutoCompleteTextView;
65import android.widget.Button;
66import android.widget.ImageButton;
67import android.widget.ImageView;
68import android.widget.ListView;
69import android.widget.TextView;
70import android.widget.AdapterView.OnItemClickListener;
71import android.widget.AdapterView.OnItemSelectedListener;
72
73import java.util.ArrayList;
74import java.util.WeakHashMap;
75import java.util.concurrent.atomic.AtomicLong;
76
77/**
78 * System search dialog. This is controlled by the
79 * SearchManagerService and runs in the system process.
80 *
81 * @hide
82 */
83public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
84
85    // Debugging support
86    private static final boolean DBG = false;
87    private static final String LOG_TAG = "SearchDialog";
88    private static final boolean DBG_LOG_TIMING = false;
89
90    private static final String INSTANCE_KEY_COMPONENT = "comp";
91    private static final String INSTANCE_KEY_APPDATA = "data";
92    private static final String INSTANCE_KEY_GLOBALSEARCH = "glob";
93    private static final String INSTANCE_KEY_STORED_COMPONENT = "sComp";
94    private static final String INSTANCE_KEY_STORED_APPDATA = "sData";
95    private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev";
96    private static final String INSTANCE_KEY_USER_QUERY = "uQry";
97
98    // The extra key used in an intent to the speech recognizer for in-app voice search.
99    private static final String EXTRA_CALLING_PACKAGE = "calling_package";
100
101    private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12;
102    private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
103
104    // views & widgets
105    private TextView mBadgeLabel;
106    private ImageView mAppIcon;
107    private SearchAutoComplete mSearchAutoComplete;
108    private Button mGoButton;
109    private ImageButton mVoiceButton;
110    private View mSearchPlate;
111    private Drawable mWorkingSpinner;
112
113    // interaction with searchable application
114    private SearchableInfo mSearchable;
115    private ComponentName mLaunchComponent;
116    private Bundle mAppSearchData;
117    private boolean mGlobalSearchMode;
118    private Context mActivityContext;
119
120    // Values we store to allow user to toggle between in-app search and global search.
121    private ComponentName mStoredComponentName;
122    private Bundle mStoredAppSearchData;
123
124    // stack of previous searchables, to support the BACK key after
125    // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.
126    // The top of the stack (= previous searchable) is the last element of the list,
127    // since adding and removing is efficient at the end of an ArrayList.
128    private ArrayList<ComponentName> mPreviousComponents;
129
130    // For voice searching
131    private Intent mVoiceWebSearchIntent;
132    private Intent mVoiceAppSearchIntent;
133
134    // support for AutoCompleteTextView suggestions display
135    private SuggestionsAdapter mSuggestionsAdapter;
136
137    // Whether to rewrite queries when selecting suggestions
138    private static final boolean REWRITE_QUERIES = true;
139
140    // The query entered by the user. This is not changed when selecting a suggestion
141    // that modifies the contents of the text field. But if the user then edits
142    // the suggestion, the resulting string is saved.
143    private String mUserQuery;
144
145    // A weak map of drawables we've gotten from other packages, so we don't load them
146    // more than once.
147    private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
148            new WeakHashMap<String, Drawable.ConstantState>();
149
150    // Last known IME options value for the search edit text.
151    private int mSearchAutoCompleteImeOptions;
152
153    /**
154     * Constructor - fires it up and makes it look like the search UI.
155     *
156     * @param context Application Context we can use for system acess
157     */
158    public SearchDialog(Context context) {
159        super(context, com.android.internal.R.style.Theme_GlobalSearchBar);
160    }
161
162    /**
163     * We create the search dialog just once, and it stays around (hidden)
164     * until activated by the user.
165     */
166    @Override
167    protected void onCreate(Bundle savedInstanceState) {
168        super.onCreate(savedInstanceState);
169
170        setContentView(com.android.internal.R.layout.search_bar);
171
172        Window theWindow = getWindow();
173        WindowManager.LayoutParams lp = theWindow.getAttributes();
174        lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR;
175        lp.width = ViewGroup.LayoutParams.FILL_PARENT;
176        // taking up the whole window (even when transparent) is less than ideal,
177        // but necessary to show the popup window until the window manager supports
178        // having windows anchored by their parent but not clipped by them.
179        lp.height = ViewGroup.LayoutParams.FILL_PARENT;
180        lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
181        lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
182        theWindow.setAttributes(lp);
183
184        // get the view elements for local access
185        mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
186        mSearchAutoComplete = (SearchAutoComplete)
187                findViewById(com.android.internal.R.id.search_src_text);
188        mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon);
189        mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
190        mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
191        mSearchPlate = findViewById(com.android.internal.R.id.search_plate);
192        mWorkingSpinner = getContext().getResources().
193                getDrawable(com.android.internal.R.drawable.search_spinner);
194
195        // attach listeners
196        mSearchAutoComplete.addTextChangedListener(mTextWatcher);
197        mSearchAutoComplete.setOnKeyListener(mTextKeyListener);
198        mSearchAutoComplete.setOnItemClickListener(this);
199        mSearchAutoComplete.setOnItemSelectedListener(this);
200        mGoButton.setOnClickListener(mGoButtonClickListener);
201        mGoButton.setOnKeyListener(mButtonsKeyListener);
202        mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
203        mVoiceButton.setOnKeyListener(mButtonsKeyListener);
204
205        mSearchAutoComplete.setSearchDialog(this);
206
207        // pre-hide all the extraneous elements
208        mBadgeLabel.setVisibility(View.GONE);
209
210        // Additional adjustments to make Dialog work for Search
211
212        // Touching outside of the search dialog will dismiss it
213        setCanceledOnTouchOutside(true);
214
215        // Save voice intent for later queries/launching
216        mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
217        mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
218        mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
219                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
220
221        mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
222        mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
223
224        mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
225    }
226
227    /**
228     * Set up the search dialog
229     *
230     * @return true if search dialog launched, false if not
231     */
232    public boolean show(String initialQuery, boolean selectInitialQuery,
233            ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
234
235        // Reset any stored values from last time dialog was shown.
236        mStoredComponentName = null;
237        mStoredAppSearchData = null;
238
239        boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData,
240                globalSearch);
241        if (success) {
242            // Display the drop down as soon as possible instead of waiting for the rest of the
243            // pending UI stuff to get done, so that things appear faster to the user.
244            mSearchAutoComplete.showDropDownAfterLayout();
245        }
246        return success;
247    }
248
249    private boolean isInRealAppSearch() {
250        return !mGlobalSearchMode
251                && (mPreviousComponents == null || mPreviousComponents.isEmpty());
252    }
253
254    /**
255     * Called in response to a press of the hard search button in
256     * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app
257     * search and global search when relevant.
258     *
259     * If pressed within an in-app search context, this switches the search dialog out to
260     * global search. If pressed within a global search context that was originally an in-app
261     * search context, this switches back to the in-app search context. If pressed within a
262     * global search context that has no original in-app search context (e.g., global search
263     * from Home), this does nothing.
264     *
265     * @return false if we wanted to toggle context but could not do so successfully, true
266     * in all other cases
267     */
268    private boolean toggleGlobalSearch() {
269        String currentSearchText = mSearchAutoComplete.getText().toString();
270        if (!mGlobalSearchMode) {
271            mStoredComponentName = mLaunchComponent;
272            mStoredAppSearchData = mAppSearchData;
273            return doShow(currentSearchText, false, null, mAppSearchData, true);
274        } else {
275            if (mStoredComponentName != null) {
276                // This means we should toggle *back* to an in-app search context from
277                // global search.
278                return doShow(currentSearchText, false, mStoredComponentName,
279                        mStoredAppSearchData, false);
280            } else {
281                return true;
282            }
283        }
284    }
285
286    /**
287     * Does the rest of the work required to show the search dialog. Called by both
288     * {@link #show(String, boolean, ComponentName, Bundle, boolean)} and
289     * {@link #toggleGlobalSearch()}.
290     *
291     * @return true if search dialog showed, false if not
292     */
293    private boolean doShow(String initialQuery, boolean selectInitialQuery,
294            ComponentName componentName, Bundle appSearchData,
295            boolean globalSearch) {
296        // set up the searchable and show the dialog
297        if (!show(componentName, appSearchData, globalSearch)) {
298            return false;
299        }
300
301        // finally, load the user's initial text (which may trigger suggestions)
302        setUserQuery(initialQuery);
303        if (selectInitialQuery) {
304            mSearchAutoComplete.selectAll();
305        }
306
307        return true;
308    }
309
310    /**
311     * Sets up the search dialog and shows it.
312     *
313     * @return <code>true</code> if search dialog launched
314     */
315    private boolean show(ComponentName componentName, Bundle appSearchData,
316            boolean globalSearch) {
317
318        if (DBG) {
319            Log.d(LOG_TAG, "show(" + componentName + ", "
320                    + appSearchData + ", " + globalSearch + ")");
321        }
322
323        SearchManager searchManager = (SearchManager)
324                mContext.getSystemService(Context.SEARCH_SERVICE);
325        // Try to get the searchable info for the provided component (or for global search,
326        // if globalSearch == true).
327        mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
328
329        // If we got back nothing, and it wasn't a request for global search, then try again
330        // for global search, as we'll try to launch that in lieu of any component-specific search.
331        if (!globalSearch && mSearchable == null) {
332            globalSearch = true;
333            mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
334        }
335
336        // If there's not even a searchable info available for global search, then really give up.
337        if (mSearchable == null) {
338            Log.w(LOG_TAG, "No global search provider.");
339            return false;
340        }
341
342        mLaunchComponent = componentName;
343        mAppSearchData = appSearchData;
344        // Using globalSearch here is just an optimization, just calling
345        // isDefaultSearchable() should always give the same result.
346        mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable);
347        mActivityContext = mSearchable.getActivityContext(getContext());
348
349        // show the dialog. this will call onStart().
350        if (!isShowing()) {
351            // The Dialog uses a ContextThemeWrapper for the context; use this to change the
352            // theme out from underneath us, between the global search theme and the in-app
353            // search theme. They are identical except that the global search theme does not
354            // dim the background of the window (because global search is full screen so it's
355            // not needed and this should save a little bit of time on global search invocation).
356            Object context = getContext();
357            if (context instanceof ContextThemeWrapper) {
358                ContextThemeWrapper wrapper = (ContextThemeWrapper) context;
359                if (globalSearch) {
360                    wrapper.setTheme(com.android.internal.R.style.Theme_GlobalSearchBar);
361                } else {
362                    wrapper.setTheme(com.android.internal.R.style.Theme_SearchBar);
363                }
364            }
365            show();
366        }
367        updateUI();
368
369        return true;
370    }
371
372    /**
373     * The search dialog is being dismissed, so handle all of the local shutdown operations.
374     *
375     * This function is designed to be idempotent so that dismiss() can be safely called at any time
376     * (even if already closed) and more likely to really dump any memory.  No leaks!
377     */
378    @Override
379    public void onStop() {
380        super.onStop();
381
382        closeSuggestionsAdapter();
383
384        // dump extra memory we're hanging on to
385        mLaunchComponent = null;
386        mAppSearchData = null;
387        mSearchable = null;
388        mActivityContext = null;
389        mUserQuery = null;
390        mPreviousComponents = null;
391    }
392
393    /**
394     * Sets the search dialog to the 'working' state, which shows a working spinner in the
395     * right hand size of the text field.
396     *
397     * @param working true to show spinner, false to hide spinner
398     */
399    public void setWorking(boolean working) {
400        if (working) {
401            mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
402                    null, null, mWorkingSpinner, null);
403            ((Animatable) mWorkingSpinner).start();
404        } else {
405            mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
406                    null, null, null, null);
407            ((Animatable) mWorkingSpinner).stop();
408        }
409    }
410
411    /**
412     * Closes and gets rid of the suggestions adapter.
413     */
414    private void closeSuggestionsAdapter() {
415        // remove the adapter from the autocomplete first, to avoid any updates
416        // when we drop the cursor
417        mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
418        // close any leftover cursor
419        if (mSuggestionsAdapter != null) {
420            mSuggestionsAdapter.changeCursor(null);
421        }
422        mSuggestionsAdapter = null;
423    }
424
425    /**
426     * Save the minimal set of data necessary to recreate the search
427     *
428     * @return A bundle with the state of the dialog, or {@code null} if the search
429     *         dialog is not showing.
430     */
431    @Override
432    public Bundle onSaveInstanceState() {
433        if (!isShowing()) return null;
434
435        Bundle bundle = new Bundle();
436
437        // setup info so I can recreate this particular search
438        bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
439        bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
440        bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode);
441        bundle.putParcelable(INSTANCE_KEY_STORED_COMPONENT, mStoredComponentName);
442        bundle.putBundle(INSTANCE_KEY_STORED_APPDATA, mStoredAppSearchData);
443        bundle.putParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS, mPreviousComponents);
444        bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
445
446        return bundle;
447    }
448
449    /**
450     * Restore the state of the dialog from a previously saved bundle.
451     *
452     * TODO: go through this and make sure that it saves everything that is saved
453     *
454     * @param savedInstanceState The state of the dialog previously saved by
455     *     {@link #onSaveInstanceState()}.
456     */
457    @Override
458    public void onRestoreInstanceState(Bundle savedInstanceState) {
459        if (savedInstanceState == null) return;
460
461        ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
462        Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
463        boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
464        ComponentName storedComponentName =
465                savedInstanceState.getParcelable(INSTANCE_KEY_STORED_COMPONENT);
466        Bundle storedAppSearchData =
467                savedInstanceState.getBundle(INSTANCE_KEY_STORED_APPDATA);
468        ArrayList<ComponentName> previousComponents =
469                savedInstanceState.getParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS);
470        String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
471
472        // Set stored state
473        mStoredComponentName = storedComponentName;
474        mStoredAppSearchData = storedAppSearchData;
475        mPreviousComponents = previousComponents;
476
477        // show the dialog.
478        if (!doShow(userQuery, false, launchComponent, appSearchData, globalSearch)) {
479            // for some reason, we couldn't re-instantiate
480            return;
481        }
482    }
483
484    /**
485     * Called after resources have changed, e.g. after screen rotation or locale change.
486     */
487    public void onConfigurationChanged() {
488        if (isShowing()) {
489            // Redraw (resources may have changed)
490            updateSearchButton();
491            updateSearchAppIcon();
492            updateSearchBadge();
493            updateQueryHint();
494        }
495    }
496
497    /**
498     * Update the UI according to the info in the current value of {@link #mSearchable}.
499     */
500    private void updateUI() {
501        if (mSearchable != null) {
502            mDecor.setVisibility(View.VISIBLE);
503            updateSearchAutoComplete();
504            updateSearchButton();
505            updateSearchAppIcon();
506            updateSearchBadge();
507            updateQueryHint();
508            updateVoiceButton();
509
510            // In order to properly configure the input method (if one is being used), we
511            // need to let it know if we'll be providing suggestions.  Although it would be
512            // difficult/expensive to know if every last detail has been configured properly, we
513            // can at least see if a suggestions provider has been configured, and use that
514            // as our trigger.
515            int inputType = mSearchable.getInputType();
516            // We only touch this if the input type is set up for text (which it almost certainly
517            // should be, in the case of search!)
518            if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
519                // The existence of a suggestions authority is the proxy for "suggestions
520                // are available here"
521                inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
522                if (mSearchable.getSuggestAuthority() != null) {
523                    inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
524                }
525            }
526            mSearchAutoComplete.setInputType(inputType);
527            mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
528            mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
529        }
530    }
531
532    /**
533     * Updates the auto-complete text view.
534     */
535    private void updateSearchAutoComplete() {
536        // close any existing suggestions adapter
537        closeSuggestionsAdapter();
538
539        mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
540        mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
541        // we dismiss the entire dialog instead
542        mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
543
544        if (!isInRealAppSearch()) {
545            mSearchAutoComplete.setDropDownAlwaysVisible(true);  // fill space until results come in
546        } else {
547            mSearchAutoComplete.setDropDownAlwaysVisible(false);
548        }
549
550        mSearchAutoComplete.setForceIgnoreOutsideTouch(true);
551
552        // attach the suggestions adapter, if suggestions are available
553        // The existence of a suggestions authority is the proxy for "suggestions available here"
554        if (mSearchable.getSuggestAuthority() != null) {
555            mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable,
556                    mOutsideDrawablesCache, mGlobalSearchMode);
557            mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
558        }
559    }
560
561    /**
562     * Update the text in the search button.  Note: This is deprecated functionality, for
563     * 1.0 compatibility only.
564     */
565    private void updateSearchButton() {
566        String textLabel = null;
567        Drawable iconLabel = null;
568        int textId = mSearchable.getSearchButtonText();
569        if (textId != 0) {
570            textLabel = mActivityContext.getResources().getString(textId);
571        } else {
572            iconLabel = getContext().getResources().
573                    getDrawable(com.android.internal.R.drawable.ic_btn_search);
574        }
575        mGoButton.setText(textLabel);
576        mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
577    }
578
579    private void updateSearchAppIcon() {
580        if (mGlobalSearchMode) {
581            mAppIcon.setImageResource(0);
582            mAppIcon.setVisibility(View.GONE);
583            mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL,
584                    mSearchPlate.getPaddingTop(),
585                    mSearchPlate.getPaddingRight(),
586                    mSearchPlate.getPaddingBottom());
587        } else {
588            PackageManager pm = getContext().getPackageManager();
589            Drawable icon;
590            try {
591                ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
592                icon = pm.getApplicationIcon(info.applicationInfo);
593                if (DBG) Log.d(LOG_TAG, "Using app-specific icon");
594            } catch (NameNotFoundException e) {
595                icon = pm.getDefaultActivityIcon();
596                Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
597            }
598            mAppIcon.setImageDrawable(icon);
599            mAppIcon.setVisibility(View.VISIBLE);
600            mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL,
601                    mSearchPlate.getPaddingTop(),
602                    mSearchPlate.getPaddingRight(),
603                    mSearchPlate.getPaddingBottom());
604        }
605    }
606
607    /**
608     * Setup the search "Badge" if requested by mode flags.
609     */
610    private void updateSearchBadge() {
611        // assume both hidden
612        int visibility = View.GONE;
613        Drawable icon = null;
614        CharSequence text = null;
615
616        // optionally show one or the other.
617        if (mSearchable.useBadgeIcon()) {
618            icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
619            visibility = View.VISIBLE;
620            if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
621        } else if (mSearchable.useBadgeLabel()) {
622            text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
623            visibility = View.VISIBLE;
624            if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
625        }
626
627        mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
628        mBadgeLabel.setText(text);
629        mBadgeLabel.setVisibility(visibility);
630    }
631
632    /**
633     * Update the hint in the query text field.
634     */
635    private void updateQueryHint() {
636        if (isShowing()) {
637            String hint = null;
638            if (mSearchable != null) {
639                int hintId = mSearchable.getHintId();
640                if (hintId != 0) {
641                    hint = mActivityContext.getString(hintId);
642                }
643            }
644            mSearchAutoComplete.setHint(hint);
645        }
646    }
647
648    /**
649     * Update the visibility of the voice button.  There are actually two voice search modes,
650     * either of which will activate the button.
651     */
652    private void updateVoiceButton() {
653        int visibility = View.GONE;
654        if (mSearchable.getVoiceSearchEnabled()) {
655            Intent testIntent = null;
656            if (mSearchable.getVoiceSearchLaunchWebSearch()) {
657                testIntent = mVoiceWebSearchIntent;
658            } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
659                testIntent = mVoiceAppSearchIntent;
660            }
661            if (testIntent != null) {
662                ResolveInfo ri = getContext().getPackageManager().
663                        resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY);
664                if (ri != null) {
665                    visibility = View.VISIBLE;
666                }
667            }
668        }
669        mVoiceButton.setVisibility(visibility);
670    }
671
672    /*
673     * Menu.
674     */
675
676    @Override
677    public boolean onCreateOptionsMenu(Menu menu) {
678        // Show search settings menu item if anyone handles the intent for it
679        Intent settingsIntent = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS);
680        settingsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
681        PackageManager pm = getContext().getPackageManager();
682        ActivityInfo activityInfo = settingsIntent.resolveActivityInfo(pm, 0);
683        if (activityInfo != null) {
684            settingsIntent.setClassName(activityInfo.applicationInfo.packageName,
685                    activityInfo.name);
686            CharSequence label = activityInfo.loadLabel(getContext().getPackageManager());
687            menu.add(Menu.NONE, Menu.NONE, Menu.NONE, label)
688                    .setIcon(android.R.drawable.ic_menu_preferences)
689                    .setAlphabeticShortcut('P')
690                    .setIntent(settingsIntent);
691            return true;
692        }
693        return super.onCreateOptionsMenu(menu);
694    }
695
696    @Override
697    public boolean onMenuOpened(int featureId, Menu menu) {
698        // The menu shows up above the IME, regardless of whether it is in front
699        // of the drop-down or not. This looks weird when there is no IME, so
700        // we make sure it is visible.
701        mSearchAutoComplete.ensureImeVisible();
702        return super.onMenuOpened(featureId, menu);
703    }
704
705    /**
706     * Listeners of various types
707     */
708
709    /**
710     * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
711     * touch is outside the window. But the window includes space for the drop-down,
712     * so we also cancel on taps outside the search bar when the drop-down is not showing.
713     */
714    @Override
715    public boolean onTouchEvent(MotionEvent event) {
716        // cancel if the drop-down is not showing and the touch event was outside the search plate
717        if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
718            if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
719            cancel();
720            return true;
721        }
722        // Let Dialog handle events outside the window while the pop-up is showing.
723        return super.onTouchEvent(event);
724    }
725
726    private boolean isOutOfBounds(View v, MotionEvent event) {
727        final int x = (int) event.getX();
728        final int y = (int) event.getY();
729        final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
730        return (x < -slop) || (y < -slop)
731                || (x > (v.getWidth()+slop))
732                || (y > (v.getHeight()+slop));
733    }
734
735    /**
736     * Dialog's OnKeyListener implements various search-specific functionality
737     *
738     * @param keyCode This is the keycode of the typed key, and is the same value as
739     *        found in the KeyEvent parameter.
740     * @param event The complete event record for the typed key
741     *
742     * @return Return true if the event was handled here, or false if not.
743     */
744    @Override
745    public boolean onKeyDown(int keyCode, KeyEvent event) {
746        if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
747        if (mSearchable == null) {
748            return false;
749        }
750
751        // handle back key to go back to previous searchable, etc.
752        if (handleBackKey(keyCode, event)) {
753            return true;
754        }
755
756        if (keyCode == KeyEvent.KEYCODE_SEARCH) {
757            // If the search key is pressed, toggle between global and in-app search. If we are
758            // currently doing global search and there is no in-app search context to toggle to,
759            // just don't do anything.
760            return toggleGlobalSearch();
761        }
762
763        // if it's an action specified by the searchable activity, launch the
764        // entered query with the action key
765        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
766        if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
767            launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
768            return true;
769        }
770
771        return false;
772    }
773
774    /**
775     * Callback to watch the textedit field for empty/non-empty
776     */
777    private TextWatcher mTextWatcher = new TextWatcher() {
778
779        public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
780
781        public void onTextChanged(CharSequence s, int start,
782                int before, int after) {
783            if (DBG_LOG_TIMING) {
784                dbgLogTiming("onTextChanged()");
785            }
786            if (mSearchable == null) {
787                return;
788            }
789            updateWidgetState();
790            if (!mSearchAutoComplete.isPerformingCompletion()) {
791                // The user changed the query, remember it.
792                mUserQuery = s == null ? "" : s.toString();
793            }
794        }
795
796        public void afterTextChanged(Editable s) {
797            if (mSearchable == null) {
798                return;
799            }
800            if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) {
801                // The user changed the query, check if it is a URL and if so change the search
802                // button in the soft keyboard to the 'Go' button.
803                int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION));
804                if (Regex.WEB_URL_PATTERN.matcher(mUserQuery).matches()) {
805                    options = options | EditorInfo.IME_ACTION_GO;
806                } else {
807                    options = options | EditorInfo.IME_ACTION_SEARCH;
808                }
809                if (options != mSearchAutoCompleteImeOptions) {
810                    mSearchAutoCompleteImeOptions = options;
811                    mSearchAutoComplete.setImeOptions(options);
812                    // This call is required to update the soft keyboard UI with latest IME flags.
813                    mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType());
814                }
815            }
816        }
817    };
818
819    /**
820     * Enable/Disable the cancel button based on edit text state (any text?)
821     */
822    private void updateWidgetState() {
823        // enable the button if we have one or more non-space characters
824        boolean enabled = !mSearchAutoComplete.isEmpty();
825        mGoButton.setEnabled(enabled);
826        mGoButton.setFocusable(enabled);
827    }
828
829    /**
830     * React to typing in the GO search button by refocusing to EditText.
831     * Continue typing the query.
832     */
833    View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
834        public boolean onKey(View v, int keyCode, KeyEvent event) {
835            // guard against possible race conditions
836            if (mSearchable == null) {
837                return false;
838            }
839
840            if (!event.isSystem() &&
841                    (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
842                    (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
843                    (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
844                    (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
845                // restore focus and give key to EditText ...
846                if (mSearchAutoComplete.requestFocus()) {
847                    return mSearchAutoComplete.dispatchKeyEvent(event);
848                }
849            }
850
851            return false;
852        }
853    };
854
855    /**
856     * React to a click in the GO button by launching a search.
857     */
858    View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
859        public void onClick(View v) {
860            // guard against possible race conditions
861            if (mSearchable == null) {
862                return;
863            }
864            launchQuerySearch();
865        }
866    };
867
868    /**
869     * React to a click in the voice search button.
870     */
871    View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
872        public void onClick(View v) {
873            // guard against possible race conditions
874            if (mSearchable == null) {
875                return;
876            }
877            try {
878                if (mSearchable.getVoiceSearchLaunchWebSearch()) {
879                    getContext().startActivity(mVoiceWebSearchIntent);
880                } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
881                    Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);
882                    getContext().startActivity(appSearchIntent);
883                }
884            } catch (ActivityNotFoundException e) {
885                // Should not happen, since we check the availability of
886                // voice search before showing the button. But just in case...
887                Log.w(LOG_TAG, "Could not find voice search activity");
888            }
889         }
890    };
891
892    /**
893     * Create and return an Intent that can launch the voice search activity, perform a specific
894     * voice transcription, and forward the results to the searchable activity.
895     *
896     * @param baseIntent The voice app search intent to start from
897     * @return A completely-configured intent ready to send to the voice search activity
898     */
899    private Intent createVoiceAppSearchIntent(Intent baseIntent) {
900        ComponentName searchActivity = mSearchable.getSearchActivity();
901
902        // create the necessary intent to set up a search-and-forward operation
903        // in the voice search system.   We have to keep the bundle separate,
904        // because it becomes immutable once it enters the PendingIntent
905        Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
906        queryIntent.setComponent(searchActivity);
907        PendingIntent pending = PendingIntent.getActivity(
908                getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
909
910        // Now set up the bundle that will be inserted into the pending intent
911        // when it's time to do the search.  We always build it here (even if empty)
912        // because the voice search activity will always need to insert "QUERY" into
913        // it anyway.
914        Bundle queryExtras = new Bundle();
915        if (mAppSearchData != null) {
916            queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData);
917        }
918
919        // Now build the intent to launch the voice search.  Add all necessary
920        // extras to launch the voice recognizer, and then all the necessary extras
921        // to forward the results to the searchable activity
922        Intent voiceIntent = new Intent(baseIntent);
923
924        // Add all of the configuration options supplied by the searchable's metadata
925        String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
926        String prompt = null;
927        String language = null;
928        int maxResults = 1;
929        Resources resources = mActivityContext.getResources();
930        if (mSearchable.getVoiceLanguageModeId() != 0) {
931            languageModel = resources.getString(mSearchable.getVoiceLanguageModeId());
932        }
933        if (mSearchable.getVoicePromptTextId() != 0) {
934            prompt = resources.getString(mSearchable.getVoicePromptTextId());
935        }
936        if (mSearchable.getVoiceLanguageId() != 0) {
937            language = resources.getString(mSearchable.getVoiceLanguageId());
938        }
939        if (mSearchable.getVoiceMaxResults() != 0) {
940            maxResults = mSearchable.getVoiceMaxResults();
941        }
942        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
943        voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
944        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
945        voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
946        voiceIntent.putExtra(EXTRA_CALLING_PACKAGE,
947                searchActivity == null ? null : searchActivity.toShortString());
948
949        // Add the values that configure forwarding the results
950        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
951        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
952
953        return voiceIntent;
954    }
955
956    /**
957     * Corrects http/https typo errors in the given url string, and if the protocol specifier was
958     * not present defaults to http.
959     *
960     * @param inUrl URL to check and fix
961     * @return fixed URL string.
962     */
963    private String fixUrl(String inUrl) {
964        if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
965            return inUrl;
966
967        if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) {
968            if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
969                inUrl = inUrl.replaceFirst("/", "//");
970            } else {
971                inUrl = inUrl.replaceFirst(":", "://");
972            }
973        }
974
975        if (inUrl.indexOf("://") == -1) {
976            inUrl = "http://" + inUrl;
977        }
978
979        return inUrl;
980    }
981
982    /**
983     * React to the user typing "enter" or other hardwired keys while typing in the search box.
984     * This handles these special keys while the edit box has focus.
985     */
986    View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
987        public boolean onKey(View v, int keyCode, KeyEvent event) {
988            // guard against possible race conditions
989            if (mSearchable == null) {
990                return false;
991            }
992
993            if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
994            if (DBG) {
995                Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event
996                        + "), selection: " + mSearchAutoComplete.getListSelection());
997            }
998
999            // If a suggestion is selected, handle enter, search key, and action keys
1000            // as presses on the selected suggestion
1001            if (mSearchAutoComplete.isPopupShowing() &&
1002                    mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
1003                return onSuggestionsKey(v, keyCode, event);
1004            }
1005
1006            // If there is text in the query box, handle enter, and action keys
1007            // The search key is handled by the dialog's onKeyDown().
1008            if (!mSearchAutoComplete.isEmpty()) {
1009                if (keyCode == KeyEvent.KEYCODE_ENTER
1010                        && event.getAction() == KeyEvent.ACTION_UP) {
1011                    v.cancelLongPress();
1012
1013                    // If this is a url entered by the user & we displayed the 'Go' button which
1014                    // the user clicked, launch the url instead of using it as a search query.
1015                    if (mSearchable.autoUrlDetect() &&
1016                        (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION)
1017                                == EditorInfo.IME_ACTION_GO) {
1018                        Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString()));
1019                        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
1020                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1021                        launchIntent(intent);
1022                    } else {
1023                        // Launch as a regular search.
1024                        launchQuerySearch();
1025                    }
1026                    return true;
1027                }
1028                if (event.getAction() == KeyEvent.ACTION_DOWN) {
1029                    SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1030                    if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
1031                        launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
1032                        return true;
1033                    }
1034                }
1035            }
1036            return false;
1037        }
1038    };
1039
1040    @Override
1041    public void hide() {
1042        if (!isShowing()) return;
1043
1044        // We made sure the IME was displayed, so also make sure it is closed
1045        // when we go away.
1046        InputMethodManager imm = (InputMethodManager)getContext()
1047                .getSystemService(Context.INPUT_METHOD_SERVICE);
1048        if (imm != null) {
1049            imm.hideSoftInputFromWindow(
1050                    getWindow().getDecorView().getWindowToken(), 0);
1051        }
1052
1053        super.hide();
1054    }
1055
1056    /**
1057     * React to the user typing while in the suggestions list. First, check for action
1058     * keys. If not handled, try refocusing regular characters into the EditText.
1059     */
1060    private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1061        // guard against possible race conditions (late arrival after dismiss)
1062        if (mSearchable == null) {
1063            return false;
1064        }
1065        if (mSuggestionsAdapter == null) {
1066            return false;
1067        }
1068        if (event.getAction() == KeyEvent.ACTION_DOWN) {
1069            if (DBG_LOG_TIMING) {
1070                dbgLogTiming("onSuggestionsKey()");
1071            }
1072
1073            // First, check for enter or search (both of which we'll treat as a "click")
1074            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
1075                int position = mSearchAutoComplete.getListSelection();
1076                return launchSuggestion(position);
1077            }
1078
1079            // Next, check for left/right moves, which we use to "return" the user to the edit view
1080            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
1081                // give "focus" to text editor, with cursor at the beginning if
1082                // left key, at end if right key
1083                // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
1084                int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
1085                        0 : mSearchAutoComplete.length();
1086                mSearchAutoComplete.setSelection(selPoint);
1087                mSearchAutoComplete.setListSelection(0);
1088                mSearchAutoComplete.clearListSelection();
1089                mSearchAutoComplete.ensureImeVisible();
1090
1091                return true;
1092            }
1093
1094            // Next, check for an "up and out" move
1095            if (keyCode == KeyEvent.KEYCODE_DPAD_UP
1096                    && 0 == mSearchAutoComplete.getListSelection()) {
1097                restoreUserQuery();
1098                // let ACTV complete the move
1099                return false;
1100            }
1101
1102            // Next, check for an "action key"
1103            SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1104            if ((actionKey != null) &&
1105                    ((actionKey.getSuggestActionMsg() != null) ||
1106                     (actionKey.getSuggestActionMsgColumn() != null))) {
1107                // launch suggestion using action key column
1108                int position = mSearchAutoComplete.getListSelection();
1109                if (position != ListView.INVALID_POSITION) {
1110                    Cursor c = mSuggestionsAdapter.getCursor();
1111                    if (c.moveToPosition(position)) {
1112                        final String actionMsg = getActionKeyMessage(c, actionKey);
1113                        if (actionMsg != null && (actionMsg.length() > 0)) {
1114                            return launchSuggestion(position, keyCode, actionMsg);
1115                        }
1116                    }
1117                }
1118            }
1119        }
1120        return false;
1121    }
1122
1123    /**
1124     * Launch a search for the text in the query text field.
1125     */
1126    protected void launchQuerySearch()  {
1127        launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
1128    }
1129
1130    /**
1131     * Launch a search for the text in the query text field.
1132     *
1133     * @param actionKey The key code of the action key that was pressed,
1134     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1135     * @param actionMsg The message for the action key that was pressed,
1136     *        or <code>null</code> if none.
1137     */
1138    protected void launchQuerySearch(int actionKey, String actionMsg)  {
1139        String query = mSearchAutoComplete.getText().toString();
1140        String action = mGlobalSearchMode ? Intent.ACTION_WEB_SEARCH : Intent.ACTION_SEARCH;
1141        Intent intent = createIntent(action, null, null, query, null,
1142                actionKey, actionMsg);
1143        launchIntent(intent);
1144    }
1145
1146    /**
1147     * Launches an intent based on a suggestion.
1148     *
1149     * @param position The index of the suggestion to create the intent from.
1150     * @return true if a successful launch, false if could not (e.g. bad position).
1151     */
1152    protected boolean launchSuggestion(int position) {
1153        return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1154    }
1155
1156    /**
1157     * Launches an intent based on a suggestion.
1158     *
1159     * @param position The index of the suggestion to create the intent from.
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 true if a successful launch, false if could not (e.g. bad position).
1165     */
1166    protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1167        Cursor c = mSuggestionsAdapter.getCursor();
1168        if ((c != null) && c.moveToPosition(position)) {
1169
1170            Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1171
1172            // report back about the click
1173            if (mGlobalSearchMode) {
1174                // in global search mode, do it via cursor
1175                mSuggestionsAdapter.callCursorOnClick(c, position);
1176            } else if (intent != null
1177                    && mPreviousComponents != null
1178                    && !mPreviousComponents.isEmpty()) {
1179                // in-app search (and we have pivoted in as told by mPreviousComponents,
1180                // which is used for keeping track of what we pop back to when we are pivoting into
1181                // in app search.)
1182                reportInAppClickToGlobalSearch(c, intent);
1183            }
1184
1185            // launch the intent
1186            launchIntent(intent);
1187
1188            return true;
1189        }
1190        return false;
1191    }
1192
1193    /**
1194     * Report a click from an in app search result back to global search for shortcutting porpoises.
1195     *
1196     * @param c The cursor that is pointing to the clicked position.
1197     * @param intent The intent that will be launched for the click.
1198     */
1199    private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) {
1200        // for in app search, still tell global search via content provider
1201        Uri uri = getClickReportingUri();
1202        final ContentValues cv = new ContentValues();
1203        cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery);
1204        final ComponentName source = mSearchable.getSearchActivity();
1205        cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString());
1206
1207        // grab the intent columns from the intent we created since it has additional
1208        // logic for falling back on the searchable default
1209        cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction());
1210        cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString());
1211        cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME,
1212                        intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY));
1213
1214        // ensure the icons will work for global search
1215        cv.put(SearchManager.SUGGEST_COLUMN_ICON_1,
1216                        wrapIconForPackage(
1217                                source,
1218                                getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1)));
1219        cv.put(SearchManager.SUGGEST_COLUMN_ICON_2,
1220                        wrapIconForPackage(
1221                                source,
1222                                getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2)));
1223
1224        // the rest can be passed through directly
1225        cv.put(SearchManager.SUGGEST_COLUMN_FORMAT,
1226                getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT));
1227        cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
1228                getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1));
1229        cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
1230                getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2));
1231        cv.put(SearchManager.SUGGEST_COLUMN_QUERY,
1232                getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY));
1233        cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
1234                getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID));
1235        // note: deliberately omitting background color since it is only for global search
1236        // "more results" entries
1237        mContext.getContentResolver().insert(uri, cv);
1238    }
1239
1240    /**
1241     * @return A URI appropriate for reporting a click.
1242     */
1243    private Uri getClickReportingUri() {
1244        Uri.Builder uriBuilder = new Uri.Builder()
1245                .scheme(ContentResolver.SCHEME_CONTENT)
1246                .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY);
1247
1248        uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH);
1249
1250        return uriBuilder
1251                .query("")     // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1252                .fragment("")  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1253                .build();
1254    }
1255
1256    /**
1257     * Wraps an icon for a particular package.  If the icon is a resource id, it is converted into
1258     * an android.resource:// URI.
1259     *
1260     * @param source The source of the icon
1261     * @param icon The icon retrieved from a suggestion column
1262     * @return An icon string appropriate for the package.
1263     */
1264    private String wrapIconForPackage(ComponentName source, String icon) {
1265        if (icon == null || icon.length() == 0 || "0".equals(icon)) {
1266            // SearchManager specifies that null or zero can be returned to indicate
1267            // no icon. We also allow empty string.
1268            return null;
1269        } else if (!Character.isDigit(icon.charAt(0))){
1270            return icon;
1271        } else {
1272            String packageName = source.getPackageName();
1273            return new Uri.Builder()
1274                    .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
1275                    .authority(packageName)
1276                    .encodedPath(icon)
1277                    .toString();
1278        }
1279    }
1280
1281    /**
1282     * Launches an intent, including any special intent handling.  Doesn't dismiss the dialog
1283     * since that will be handled in {@link SearchDialogWrapper#performActivityResuming}
1284     */
1285    private void launchIntent(Intent intent) {
1286        if (intent == null) {
1287            return;
1288        }
1289        if (handleSpecialIntent(intent)){
1290            return;
1291        }
1292        Log.d(LOG_TAG, "launching " + intent);
1293        try {
1294            // in global search mode, we send the activity straight to the original suggestion
1295            // source. this is because GlobalSearch may not have permission to launch the
1296            // intent, and to avoid the extra step of going through GlobalSearch.
1297            if (mGlobalSearchMode) {
1298                launchGlobalSearchIntent(intent);
1299            } else {
1300                // If the intent was created from a suggestion, it will always have an explicit
1301                // component here.
1302                Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI());
1303                getContext().startActivity(intent);
1304                // If the search switches to a different activity,
1305                // SearchDialogWrapper#performActivityResuming
1306                // will handle hiding the dialog when the next activity starts, but for
1307                // real in-app search, we still need to dismiss the dialog.
1308                if (isInRealAppSearch()) {
1309                    dismiss();
1310                }
1311            }
1312        } catch (RuntimeException ex) {
1313            Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1314        }
1315    }
1316
1317    private void launchGlobalSearchIntent(Intent intent) {
1318        final String packageName;
1319        // GlobalSearch puts the original source of the suggestion in the
1320        // 'component name' column. If set, we send the intent to that activity.
1321        // We trust GlobalSearch to always set this to the suggestion source.
1322        String intentComponent = intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY);
1323        if (intentComponent != null) {
1324            ComponentName componentName = ComponentName.unflattenFromString(intentComponent);
1325            intent.setComponent(componentName);
1326            intent.removeExtra(SearchManager.COMPONENT_NAME_KEY);
1327            // Launch the intent as the suggestion source.
1328            // This prevents sources from using the search dialog to launch
1329            // intents that they don't have permission for themselves.
1330            packageName = componentName.getPackageName();
1331        } else {
1332            // If there is no component in the suggestion, it must be a built-in suggestion
1333            // from GlobalSearch (e.g. "Search the web for") or the intent
1334            // launched when pressing the search/go button in the search dialog.
1335            // Launch the intent with the permissions of GlobalSearch.
1336            packageName = mSearchable.getSearchActivity().getPackageName();
1337        }
1338
1339        // Launch all global search suggestions as new tasks, since they don't relate
1340        // to the current task.
1341        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1342        setBrowserApplicationId(intent);
1343
1344        startActivityInPackage(intent, packageName);
1345    }
1346
1347    /**
1348     * If the intent is to open an HTTP or HTTPS URL, we set
1349     * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that
1350     * has been opened by us for the same URL will be reused.
1351     */
1352    private void setBrowserApplicationId(Intent intent) {
1353        Uri data = intent.getData();
1354        if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) {
1355            String scheme = data.getScheme();
1356            if (scheme != null && scheme.startsWith("http")) {
1357                intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString());
1358            }
1359        }
1360    }
1361
1362    /**
1363     * Starts an activity as if it had been started by the given package.
1364     *
1365     * @param intent The description of the activity to start.
1366     * @param packageName
1367     * @throws ActivityNotFoundException If the intent could not be resolved to
1368     *         and existing activity.
1369     * @throws SecurityException If the package does not have permission to start
1370     *         start the activity.
1371     * @throws AndroidRuntimeException If some other error occurs.
1372     */
1373    private void startActivityInPackage(Intent intent, String packageName) {
1374        try {
1375            int uid = ActivityThread.getPackageManager().getPackageUid(packageName);
1376            if (uid < 0) {
1377                throw new AndroidRuntimeException("Package UID not found " + packageName);
1378            }
1379            String resolvedType = intent.resolveTypeIfNeeded(getContext().getContentResolver());
1380            IBinder resultTo = null;
1381            String resultWho = null;
1382            int requestCode = -1;
1383            boolean onlyIfNeeded = false;
1384            Log.i(LOG_TAG, "Starting (uid " + uid + ", " + packageName + ") " + intent.toURI());
1385            int result = ActivityManagerNative.getDefault().startActivityInPackage(
1386                    uid, intent, resolvedType, resultTo, resultWho, requestCode, onlyIfNeeded);
1387            checkStartActivityResult(result, intent);
1388        } catch (RemoteException ex) {
1389            throw new AndroidRuntimeException(ex);
1390        }
1391    }
1392
1393    // Stolen from Instrumentation.checkStartActivityResult()
1394    private static void checkStartActivityResult(int res, Intent intent) {
1395        if (res >= IActivityManager.START_SUCCESS) {
1396            return;
1397        }
1398        switch (res) {
1399            case IActivityManager.START_INTENT_NOT_RESOLVED:
1400            case IActivityManager.START_CLASS_NOT_FOUND:
1401                if (intent.getComponent() != null)
1402                    throw new ActivityNotFoundException(
1403                            "Unable to find explicit activity class "
1404                            + intent.getComponent().toShortString()
1405                            + "; have you declared this activity in your AndroidManifest.xml?");
1406                throw new ActivityNotFoundException(
1407                        "No Activity found to handle " + intent);
1408            case IActivityManager.START_PERMISSION_DENIED:
1409                throw new SecurityException("Not allowed to start activity "
1410                        + intent);
1411            case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
1412                throw new AndroidRuntimeException(
1413                        "FORWARD_RESULT_FLAG used while also requesting a result");
1414            default:
1415                throw new AndroidRuntimeException("Unknown error code "
1416                        + res + " when starting " + intent);
1417        }
1418    }
1419
1420    /**
1421     * Handles the special intent actions declared in {@link SearchManager}.
1422     *
1423     * @return <code>true</code> if the intent was handled.
1424     */
1425    private boolean handleSpecialIntent(Intent intent) {
1426        String action = intent.getAction();
1427        if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) {
1428            handleChangeSourceIntent(intent);
1429            return true;
1430        }
1431        return false;
1432    }
1433
1434    /**
1435     * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}.
1436     */
1437    private void handleChangeSourceIntent(Intent intent) {
1438        Uri dataUri = intent.getData();
1439        if (dataUri == null) {
1440            Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data.");
1441            return;
1442        }
1443        ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString());
1444        if (componentName == null) {
1445            Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri);
1446            return;
1447        }
1448        if (DBG) Log.d(LOG_TAG, "Switching to " + componentName);
1449
1450        pushPreviousComponent(mLaunchComponent);
1451        if (!show(componentName, mAppSearchData, false)) {
1452            Log.w(LOG_TAG, "Failed to switch to source " + componentName);
1453            popPreviousComponent();
1454            return;
1455        }
1456
1457        String query = intent.getStringExtra(SearchManager.QUERY);
1458        setUserQuery(query);
1459        mSearchAutoComplete.showDropDown();
1460    }
1461
1462    /**
1463     * Sets the list item selection in the AutoCompleteTextView's ListView.
1464     */
1465    public void setListSelection(int index) {
1466        mSearchAutoComplete.setListSelection(index);
1467    }
1468
1469    /**
1470     * Saves the previous component that was searched, so that we can go
1471     * back to it.
1472     */
1473    private void pushPreviousComponent(ComponentName componentName) {
1474        if (mPreviousComponents == null) {
1475            mPreviousComponents = new ArrayList<ComponentName>();
1476        }
1477        mPreviousComponents.add(componentName);
1478    }
1479
1480    /**
1481     * Pops the previous component off the stack and returns it.
1482     *
1483     * @return The component name, or <code>null</code> if there was
1484     *         no previous component.
1485     */
1486    private ComponentName popPreviousComponent() {
1487        if (mPreviousComponents == null) {
1488            return null;
1489        }
1490        int size = mPreviousComponents.size();
1491        if (size == 0) {
1492            return null;
1493        }
1494        return mPreviousComponents.remove(size - 1);
1495    }
1496
1497    /**
1498     * Goes back to the previous component that was searched, if any.
1499     *
1500     * @return <code>true</code> if there was a previous component that we could go back to.
1501     */
1502    private boolean backToPreviousComponent() {
1503        ComponentName previous = popPreviousComponent();
1504        if (previous == null) {
1505            return false;
1506        }
1507        if (!show(previous, mAppSearchData, false)) {
1508            Log.w(LOG_TAG, "Failed to switch to source " + previous);
1509            return false;
1510        }
1511
1512        // must touch text to trigger suggestions
1513        // TODO: should this be the text as it was when the user left
1514        // the source that we are now going back to?
1515        String query = mSearchAutoComplete.getText().toString();
1516        setUserQuery(query);
1517
1518        return true;
1519    }
1520
1521    /**
1522     * When a particular suggestion has been selected, perform the various lookups required
1523     * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
1524     * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
1525     * the suggestion includes a data id.
1526     *
1527     * @param c The suggestions cursor, moved to the row of the user's selection
1528     * @param actionKey The key code of the action key that was pressed,
1529     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1530     * @param actionMsg The message for the action key that was pressed,
1531     *        or <code>null</code> if none.
1532     * @return An intent for the suggestion at the cursor's position.
1533     */
1534    private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1535        try {
1536            // use specific action if supplied, or default action if supplied, or fixed default
1537            String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1538
1539            // some items are display only, or have effect via the cursor respond click reporting.
1540            if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
1541                return null;
1542            }
1543
1544            if (action == null) {
1545                action = mSearchable.getSuggestIntentAction();
1546            }
1547            if (action == null) {
1548                action = Intent.ACTION_SEARCH;
1549            }
1550
1551            // use specific data if supplied, or default data if supplied
1552            String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1553            if (data == null) {
1554                data = mSearchable.getSuggestIntentData();
1555            }
1556            // then, if an ID was provided, append it.
1557            if (data != null) {
1558                String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1559                if (id != null) {
1560                    data = data + "/" + Uri.encode(id);
1561                }
1562            }
1563            Uri dataUri = (data == null) ? null : Uri.parse(data);
1564
1565            String componentName = getColumnString(
1566                    c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
1567
1568            String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1569            String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1570
1571            return createIntent(action, dataUri, extraData, query, componentName, actionKey,
1572                    actionMsg);
1573        } catch (RuntimeException e ) {
1574            int rowNum;
1575            try {                       // be really paranoid now
1576                rowNum = c.getPosition();
1577            } catch (RuntimeException e2 ) {
1578                rowNum = -1;
1579            }
1580            Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
1581                            " returned exception" + e.toString());
1582            return null;
1583        }
1584    }
1585
1586    /**
1587     * Constructs an intent from the given information and the search dialog state.
1588     *
1589     * @param action Intent action.
1590     * @param data Intent data, or <code>null</code>.
1591     * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1592     * @param query Intent query, or <code>null</code>.
1593     * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
1594     * @param actionKey The key code of the action key that was pressed,
1595     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1596     * @param actionMsg The message for the action key that was pressed,
1597     *        or <code>null</code> if none.
1598     * @return The intent.
1599     */
1600    private Intent createIntent(String action, Uri data, String extraData, String query,
1601            String componentName, int actionKey, String actionMsg) {
1602        // Now build the Intent
1603        Intent intent = new Intent(action);
1604        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1605        if (data != null) {
1606            intent.setData(data);
1607        }
1608        intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
1609        if (query != null) {
1610            intent.putExtra(SearchManager.QUERY, query);
1611        }
1612        if (extraData != null) {
1613            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1614        }
1615        if (componentName != null) {
1616            intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName);
1617        }
1618        if (mAppSearchData != null) {
1619            intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1620        }
1621        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1622            intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1623            intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1624        }
1625        // Only allow 3rd-party intents from GlobalSearch
1626        if (!mGlobalSearchMode) {
1627            intent.setComponent(mSearchable.getSearchActivity());
1628        }
1629        return intent;
1630    }
1631
1632    /**
1633     * For a given suggestion and a given cursor row, get the action message.  If not provided
1634     * by the specific row/column, also check for a single definition (for the action key).
1635     *
1636     * @param c The cursor providing suggestions
1637     * @param actionKey The actionkey record being examined
1638     *
1639     * @return Returns a string, or null if no action key message for this suggestion
1640     */
1641    private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
1642        String result = null;
1643        // check first in the cursor data, for a suggestion-specific message
1644        final String column = actionKey.getSuggestActionMsgColumn();
1645        if (column != null) {
1646            result = SuggestionsAdapter.getColumnString(c, column);
1647        }
1648        // If the cursor didn't give us a message, see if there's a single message defined
1649        // for the actionkey (for all suggestions)
1650        if (result == null) {
1651            result = actionKey.getSuggestActionMsg();
1652        }
1653        return result;
1654    }
1655
1656    /**
1657     * Local subclass for AutoCompleteTextView.
1658     */
1659    public static class SearchAutoComplete extends AutoCompleteTextView {
1660
1661        private int mThreshold;
1662        private SearchDialog mSearchDialog;
1663
1664        public SearchAutoComplete(Context context) {
1665            super(context);
1666            mThreshold = getThreshold();
1667        }
1668
1669        public SearchAutoComplete(Context context, AttributeSet attrs) {
1670            super(context, attrs);
1671            mThreshold = getThreshold();
1672        }
1673
1674        public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1675            super(context, attrs, defStyle);
1676            mThreshold = getThreshold();
1677        }
1678
1679        private void setSearchDialog(SearchDialog searchDialog) {
1680            mSearchDialog = searchDialog;
1681        }
1682
1683        @Override
1684        public void setThreshold(int threshold) {
1685            super.setThreshold(threshold);
1686            mThreshold = threshold;
1687        }
1688
1689        /**
1690         * Returns true if the text field is empty, or contains only whitespace.
1691         */
1692        private boolean isEmpty() {
1693            return TextUtils.getTrimmedLength(getText()) == 0;
1694        }
1695
1696        /**
1697         * We override this method to avoid replacing the query box text
1698         * when a suggestion is clicked.
1699         */
1700        @Override
1701        protected void replaceText(CharSequence text) {
1702        }
1703
1704        /**
1705         * We override this method to avoid an extra onItemClick being called on the
1706         * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)}
1707         * when an item is clicked with the trackball.
1708         */
1709        @Override
1710        public void performCompletion() {
1711        }
1712
1713        /**
1714         * We override this method to be sure and show the soft keyboard if appropriate when
1715         * the TextView has focus.
1716         */
1717        @Override
1718        public void onWindowFocusChanged(boolean hasWindowFocus) {
1719            super.onWindowFocusChanged(hasWindowFocus);
1720
1721            if (hasWindowFocus) {
1722                InputMethodManager inputManager = (InputMethodManager)
1723                        getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
1724                inputManager.showSoftInput(this, 0);
1725            }
1726        }
1727
1728        /**
1729         * We override this method so that we can allow a threshold of zero, which ACTV does not.
1730         */
1731        @Override
1732        public boolean enoughToFilter() {
1733            return mThreshold <= 0 || super.enoughToFilter();
1734        }
1735
1736        /**
1737         * {@link AutoCompleteTextView#onKeyPreIme(int, KeyEvent)}) dismisses the drop-down on BACK,
1738         * so we must override this method to modify the BACK behavior.
1739         */
1740        @Override
1741        public boolean onKeyPreIme(int keyCode, KeyEvent event) {
1742            if (mSearchDialog.mSearchable == null) {
1743                return false;
1744            }
1745            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
1746                if (mSearchDialog.backToPreviousComponent()) {
1747                    return true;
1748                }
1749                // If the drop-down obscures the keyboard, the user wouldn't see anything
1750                // happening when pressing back, so we dismiss the entire dialog instead.
1751                if (isInputMethodNotNeeded()) {
1752                    mSearchDialog.cancel();
1753                    return true;
1754                }
1755                return false; // will dismiss soft keyboard if necessary
1756            }
1757            return false;
1758        }
1759    }
1760
1761    protected boolean handleBackKey(int keyCode, KeyEvent event) {
1762        if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
1763            if (backToPreviousComponent()) {
1764                return true;
1765            }
1766            cancel();
1767            return true;
1768        }
1769        return false;
1770    }
1771
1772    /**
1773     * Implements OnItemClickListener
1774     */
1775    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1776        if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1777        launchSuggestion(position);
1778    }
1779
1780    /**
1781     * Implements OnItemSelectedListener
1782     */
1783     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
1784         if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1785         // A suggestion has been selected, rewrite the query if possible,
1786         // otherwise the restore the original query.
1787         if (REWRITE_QUERIES) {
1788             rewriteQueryFromSuggestion(position);
1789         }
1790     }
1791
1792     /**
1793      * Implements OnItemSelectedListener
1794      */
1795     public void onNothingSelected(AdapterView<?> parent) {
1796         if (DBG) Log.d(LOG_TAG, "onNothingSelected()");
1797     }
1798
1799     /**
1800      * Query rewriting.
1801      */
1802
1803     private void rewriteQueryFromSuggestion(int position) {
1804         Cursor c = mSuggestionsAdapter.getCursor();
1805         if (c == null) {
1806             return;
1807         }
1808         if (c.moveToPosition(position)) {
1809             // Get the new query from the suggestion.
1810             CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1811             if (newQuery != null) {
1812                 // The suggestion rewrites the query.
1813                 if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'");
1814                 // Update the text field, without getting new suggestions.
1815                 setQuery(newQuery);
1816             } else {
1817                 // The suggestion does not rewrite the query, restore the user's query.
1818                 if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query.");
1819                 restoreUserQuery();
1820             }
1821         } else {
1822             // We got a bad position, restore the user's query.
1823             Log.w(LOG_TAG, "Bad suggestion position: " + position);
1824             restoreUserQuery();
1825         }
1826     }
1827
1828     /**
1829      * Restores the query entered by the user if needed.
1830      */
1831     private void restoreUserQuery() {
1832         if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'");
1833         setQuery(mUserQuery);
1834     }
1835
1836     /**
1837      * Sets the text in the query box, without updating the suggestions.
1838      */
1839     private void setQuery(CharSequence query) {
1840         mSearchAutoComplete.setText(query, false);
1841         if (query != null) {
1842             mSearchAutoComplete.setSelection(query.length());
1843         }
1844     }
1845
1846     /**
1847      * Sets the text in the query box, updating the suggestions.
1848      */
1849     private void setUserQuery(String query) {
1850         if (query == null) {
1851             query = "";
1852         }
1853         mUserQuery = query;
1854         mSearchAutoComplete.setText(query);
1855         mSearchAutoComplete.setSelection(query.length());
1856     }
1857
1858    /**
1859     * Debugging Support
1860     */
1861
1862    /**
1863     * For debugging only, sample the millisecond clock and log it.
1864     * Uses AtomicLong so we can use in multiple threads
1865     */
1866    private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
1867    private void dbgLogTiming(final String caller) {
1868        long millis = SystemClock.uptimeMillis();
1869        long oldTime = mLastLogTime.getAndSet(millis);
1870        long delta = millis - oldTime;
1871        final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
1872        Log.d(LOG_TAG,report);
1873    }
1874}
1875