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