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