SearchDialog.java revision a8f556ee8cee24674663fd73c7a5b5a919b2d5bb
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.app;
18
19import android.content.ActivityNotFoundException;
20import android.content.BroadcastReceiver;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.content.res.Configuration;
28import android.content.res.Resources;
29import android.content.res.Resources.NotFoundException;
30import android.database.Cursor;
31import android.graphics.drawable.Drawable;
32import android.net.Uri;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.RemoteException;
36import android.os.ServiceManager;
37import android.os.SystemClock;
38import android.server.search.SearchableInfo;
39import android.speech.RecognizerIntent;
40import android.text.Editable;
41import android.text.InputType;
42import android.text.TextUtils;
43import android.text.TextWatcher;
44import android.util.AttributeSet;
45import android.util.Log;
46import android.view.Gravity;
47import android.view.KeyEvent;
48import android.view.View;
49import android.view.ViewGroup;
50import android.view.Window;
51import android.view.WindowManager;
52import android.view.inputmethod.InputMethodManager;
53import android.widget.AdapterView;
54import android.widget.AutoCompleteTextView;
55import android.widget.Button;
56import android.widget.CursorAdapter;
57import android.widget.ImageButton;
58import android.widget.ImageView;
59import android.widget.ListView;
60import android.widget.SimpleCursorAdapter;
61import android.widget.TextView;
62import android.widget.WrapperListAdapter;
63import android.widget.AdapterView.OnItemClickListener;
64import android.widget.AdapterView.OnItemSelectedListener;
65
66import java.lang.ref.WeakReference;
67import java.util.concurrent.atomic.AtomicLong;
68
69/**
70 * In-application-process implementation of Search Bar.  This is still controlled by the
71 * SearchManager, but it runs in the current activity's process to keep things lighter weight.
72 *
73 * @hide
74 */
75public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
76
77    // Debugging support
78    final static String LOG_TAG = "SearchDialog";
79    private static final int DBG_LOG_TIMING = 0;
80    final static int DBG_JAM_THREADING = 0;
81
82    // interaction with runtime
83    IntentFilter mCloseDialogsFilter;
84    IntentFilter mPackageFilter;
85
86    private static final String INSTANCE_KEY_COMPONENT = "comp";
87    private static final String INSTANCE_KEY_APPDATA = "data";
88    private static final String INSTANCE_KEY_GLOBALSEARCH = "glob";
89    private static final String INSTANCE_KEY_DISPLAY_QUERY = "dQry";
90    private static final String INSTANCE_KEY_DISPLAY_SEL_START = "sel1";
91    private static final String INSTANCE_KEY_DISPLAY_SEL_END = "sel2";
92    private static final String INSTANCE_KEY_USER_QUERY = "uQry";
93    private static final String INSTANCE_KEY_SUGGESTION_QUERY = "sQry";
94    private static final String INSTANCE_KEY_SELECTED_ELEMENT = "slEl";
95    private static final int INSTANCE_SELECTED_BUTTON = -2;
96    private static final int INSTANCE_SELECTED_QUERY = -1;
97
98    // views & widgets
99    private TextView mBadgeLabel;
100    private AutoCompleteTextView mSearchTextField;
101    private Button mGoButton;
102    private ImageButton mVoiceButton;
103
104    // interaction with searchable application
105    private ComponentName mLaunchComponent;
106    private Bundle mAppSearchData;
107    private boolean mGlobalSearchMode;
108    private Context mActivityContext;
109
110    // interaction with the search manager service
111    private SearchableInfo mSearchable;
112
113    // support for suggestions
114    private String mUserQuery = null;
115    private int mUserQuerySelStart;
116    private int mUserQuerySelEnd;
117    private boolean mLeaveJammedQueryOnRefocus = false;
118    private String mPreviousSuggestionQuery = null;
119    private int mPresetSelection = -1;
120    private String mSuggestionAction = null;
121    private Uri mSuggestionData = null;
122    private String mSuggestionQuery = null;
123
124    // For voice searching
125    private Intent mVoiceWebSearchIntent;
126    private Intent mVoiceAppSearchIntent;
127
128    // support for AutoCompleteTextView suggestions display
129    private SuggestionsAdapter mSuggestionsAdapter;
130
131    /**
132     * Constructor - fires it up and makes it look like the search UI.
133     *
134     * @param context Application Context we can use for system acess
135     */
136    public SearchDialog(Context context) {
137        super(context, com.android.internal.R.style.Theme_SearchBar);
138    }
139
140    /**
141     * We create the search dialog just once, and it stays around (hidden)
142     * until activated by the user.
143     */
144    @Override
145    protected void onCreate(Bundle savedInstanceState) {
146        super.onCreate(savedInstanceState);
147
148        Window theWindow = getWindow();
149        theWindow.setGravity(Gravity.TOP|Gravity.FILL_HORIZONTAL);
150
151        setContentView(com.android.internal.R.layout.search_bar);
152
153        theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT,
154                ViewGroup.LayoutParams.WRAP_CONTENT);
155        WindowManager.LayoutParams lp = theWindow.getAttributes();
156        lp.setTitle("Search Dialog");
157        lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
158        theWindow.setAttributes(lp);
159
160        // get the view elements for local access
161        mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
162        mSearchTextField = (AutoCompleteTextView)
163                findViewById(com.android.internal.R.id.search_src_text);
164        mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
165        mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
166
167        // attach listeners
168        mSearchTextField.addTextChangedListener(mTextWatcher);
169        mSearchTextField.setOnKeyListener(mTextKeyListener);
170        mGoButton.setOnClickListener(mGoButtonClickListener);
171        mGoButton.setOnKeyListener(mButtonsKeyListener);
172        mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
173        mVoiceButton.setOnKeyListener(mButtonsKeyListener);
174
175        // pre-hide all the extraneous elements
176        mBadgeLabel.setVisibility(View.GONE);
177
178        // Additional adjustments to make Dialog work for Search
179
180        // Touching outside of the search dialog will dismiss it
181        setCanceledOnTouchOutside(true);
182
183        // Set up broadcast filters
184        mCloseDialogsFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
185        mPackageFilter = new IntentFilter();
186        mPackageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
187        mPackageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
188        mPackageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
189        mPackageFilter.addDataScheme("package");
190
191        // Save voice intent for later queries/launching
192        mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
193        mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
194                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
195
196        mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
197    }
198
199    /**
200     * Set up the search dialog
201     *
202     * @param Returns true if search dialog launched, false if not
203     */
204    public boolean show(String initialQuery, boolean selectInitialQuery,
205            ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
206        if (isShowing()) {
207            // race condition - already showing but not handling events yet.
208            // in this case, just discard the "show" request
209            return true;
210        }
211
212        // Get searchable info from search manager and use to set up other elements of UI
213        // Do this first so we can get out quickly if there's nothing to search
214        ISearchManager sms;
215        sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE));
216        try {
217            mSearchable = sms.getSearchableInfo(componentName, globalSearch);
218        } catch (RemoteException e) {
219            mSearchable = null;
220        }
221        if (mSearchable == null) {
222            // unfortunately, we can't log here.  it would be logspam every time the user
223            // clicks the "search" key on a non-search app
224            return false;
225        }
226
227        // OK, we're going to show ourselves
228        super.show();
229
230        setupSearchableInfo();
231
232        mLaunchComponent = componentName;
233        mAppSearchData = appSearchData;
234        mGlobalSearchMode = globalSearch;
235
236        // receive broadcasts
237        getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
238        getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
239
240        // configure the autocomplete aspects of the input box
241        mSearchTextField.setOnItemClickListener(this);
242        mSearchTextField.setOnItemSelectedListener(this);
243
244        // This conversion is necessary to force a preload of the EditText and thus force
245        // suggestions to be presented (even for an empty query)
246        if (initialQuery == null) {
247            initialQuery = "";     // This forces the preload to happen, triggering suggestions
248        }
249
250        // attach the suggestions adapter, if suggestions are available
251        // The existence of a suggestions authority is the proxy for "suggestions available here"
252        if (mSearchable.getSuggestAuthority() == null) {
253            mSuggestionsAdapter = null;
254            mSearchTextField.setAdapter(mSuggestionsAdapter);
255            mSearchTextField.setText(initialQuery);
256        } else {
257            mSuggestionsAdapter = new SuggestionsAdapter(getContext(), mSearchable,
258                    mSearchTextField);
259            mSearchTextField.setAdapter(mSuggestionsAdapter);
260
261            // finally, load the user's initial text (which may trigger suggestions)
262            mSuggestionsAdapter.setNonUserQuery(false);
263            mSearchTextField.setText(initialQuery);
264        }
265
266        if (selectInitialQuery) {
267            mSearchTextField.selectAll();
268        } else {
269            mSearchTextField.setSelection(initialQuery.length());
270        }
271        return true;
272    }
273
274    /**
275     * The default show() for this Dialog is not supported.
276     */
277    @Override
278    public void show() {
279        return;
280    }
281
282    /**
283     * The search dialog is being dismissed, so handle all of the local shutdown operations.
284     *
285     * This function is designed to be idempotent so that dismiss() can be safely called at any time
286     * (even if already closed) and more likely to really dump any memory.  No leaks!
287     */
288    @Override
289    public void onStop() {
290        super.onStop();
291
292        setOnCancelListener(null);
293        setOnDismissListener(null);
294
295        // stop receiving broadcasts (throws exception if none registered)
296        try {
297            getContext().unregisterReceiver(mBroadcastReceiver);
298        } catch (RuntimeException e) {
299            // This is OK - it just means we didn't have any registered
300        }
301
302        // close any leftover cursor
303        if (mSuggestionsAdapter != null) {
304            mSuggestionsAdapter.changeCursor(null);
305        }
306
307        // dump extra memory we're hanging on to
308        mLaunchComponent = null;
309        mAppSearchData = null;
310        mSearchable = null;
311        mSuggestionAction = null;
312        mSuggestionData = null;
313        mSuggestionQuery = null;
314        mActivityContext = null;
315        mPreviousSuggestionQuery = null;
316        mUserQuery = null;
317    }
318
319    /**
320     * Save the minimal set of data necessary to recreate the search
321     *
322     * @return A bundle with the state of the dialog.
323     */
324    @Override
325    public Bundle onSaveInstanceState() {
326        Bundle bundle = new Bundle();
327
328        // setup info so I can recreate this particular search
329        bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
330        bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
331        bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode);
332
333        // UI state
334        bundle.putString(INSTANCE_KEY_DISPLAY_QUERY, mSearchTextField.getText().toString());
335        bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_START, mSearchTextField.getSelectionStart());
336        bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_END, mSearchTextField.getSelectionEnd());
337        bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
338        bundle.putString(INSTANCE_KEY_SUGGESTION_QUERY, mPreviousSuggestionQuery);
339
340        int selectedElement = INSTANCE_SELECTED_QUERY;
341        if (mGoButton.isFocused()) {
342            selectedElement = INSTANCE_SELECTED_BUTTON;
343        } else if (mSearchTextField.isPopupShowing()) {
344            selectedElement = 0; // TODO mSearchTextField.getListSelection()    // 0..n
345        }
346        bundle.putInt(INSTANCE_KEY_SELECTED_ELEMENT, selectedElement);
347
348        return bundle;
349    }
350
351    /**
352     * Restore the state of the dialog from a previously saved bundle.
353     *
354     * @param savedInstanceState The state of the dialog previously saved by
355     *     {@link #onSaveInstanceState()}.
356     */
357    @Override
358    public void onRestoreInstanceState(Bundle savedInstanceState) {
359        // Get the launch info
360        ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
361        Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
362        boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
363
364        // get the UI state
365        String displayQuery = savedInstanceState.getString(INSTANCE_KEY_DISPLAY_QUERY);
366        int querySelStart = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_START, -1);
367        int querySelEnd = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_END, -1);
368        String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
369        int selectedElement = savedInstanceState.getInt(INSTANCE_KEY_SELECTED_ELEMENT);
370        String suggestionQuery = savedInstanceState.getString(INSTANCE_KEY_SUGGESTION_QUERY);
371
372        // show the dialog.  skip any show/hide animation, we want to go fast.
373        // send the text that actually generates the suggestions here;  we'll replace the display
374        // text as necessary in a moment.
375        if (!show(suggestionQuery, false, launchComponent, appSearchData, globalSearch)) {
376            // for some reason, we couldn't re-instantiate
377            return;
378        }
379
380        if (mSuggestionsAdapter != null) {
381            mSuggestionsAdapter.setNonUserQuery(true);
382        }
383        mSearchTextField.setText(displayQuery);
384        // TODO because the new query is (not) processed in another thread, we can't just
385        // take away this flag (yet).  The better solution here is going to require a new API
386        // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions.
387//      mSuggestionsAdapter.setNonUserQuery(false);
388
389        // clean up the selection state
390        switch (selectedElement) {
391        case INSTANCE_SELECTED_BUTTON:
392            mGoButton.setEnabled(true);
393            mGoButton.setFocusable(true);
394            mGoButton.requestFocus();
395            break;
396        case INSTANCE_SELECTED_QUERY:
397            if (querySelStart >= 0 && querySelEnd >= 0) {
398                mSearchTextField.requestFocus();
399                mSearchTextField.setSelection(querySelStart, querySelEnd);
400            }
401            break;
402        default:
403            // defer selecting a list element until suggestion list appears
404            mPresetSelection = selectedElement;
405            // TODO mSearchTextField.setListSelection(selectedElement)
406            break;
407        }
408    }
409
410    /**
411     * Hook for updating layout on a rotation
412     *
413     */
414    public void onConfigurationChanged(Configuration newConfig) {
415        if (isShowing()) {
416            // Redraw (resources may have changed)
417            updateSearchButton();
418            updateSearchBadge();
419            updateQueryHint();
420        }
421    }
422
423    /**
424     * Use SearchableInfo record (from search manager service) to preconfigure the UI in various
425     * ways.
426     */
427    private void setupSearchableInfo() {
428        if (mSearchable != null) {
429            mActivityContext = mSearchable.getActivityContext(getContext());
430
431            updateSearchButton();
432            updateSearchBadge();
433            updateQueryHint();
434            updateVoiceButton();
435
436            // In order to properly configure the input method (if one is being used), we
437            // need to let it know if we'll be providing suggestions.  Although it would be
438            // difficult/expensive to know if every last detail has been configured properly, we
439            // can at least see if a suggestions provider has been configured, and use that
440            // as our trigger.
441            int inputType = mSearchable.getInputType();
442            // We only touch this if the input type is set up for text (which it almost certainly
443            // should be, in the case of search!)
444            if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
445                // The existence of a suggestions authority is the proxy for "suggestions
446                // are available here"
447                inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
448                if (mSearchable.getSuggestAuthority() != null) {
449                    inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
450                }
451            }
452            mSearchTextField.setInputType(inputType);
453            mSearchTextField.setImeOptions(mSearchable.getImeOptions());
454        }
455    }
456
457    /**
458     * The list of installed packages has just changed.  This means that our current context
459     * may no longer be valid.  This would only happen if a package is installed/removed exactly
460     * when the search bar is open.  So for now we're just going to close the search
461     * bar.
462     *
463     * Anything fancier would require some checks to see if the user's context was still valid.
464     * Which would be messier.
465     */
466    public void onPackageListChange() {
467        cancel();
468    }
469
470    /**
471     * Update the text in the search button.  Note: This is deprecated functionality, for
472     * 1.0 compatibility only.
473     */
474    private void updateSearchButton() {
475        String textLabel = null;
476        Drawable iconLabel = null;
477        int textId = mSearchable.getSearchButtonText();
478        if (textId != 0) {
479            textLabel = mActivityContext.getResources().getString(textId);
480        } else {
481            iconLabel = getContext().getResources().
482                    getDrawable(com.android.internal.R.drawable.ic_btn_search);
483        }
484        mGoButton.setText(textLabel);
485        mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
486    }
487
488    /**
489     * Setup the search "Badge" if request by mode flags.
490     */
491    private void updateSearchBadge() {
492        // assume both hidden
493        int visibility = View.GONE;
494        Drawable icon = null;
495        String text = null;
496
497        // optionally show one or the other.
498        if (mSearchable.mBadgeIcon) {
499            icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
500            visibility = View.VISIBLE;
501        } else if (mSearchable.mBadgeLabel) {
502            text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
503            visibility = View.VISIBLE;
504        }
505
506        mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
507        mBadgeLabel.setText(text);
508        mBadgeLabel.setVisibility(visibility);
509    }
510
511    /**
512     * Update the hint in the query text field.
513     */
514    private void updateQueryHint() {
515        if (isShowing()) {
516            String hint = null;
517            if (mSearchable != null) {
518                int hintId = mSearchable.getHintId();
519                if (hintId != 0) {
520                    hint = mActivityContext.getString(hintId);
521                }
522            }
523            mSearchTextField.setHint(hint);
524        }
525    }
526
527    /**
528     * Update the visibility of the voice button.  There are actually two voice search modes,
529     * either of which will activate the button.
530     */
531    private void updateVoiceButton() {
532        int visibility = View.GONE;
533        if (mSearchable.getVoiceSearchEnabled()) {
534            Intent testIntent = null;
535            if (mSearchable.getVoiceSearchLaunchWebSearch()) {
536                testIntent = mVoiceWebSearchIntent;
537            } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
538                testIntent = mVoiceAppSearchIntent;
539            }
540            if (testIntent != null) {
541                ResolveInfo ri = getContext().getPackageManager().
542                        resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY);
543                if (ri != null) {
544                    visibility = View.VISIBLE;
545                }
546            }
547        }
548        mVoiceButton.setVisibility(visibility);
549    }
550
551    /**
552     * Listeners of various types
553     */
554
555    /**
556     * Dialog's OnKeyListener implements various search-specific functionality
557     *
558     * @param keyCode This is the keycode of the typed key, and is the same value as
559     * found in the KeyEvent parameter.
560     * @param event The complete event record for the typed key
561     *
562     * @return Return true if the event was handled here, or false if not.
563     */
564    @Override
565    public boolean onKeyDown(int keyCode, KeyEvent event) {
566        switch (keyCode) {
567        case KeyEvent.KEYCODE_BACK:
568            cancel();
569            return true;
570        case KeyEvent.KEYCODE_SEARCH:
571            if (TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0) {
572                launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
573            } else {
574                cancel();
575            }
576            return true;
577        default:
578            SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
579            if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
580                launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
581                return true;
582            }
583            break;
584        }
585        return false;
586    }
587
588    /**
589     * Callback to watch the textedit field for empty/non-empty
590     */
591    private TextWatcher mTextWatcher = new TextWatcher() {
592
593        public void beforeTextChanged(CharSequence s, int start, int
594                before, int after) { }
595
596        public void onTextChanged(CharSequence s, int start,
597                int before, int after) {
598            if (DBG_LOG_TIMING == 1) {
599                dbgLogTiming("onTextChanged()");
600            }
601            updateWidgetState();
602            // Only do suggestions if actually typed by user
603            if ((mSuggestionsAdapter != null) && !mSuggestionsAdapter.getNonUserQuery()) {
604                mPreviousSuggestionQuery = s.toString();
605                mUserQuery = mSearchTextField.getText().toString();
606                mUserQuerySelStart = mSearchTextField.getSelectionStart();
607                mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
608            }
609        }
610
611        public void afterTextChanged(Editable s) { }
612    };
613
614    /**
615     * Enable/Disable the cancel button based on edit text state (any text?)
616     */
617    private void updateWidgetState() {
618        // enable the button if we have one or more non-space characters
619        boolean enabled =
620            TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0;
621
622        mGoButton.setEnabled(enabled);
623        mGoButton.setFocusable(enabled);
624    }
625
626    private final static String[] ONE_LINE_FROM =       {SearchManager.SUGGEST_COLUMN_TEXT_1 };
627    private final static String[] ONE_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
628                                                         SearchManager.SUGGEST_COLUMN_ICON_1,
629                                                         SearchManager.SUGGEST_COLUMN_ICON_2};
630    private final static String[] TWO_LINE_FROM =       {SearchManager.SUGGEST_COLUMN_TEXT_1,
631                                                         SearchManager.SUGGEST_COLUMN_TEXT_2 };
632    private final static String[] TWO_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
633                                                         SearchManager.SUGGEST_COLUMN_TEXT_2,
634                                                         SearchManager.SUGGEST_COLUMN_ICON_1,
635                                                         SearchManager.SUGGEST_COLUMN_ICON_2 };
636
637    private final static int[] ONE_LINE_TO =       {com.android.internal.R.id.text1};
638    private final static int[] ONE_LINE_ICONS_TO = {com.android.internal.R.id.text1,
639                                                    com.android.internal.R.id.icon1,
640                                                    com.android.internal.R.id.icon2};
641    private final static int[] TWO_LINE_TO =       {com.android.internal.R.id.text1,
642                                                    com.android.internal.R.id.text2};
643    private final static int[] TWO_LINE_ICONS_TO = {com.android.internal.R.id.text1,
644                                                    com.android.internal.R.id.text2,
645                                                    com.android.internal.R.id.icon1,
646                                                    com.android.internal.R.id.icon2};
647
648    /**
649     * Safely retrieve the suggestions cursor adapter from the ListView
650     *
651     * @param adapterView The ListView containing our adapter
652     * @result The CursorAdapter that we installed, or null if not set
653     */
654    private static CursorAdapter getSuggestionsAdapter(AdapterView<?> adapterView) {
655        CursorAdapter result = null;
656        if (adapterView != null) {
657            Object ad = adapterView.getAdapter();
658            if (ad instanceof CursorAdapter) {
659                result = (CursorAdapter) ad;
660            } else if (ad instanceof WrapperListAdapter) {
661                result = (CursorAdapter) ((WrapperListAdapter)ad).getWrappedAdapter();
662            }
663        }
664        return result;
665    }
666
667    /**
668     * React to typing in the GO search button by refocusing to EditText.
669     * Continue typing the query.
670     */
671    View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
672        public boolean onKey(View v, int keyCode, KeyEvent event) {
673            // also guard against possible race conditions (late arrival after dismiss)
674            if (mSearchable != null) {
675                return refocusingKeyListener(v, keyCode, event);
676            }
677            return false;
678        }
679    };
680
681    /**
682     * React to a click in the GO button by launching a search.
683     */
684    View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
685        public void onClick(View v) {
686            // also guard against possible race conditions (late arrival after dismiss)
687            if (mSearchable != null) {
688                launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
689            }
690        }
691    };
692
693    /**
694     * React to a click in the voice search button.
695     */
696    View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
697        public void onClick(View v) {
698            try {
699                if (mSearchable.getVoiceSearchLaunchWebSearch()) {
700                    getContext().startActivity(mVoiceWebSearchIntent);
701                    dismiss();
702                } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
703                    Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);
704                    getContext().startActivity(appSearchIntent);
705                    dismiss();
706                }
707            } catch (ActivityNotFoundException e) {
708                // Should not happen, since we check the availability of
709                // voice search before showing the button. But just in case...
710                Log.w(LOG_TAG, "Could not find voice search activity");
711            }
712         }
713    };
714
715    /**
716     * Create and return an Intent that can launch the voice search activity, perform a specific
717     * voice transcription, and forward the results to the searchable activity.
718     *
719     * @param baseIntent The voice app search intent to start from
720     * @return A completely-configured intent ready to send to the voice search activity
721     */
722    private Intent createVoiceAppSearchIntent(Intent baseIntent) {
723        // create the necessary intent to set up a search-and-forward operation
724        // in the voice search system.   We have to keep the bundle separate,
725        // because it becomes immutable once it enters the PendingIntent
726        Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
727        queryIntent.setComponent(mSearchable.mSearchActivity);
728        PendingIntent pending = PendingIntent.getActivity(
729                getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
730
731        // Now set up the bundle that will be inserted into the pending intent
732        // when it's time to do the search.  We always build it here (even if empty)
733        // because the voice search activity will always need to insert "QUERY" into
734        // it anyway.
735        Bundle queryExtras = new Bundle();
736        if (mAppSearchData != null) {
737            queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData);
738        }
739
740        // Now build the intent to launch the voice search.  Add all necessary
741        // extras to launch the voice recognizer, and then all the necessary extras
742        // to forward the results to the searchable activity
743        Intent voiceIntent = new Intent(baseIntent);
744
745        // Add all of the configuration options supplied by the searchable's metadata
746        String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
747        String prompt = null;
748        String language = null;
749        int maxResults = 1;
750        Resources resources = mActivityContext.getResources();
751        if (mSearchable.getVoiceLanguageModeId() != 0) {
752            languageModel = resources.getString(mSearchable.getVoiceLanguageModeId());
753        }
754        if (mSearchable.getVoicePromptTextId() != 0) {
755            prompt = resources.getString(mSearchable.getVoicePromptTextId());
756        }
757        if (mSearchable.getVoiceLanguageId() != 0) {
758            language = resources.getString(mSearchable.getVoiceLanguageId());
759        }
760        if (mSearchable.getVoiceMaxResults() != 0) {
761            maxResults = mSearchable.getVoiceMaxResults();
762        }
763        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
764        voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
765        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
766        voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
767
768        // Add the values that configure forwarding the results
769        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
770        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
771
772        return voiceIntent;
773    }
774
775    /**
776     * React to the user typing "enter" or other hardwired keys while typing in the search box.
777     * This handles these special keys while the edit box has focus.
778     */
779    View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
780        public boolean onKey(View v, int keyCode, KeyEvent event) {
781            if (keyCode == KeyEvent.KEYCODE_BACK) {
782                cancel();
783                return true;
784            }
785            // also guard against possible race conditions (late arrival after dismiss)
786            if (mSearchable != null &&
787                    TextUtils.getTrimmedLength(mSearchTextField.getText()) > 0) {
788                if (DBG_LOG_TIMING == 1) {
789                    dbgLogTiming("doTextKey()");
790                }
791                // dispatch "typing in the list" first
792                if (mSearchTextField.isPopupShowing() &&
793                        mSearchTextField.getListSelection() != ListView.INVALID_POSITION) {
794                     return onSuggestionsKey(v, keyCode, event);
795                }
796                // otherwise, dispatch an "edit view" key
797                switch (keyCode) {
798                case KeyEvent.KEYCODE_ENTER:
799                    if (event.getAction() == KeyEvent.ACTION_UP) {
800                        v.cancelLongPress();
801                        launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
802                        return true;
803                    }
804                    break;
805                case KeyEvent.KEYCODE_DPAD_DOWN:
806                    // capture the EditText state, so we can restore the user entry later
807                    mUserQuery = mSearchTextField.getText().toString();
808                    mUserQuerySelStart = mSearchTextField.getSelectionStart();
809                    mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
810                    // pass through - we're just watching here
811                    break;
812                default:
813                    if (event.getAction() == KeyEvent.ACTION_DOWN) {
814                        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
815                        if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
816                            launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
817                            return true;
818                        }
819                    }
820                    break;
821                }
822            }
823            return false;
824        }
825    };
826
827    /**
828     * React to the user typing while the suggestions are focused.  First, check for action
829     * keys.  If not handled, try refocusing regular characters into the EditText.  In this case,
830     * replace the query text (start typing fresh text).
831     */
832    private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
833        boolean handled = false;
834        // also guard against possible race conditions (late arrival after dismiss)
835        if (mSearchable != null) {
836            handled = doSuggestionsKey(v, keyCode, event);
837        }
838        return handled;
839    }
840
841    /**
842     * Per UI design, we're going to "steer" any typed keystrokes back into the EditText
843     * box, even if the user has navigated the focus to the dropdown or to the GO button.
844     *
845     * @param v The view into which the keystroke was typed
846     * @param keyCode keyCode of entered key
847     * @param event Full KeyEvent record of entered key
848     */
849    private boolean refocusingKeyListener(View v, int keyCode, KeyEvent event) {
850        boolean handled = false;
851
852        if (!event.isSystem() &&
853                (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
854                (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
855                (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
856                (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
857                (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
858            // restore focus and give key to EditText ...
859            // but don't replace the user's query
860            mLeaveJammedQueryOnRefocus = true;
861            if (mSearchTextField.requestFocus()) {
862                handled = mSearchTextField.dispatchKeyEvent(event);
863            }
864            mLeaveJammedQueryOnRefocus = false;
865        }
866        return handled;
867    }
868
869    /**
870     * Update query text based on transitions in and out of suggestions list.
871     */
872    /*
873     * TODO - figure out if this logic is required for the autocomplete text view version
874
875    OnFocusChangeListener mSuggestFocusListener = new OnFocusChangeListener() {
876        public void onFocusChange(View v, boolean hasFocus) {
877            // also guard against possible race conditions (late arrival after dismiss)
878            if (mSearchable == null) {
879                return;
880            }
881            // Update query text based on navigation in to/out of the suggestions list
882            if (hasFocus) {
883                // Entering the list view - record selection point from user's query
884                mUserQuery = mSearchTextField.getText().toString();
885                mUserQuerySelStart = mSearchTextField.getSelectionStart();
886                mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
887                // then update the query to match the entered selection
888                jamSuggestionQuery(true, mSuggestionsList,
889                                    mSuggestionsList.getSelectedItemPosition());
890            } else {
891                // Exiting the list view
892
893                if (mSuggestionsList.getSelectedItemPosition() < 0) {
894                    // Direct exit - Leave new suggestion in place (do nothing)
895                } else {
896                    // Navigation exit - restore user's query text
897                    if (!mLeaveJammedQueryOnRefocus) {
898                        jamSuggestionQuery(false, null, -1);
899                    }
900                }
901            }
902
903        }
904    };
905    */
906
907    /**
908     * This is the listener for the ACTION_CLOSE_SYSTEM_DIALOGS intent.  It's an indication that
909     * we should close ourselves immediately, in order to allow a higher-priority UI to take over
910     * (e.g. phone call received).
911     */
912    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
913        @Override
914        public void onReceive(Context context, Intent intent) {
915            String action = intent.getAction();
916            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
917                cancel();
918            } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)
919                    || Intent.ACTION_PACKAGE_REMOVED.equals(action)
920                    || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
921                onPackageListChange();
922            }
923        }
924    };
925
926    @Override
927    public void cancel() {
928        // We made sure the IME was displayed, so also make sure it is closed
929        // when we go away.
930        InputMethodManager imm = (InputMethodManager)getContext()
931                .getSystemService(Context.INPUT_METHOD_SERVICE);
932        if (imm != null) {
933            imm.hideSoftInputFromWindow(
934                    getWindow().getDecorView().getWindowToken(), 0);
935        }
936
937        super.cancel();
938    }
939
940    /**
941     * Various ways to launch searches
942     */
943
944    /**
945     * React to the user clicking the "GO" button.  Hide the UI and launch a search.
946     *
947     * @param actionKey Pass a keycode if the launch was triggered by an action key.  Pass
948     * KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
949     * @param actionMsg Pass the suggestion-provided message if the launch was triggered by an
950     * action key.  Pass null for no actionKey message.
951     */
952    private void launchQuerySearch(int actionKey, final String actionMsg)  {
953        final String query = mSearchTextField.getText().toString();
954        final Bundle appData = mAppSearchData;
955        final SearchableInfo si = mSearchable;      // cache briefly (dismiss() nulls it)
956        dismiss();
957        sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, actionKey, actionMsg, si);
958    }
959
960    /**
961     * React to the user typing an action key while in the suggestions list
962     */
963    private boolean doSuggestionsKey(View v, int keyCode, KeyEvent event) {
964        // Exit early in case of race condition
965        if (mSuggestionsAdapter == null) {
966            return false;
967        }
968        if (event.getAction() == KeyEvent.ACTION_DOWN) {
969            if (DBG_LOG_TIMING == 1) {
970                dbgLogTiming("doSuggestionsKey()");
971            }
972
973            // First, check for enter or search (both of which we'll treat as a "click")
974            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
975                int position = mSearchTextField.getListSelection();
976                return launchSuggestion(mSuggestionsAdapter, position);
977            }
978
979            // Next, check for left/right moves, which we use to "return" the user to the edit view
980            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
981                // give "focus" to text editor, but don't restore the user's original query
982                int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
983                        0 : mSearchTextField.length();
984                mSearchTextField.setSelection(selPoint);
985                mSearchTextField.setListSelection(0);
986                mSearchTextField.clearListSelection();
987                return true;
988            }
989
990            // Next, check for an "up and out" move
991            if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchTextField.getListSelection()) {
992                jamSuggestionQuery(false, null, -1);
993                // let ACTV complete the move
994                return false;
995            }
996
997            // Next, check for an "action key"
998            SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
999            if ((actionKey != null) &&
1000                    ((actionKey.mSuggestActionMsg != null) ||
1001                     (actionKey.mSuggestActionMsgColumn != null))) {
1002                //   launch suggestion using action key column
1003                int position = mSearchTextField.getListSelection();
1004                if (position >= 0) {
1005                    Cursor c = mSuggestionsAdapter.getCursor();
1006                    if (c.moveToPosition(position)) {
1007                        final String actionMsg = getActionKeyMessage(c, actionKey);
1008                        if (actionMsg != null && (actionMsg.length() > 0)) {
1009                            // shut down search bar and launch the activity
1010                            // cache everything we need because dismiss releases mems
1011                            setupSuggestionIntent(c, mSearchable);
1012                            final String query = mSearchTextField.getText().toString();
1013                            final Bundle appData =  mAppSearchData;
1014                            SearchableInfo si = mSearchable;
1015                            String suggestionAction = mSuggestionAction;
1016                            Uri suggestionData = mSuggestionData;
1017                            String suggestionQuery = mSuggestionQuery;
1018                            dismiss();
1019                            sendLaunchIntent(suggestionAction, suggestionData,
1020                                    suggestionQuery, appData,
1021                                             keyCode, actionMsg, si);
1022                            return true;
1023                        }
1024                    }
1025                }
1026            }
1027        }
1028        return false;
1029    }
1030
1031    /**
1032     * Set or reset the user query to follow the selections in the suggestions
1033     *
1034     * @param jamQuery True means to set the query, false means to reset it to the user's choice
1035     */
1036    private void jamSuggestionQuery(boolean jamQuery, AdapterView<?> parent, int position) {
1037        // quick check against race conditions
1038        if (mSearchable == null) {
1039            return;
1040        }
1041
1042        mSuggestionsAdapter.setNonUserQuery(true);       // disables any suggestions processing
1043        if (jamQuery) {
1044            CursorAdapter ca = getSuggestionsAdapter(parent);
1045            Cursor c = ca.getCursor();
1046            if (c.moveToPosition(position)) {
1047                setupSuggestionIntent(c, mSearchable);
1048                String jamText = null;
1049
1050                // Simple heuristic for selecting text with which to rewrite the query.
1051                if (mSuggestionQuery != null) {
1052                    jamText = mSuggestionQuery;
1053                } else if (mSearchable.mQueryRewriteFromData && (mSuggestionData != null)) {
1054                    jamText = mSuggestionData.toString();
1055                } else if (mSearchable.mQueryRewriteFromText) {
1056                    try {
1057                        int column = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1);
1058                        jamText = c.getString(column);
1059                    } catch (RuntimeException e) {
1060                        // no work here, jamText is null
1061                    }
1062                }
1063                if (jamText != null) {
1064                    mSearchTextField.setText(jamText);
1065                    /* mSearchTextField.selectAll(); */ // this didn't work anyway in the old UI
1066                    // TODO this is only needed in the model where we have a selection in the ACTV
1067                    // and in the dropdown at the same time.
1068                    mSearchTextField.setSelection(jamText.length());
1069                }
1070            }
1071        } else {
1072            // reset user query
1073            mSearchTextField.setText(mUserQuery);
1074            try {
1075                mSearchTextField.setSelection(mUserQuerySelStart, mUserQuerySelEnd);
1076            } catch (IndexOutOfBoundsException e) {
1077                // In case of error, just select all
1078                Log.e(LOG_TAG, "Caught IndexOutOfBoundsException while setting selection.  " +
1079                        "start=" + mUserQuerySelStart + " end=" + mUserQuerySelEnd +
1080                        " text=\"" + mUserQuery + "\"");
1081                mSearchTextField.selectAll();
1082            }
1083        }
1084        // TODO because the new query is (not) processed in another thread, we can't just
1085        // take away this flag (yet).  The better solution here is going to require a new API
1086        // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions.
1087//      mSuggestionsAdapter.setNonUserQuery(false);
1088    }
1089
1090    /**
1091     * Assemble a search intent and send it.
1092     *
1093     * @param action The intent to send, typically Intent.ACTION_SEARCH
1094     * @param data The data for the intent
1095     * @param query The user text entered (so far)
1096     * @param appData The app data bundle (if supplied)
1097     * @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will
1098     * be sent here.  Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
1099     * @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the
1100     * corresponding tag message will be sent here.  Pass null for no actionKey message.
1101     * @param si Reference to the current SearchableInfo.  Passed here so it can be used even after
1102     * we've called dismiss(), which attempts to null mSearchable.
1103     */
1104    private void sendLaunchIntent(final String action, final Uri data, final String query,
1105            final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) {
1106        Intent launcher = new Intent(action);
1107
1108        if (query != null) {
1109            launcher.putExtra(SearchManager.QUERY, query);
1110        }
1111
1112        if (data != null) {
1113            launcher.setData(data);
1114        }
1115
1116        if (appData != null) {
1117            launcher.putExtra(SearchManager.APP_DATA, appData);
1118        }
1119
1120        // add launch info (action key, etc.)
1121        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1122            launcher.putExtra(SearchManager.ACTION_KEY, actionKey);
1123            launcher.putExtra(SearchManager.ACTION_MSG, actionMsg);
1124        }
1125
1126        // attempt to enforce security requirement (no 3rd-party intents)
1127        launcher.setComponent(si.mSearchActivity);
1128
1129        getContext().startActivity(launcher);
1130    }
1131
1132    /**
1133     * Shared code for launching a query from a suggestion.
1134     * @param ca The cursor adapter containing the suggestions
1135     * @param position The suggestion we'll be launching from
1136     * @return true if a successful launch, false if could not (e.g. bad position)
1137     */
1138    private boolean launchSuggestion(CursorAdapter ca, int position) {
1139        Cursor c = ca.getCursor();
1140        if ((c != null) && c.moveToPosition(position)) {
1141            setupSuggestionIntent(c, mSearchable);
1142
1143            final Bundle appData =  mAppSearchData;
1144            SearchableInfo si = mSearchable;
1145            String suggestionAction = mSuggestionAction;
1146            Uri suggestionData = mSuggestionData;
1147            String suggestionQuery = mSuggestionQuery;
1148            dismiss();
1149            sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, appData,
1150                                KeyEvent.KEYCODE_UNKNOWN, null, si);
1151            return true;
1152        }
1153        return false;
1154    }
1155
1156    /**
1157     * When a particular suggestion has been selected, perform the various lookups required
1158     * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
1159     * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
1160     * the suggestion includes a data id.
1161     *
1162     * NOTE:  Return values are in member variables mSuggestionAction & mSuggestionData.
1163     *
1164     * @param c The suggestions cursor, moved to the row of the user's selection
1165     * @param si The searchable activity's info record
1166     */
1167    void setupSuggestionIntent(Cursor c, SearchableInfo si) {
1168        try {
1169            // use specific action if supplied, or default action if supplied, or fixed default
1170            mSuggestionAction = null;
1171            int mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1172            if (mColumn >= 0) {
1173                final String action = c.getString(mColumn);
1174                if (action != null) {
1175                    mSuggestionAction = action;
1176                }
1177            }
1178            if (mSuggestionAction == null) {
1179                mSuggestionAction = si.getSuggestIntentAction();
1180            }
1181            if (mSuggestionAction == null) {
1182                mSuggestionAction = Intent.ACTION_SEARCH;
1183            }
1184
1185            // use specific data if supplied, or default data if supplied
1186            String data = null;
1187            mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1188            if (mColumn >= 0) {
1189                final String rowData = c.getString(mColumn);
1190                if (rowData != null) {
1191                    data = rowData;
1192                }
1193            }
1194            if (data == null) {
1195                data = si.getSuggestIntentData();
1196            }
1197
1198            // then, if an ID was provided, append it.
1199            if (data != null) {
1200                mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1201                if (mColumn >= 0) {
1202                    final String id = c.getString(mColumn);
1203                    if (id != null) {
1204                        data = data + "/" + Uri.encode(id);
1205                    }
1206                }
1207            }
1208            mSuggestionData = (data == null) ? null : Uri.parse(data);
1209
1210            mSuggestionQuery = null;
1211            mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
1212            if (mColumn >= 0) {
1213                final String query = c.getString(mColumn);
1214                if (query != null) {
1215                    mSuggestionQuery = query;
1216                }
1217            }
1218        } catch (RuntimeException e ) {
1219            int rowNum;
1220            try {                       // be really paranoid now
1221                rowNum = c.getPosition();
1222            } catch (RuntimeException e2 ) {
1223                rowNum = -1;
1224            }
1225            Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
1226                            " returned exception" + e.toString());
1227        }
1228    }
1229
1230    /**
1231     * For a given suggestion and a given cursor row, get the action message.  If not provided
1232     * by the specific row/column, also check for a single definition (for the action key).
1233     *
1234     * @param c The cursor providing suggestions
1235     * @param actionKey The actionkey record being examined
1236     *
1237     * @return Returns a string, or null if no action key message for this suggestion
1238     */
1239    private String getActionKeyMessage(Cursor c, final SearchableInfo.ActionKeyInfo actionKey) {
1240        String result = null;
1241        // check first in the cursor data, for a suggestion-specific message
1242        final String column = actionKey.mSuggestActionMsgColumn;
1243        if (column != null) {
1244            try {
1245                int colId = c.getColumnIndexOrThrow(column);
1246                result = c.getString(colId);
1247            } catch (RuntimeException e) {
1248                // OK - result is already null
1249            }
1250        }
1251        // If the cursor didn't give us a message, see if there's a single message defined
1252        // for the actionkey (for all suggestions)
1253        if (result == null) {
1254            result = actionKey.mSuggestActionMsg;
1255        }
1256        return result;
1257    }
1258
1259    /**
1260     * Local subclass for AutoCompleteTextView
1261     *
1262     * This exists entirely to override the threshold method.  Otherwise we just use the class
1263     * as-is.
1264     */
1265    public static class SearchAutoComplete extends AutoCompleteTextView {
1266
1267        public SearchAutoComplete(Context context) {
1268            super(null);
1269        }
1270
1271        public SearchAutoComplete(Context context, AttributeSet attrs) {
1272            super(context, attrs);
1273        }
1274
1275        public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1276            super(context, attrs, defStyle);
1277        }
1278
1279        /**
1280         * We never allow ACTV to automatically replace the text, since we use "jamSuggestionQuery"
1281         * to do that.  There's no point in letting ACTV do this here, because in the search UI,
1282         * as soon as we click a suggestion, we're going to start shutting things down.
1283         */
1284        @Override
1285        public void replaceText(CharSequence text) {
1286        }
1287
1288        /**
1289         * We always return true, so that the effective threshold is "zero".  This allows us
1290         * to provide "null" suggestions such as "just show me some recent entries".
1291         */
1292        @Override
1293        public boolean enoughToFilter() {
1294            return true;
1295        }
1296    }
1297
1298    /**
1299     * Support for AutoCompleteTextView-based suggestions
1300     */
1301    /**
1302     * This class provides the filtering-based interface to suggestions providers.
1303     * It is hardwired in a couple of places to support GoogleSearch - for example, it supports
1304     * two-line suggestions, but it does not support icons.
1305     */
1306    private static class SuggestionsAdapter extends SimpleCursorAdapter {
1307        private final String TAG = "SuggestionsAdapter";
1308
1309        SearchableInfo mSearchable;
1310        private Resources mProviderResources;
1311
1312        // These private variables are shared by the filter thread and must be protected
1313        private WeakReference<Cursor> mRecentCursor = new WeakReference<Cursor>(null);
1314        private boolean mNonUserQuery = false;
1315        private AutoCompleteTextView mParentView;
1316
1317        public SuggestionsAdapter(Context context, SearchableInfo searchable,
1318                AutoCompleteTextView actv) {
1319            super(context, -1, null, null, null);
1320            mSearchable = searchable;
1321            mParentView = actv;
1322
1323            // set up provider resources (gives us icons, etc.)
1324            Context activityContext = mSearchable.getActivityContext(mContext);
1325            Context providerContext = mSearchable.getProviderContext(mContext, activityContext);
1326            mProviderResources = providerContext.getResources();
1327        }
1328
1329        /**
1330         * Set this field (temporarily!) to disable suggestions updating.  This allows us
1331         * to change the string in the text view without changing the suggestions list.
1332         */
1333        public void setNonUserQuery(boolean nonUserQuery) {
1334            synchronized (this) {
1335                mNonUserQuery = nonUserQuery;
1336            }
1337        }
1338
1339        public boolean getNonUserQuery() {
1340            synchronized (this) {
1341                return mNonUserQuery;
1342            }
1343        }
1344
1345        /**
1346         * Use the search suggestions provider to obtain a live cursor.  This will be called
1347         * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
1348         * The results will be processed in the UI thread and changeCursor() will be called.
1349         *
1350         * In order to provide the Search Mgr functionality of seeing your query change as you
1351         * scroll through the list, we have to be able to jam new text into the string without
1352         * retriggering the suggestions.  We do that here via the "nonUserQuery" flag.  In that
1353         * case we simply return the existing cursor.
1354         *
1355         * TODO: Dianne suggests that this should simply be promoted into an AutoCompleteTextView
1356         * behavior (perhaps optionally).
1357         *
1358         * TODO: The "nonuserquery" logic has a race condition because it happens in another thread.
1359         * This also needs to be fixed.
1360         */
1361        @Override
1362        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
1363            String query = (constraint == null) ? "" : constraint.toString();
1364            Cursor c = null;
1365            synchronized (this) {
1366                if (mNonUserQuery) {
1367                    c = mRecentCursor.get();
1368                    mNonUserQuery = false;
1369                }
1370            }
1371            if (c == null) {
1372                c = getSuggestions(mSearchable, query);
1373                synchronized (this) {
1374                    mRecentCursor = new WeakReference<Cursor>(c);
1375                }
1376            }
1377            return c;
1378        }
1379
1380        /**
1381         * Overriding changeCursor() allows us to change not only the cursor, but by sampling
1382         * the cursor's columns, the actual display characteristics of the list.
1383         */
1384        @Override
1385        public void changeCursor(Cursor c) {
1386
1387            // first, check for various conditions that disqualify this cursor
1388            if ((c == null) || (c.getCount() == 0)) {
1389                // no cursor, or cursor with no data
1390                changeCursorAndColumns(null, null, null);
1391                if (c != null) {
1392                    c.close();
1393                }
1394                return;
1395            }
1396
1397            // check cursor before trying to create list views from it
1398            int colId = c.getColumnIndex("_id");
1399            int col1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
1400            int col2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
1401            int colIc1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
1402            int colIc2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
1403
1404            boolean minimal = (colId >= 0) && (col1 >= 0);
1405            boolean hasIcons = (colIc1 >= 0) && (colIc2 >= 0);
1406            boolean has2Lines = col2 >= 0;
1407
1408            if (minimal) {
1409                int layout;
1410                String[] from;
1411                int[] to;
1412
1413                if (hasIcons) {
1414                    if (has2Lines) {
1415                        layout = com.android.internal.R.layout.search_dropdown_item_icons_2line;
1416                        from = TWO_LINE_ICONS_FROM;
1417                        to = TWO_LINE_ICONS_TO;
1418                    } else {
1419                        layout = com.android.internal.R.layout.search_dropdown_item_icons_1line;
1420                        from = ONE_LINE_ICONS_FROM;
1421                        to = ONE_LINE_ICONS_TO;
1422                    }
1423                } else {
1424                    if (has2Lines) {
1425                        layout = com.android.internal.R.layout.search_dropdown_item_2line;
1426                        from = TWO_LINE_FROM;
1427                        to = TWO_LINE_TO;
1428                    } else {
1429                        layout = com.android.internal.R.layout.search_dropdown_item_1line;
1430                        from = ONE_LINE_FROM;
1431                        to = ONE_LINE_TO;
1432                    }
1433                }
1434                // Force the underlying ListView to discard and reload all layouts
1435                // (Note, this should be optimized for cases where layout/cursor remain same)
1436                mParentView.resetListAndClearViews();
1437                // Now actually set up the cursor, columns, and the list view
1438                changeCursorAndColumns(c, from, to);
1439                setViewResource(layout);
1440            } else {
1441                // Provide some help for developers instead of just silently discarding
1442                Log.w(LOG_TAG, "Suggestions cursor discarded due to missing required columns.");
1443                changeCursorAndColumns(null, null, null);
1444                c.close();
1445            }
1446            if ((colIc1 >= 0) != (colIc2 >= 0)) {
1447                Log.w(LOG_TAG, "Suggestion icon column(s) discarded, must be 0 or 2 columns.");
1448            }
1449        }
1450
1451        /**
1452         * Overriding this allows us to write the selected query back into the box.
1453         * NOTE:  This is a vastly simplified version of SearchDialog.jamQuery() and does
1454         * not universally support the search API.  But it is sufficient for Google Search.
1455         */
1456        @Override
1457        public CharSequence convertToString(Cursor cursor) {
1458            CharSequence result = null;
1459            if (cursor != null) {
1460                int column = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
1461                if (column >= 0) {
1462                    final String query = cursor.getString(column);
1463                    if (query != null) {
1464                        result = query;
1465                    }
1466                }
1467            }
1468            return result;
1469        }
1470
1471        /**
1472         * Get the query cursor for the search suggestions.
1473         *
1474         * TODO this is functionally identical to the version in SearchDialog.java.  Perhaps it
1475         * could be hoisted into SearchableInfo or some other shared spot.
1476         *
1477         * @param query The search text entered (so far)
1478         * @return Returns a cursor with suggestions, or null if no suggestions
1479         */
1480        private Cursor getSuggestions(final SearchableInfo searchable, final String query) {
1481            Cursor cursor = null;
1482            if (searchable.getSuggestAuthority() != null) {
1483                try {
1484                    StringBuilder uriStr = new StringBuilder("content://");
1485                    uriStr.append(searchable.getSuggestAuthority());
1486
1487                    // if content path provided, insert it now
1488                    final String contentPath = searchable.getSuggestPath();
1489                    if (contentPath != null) {
1490                        uriStr.append('/');
1491                        uriStr.append(contentPath);
1492                    }
1493
1494                    // append standard suggestion query path
1495                    uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY);
1496
1497                    // inject query, either as selection args or inline
1498                    String[] selArgs = null;
1499                    if (searchable.getSuggestSelection() != null) {    // use selection if provided
1500                        selArgs = new String[] {query};
1501                    } else {
1502                        uriStr.append('/');                             // no sel, use REST pattern
1503                        uriStr.append(Uri.encode(query));
1504                    }
1505
1506                    // finally, make the query
1507                    cursor = mContext.getContentResolver().query(
1508                                                        Uri.parse(uriStr.toString()), null,
1509                                                        searchable.getSuggestSelection(), selArgs,
1510                                                        null);
1511                } catch (RuntimeException e) {
1512                    Log.w(TAG, "Search Suggestions query returned exception " + e.toString());
1513                    cursor = null;
1514                }
1515            }
1516
1517            return cursor;
1518        }
1519
1520        /**
1521         * Overriding this allows us to affect the way that an icon is loaded.  Specifically,
1522         * we can be more controlling about the resource path (and allow icons to come from other
1523         * packages).
1524         *
1525         * TODO: This is 100% identical to the version in SearchDialog.java
1526         *
1527         * @param v ImageView to receive an image
1528         * @param value the value retrieved from the cursor
1529         */
1530        @Override
1531        public void setViewImage(ImageView v, String value) {
1532            int resID;
1533            Drawable img = null;
1534
1535            try {
1536                resID = Integer.parseInt(value);
1537                if (resID != 0) {
1538                    img = mProviderResources.getDrawable(resID);
1539                }
1540            } catch (NumberFormatException nfe) {
1541                // img = null;
1542            } catch (NotFoundException e2) {
1543                // img = null;
1544            }
1545
1546            // finally, set the image to whatever we've gotten
1547            v.setImageDrawable(img);
1548        }
1549
1550        /**
1551         * This method is overridden purely to provide a bit of protection against
1552         * flaky content providers.
1553         *
1554         * TODO: This is 100% identical to the version in SearchDialog.java
1555         *
1556         * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
1557         */
1558        @Override
1559        public View getView(int position, View convertView, ViewGroup parent) {
1560            try {
1561                return super.getView(position, convertView, parent);
1562            } catch (RuntimeException e) {
1563                Log.w(TAG, "Search Suggestions cursor returned exception " + e.toString());
1564                // what can I return here?
1565                View v = newView(mContext, mCursor, parent);
1566                if (v != null) {
1567                    TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1);
1568                    tv.setText(e.toString());
1569                }
1570                return v;
1571            }
1572        }
1573
1574    }
1575
1576    /**
1577     * Implements OnItemClickListener
1578     */
1579    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1580        // Log.d(LOG_TAG, "onItemClick() position " + position);
1581        launchSuggestion(mSuggestionsAdapter, position);
1582    }
1583
1584    /**
1585     * Implements OnItemSelectedListener
1586     */
1587     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
1588         // Log.d(LOG_TAG, "onItemSelected() position " + position);
1589         jamSuggestionQuery(true, parent, position);
1590     }
1591
1592     /**
1593      * Implements OnItemSelectedListener
1594      */
1595     public void onNothingSelected(AdapterView<?> parent) {
1596         // Log.d(LOG_TAG, "onNothingSelected()");
1597     }
1598
1599    /**
1600     * Debugging Support
1601     */
1602
1603    /**
1604     * For debugging only, sample the millisecond clock and log it.
1605     * Uses AtomicLong so we can use in multiple threads
1606     */
1607    private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
1608    private void dbgLogTiming(final String caller) {
1609        long millis = SystemClock.uptimeMillis();
1610        long oldTime = mLastLogTime.getAndSet(millis);
1611        long delta = millis - oldTime;
1612        final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
1613        Log.d(LOG_TAG,report);
1614    }
1615}
1616