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