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