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