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