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