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