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