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