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