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