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