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