SearchActivity.java revision 13904ef3365894e342285492c0ffeb2287504916
1/*
2 * Copyright (C) 2009 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 com.android.quicksearchbox;
18
19import com.android.common.Search;
20import com.android.quicksearchbox.ui.CorpusViewFactory;
21import com.android.quicksearchbox.ui.QueryTextView;
22import com.android.quicksearchbox.ui.SuggestionClickListener;
23import com.android.quicksearchbox.ui.SuggestionsAdapter;
24import com.android.quicksearchbox.ui.SuggestionsView;
25import com.android.quicksearchbox.util.Consumer;
26import com.android.quicksearchbox.util.Consumers;
27import com.google.common.annotations.VisibleForTesting;
28import com.google.common.base.CharMatcher;
29
30import android.app.Activity;
31import android.app.SearchManager;
32import android.content.DialogInterface;
33import android.content.Intent;
34import android.database.DataSetObserver;
35import android.graphics.drawable.Drawable;
36import android.net.Uri;
37import android.os.Bundle;
38import android.os.Debug;
39import android.os.Handler;
40import android.text.Editable;
41import android.text.TextUtils;
42import android.text.TextWatcher;
43import android.util.Log;
44import android.view.KeyEvent;
45import android.view.Menu;
46import android.view.View;
47import android.view.View.OnFocusChangeListener;
48import android.view.inputmethod.CompletionInfo;
49import android.view.inputmethod.InputMethodManager;
50import android.widget.AbsListView;
51import android.widget.ImageButton;
52import android.widget.ImageView;
53
54import java.io.File;
55import java.util.ArrayList;
56import java.util.Arrays;
57import java.util.Collection;
58import java.util.List;
59import java.util.Set;
60
61/**
62 * The main activity for Quick Search Box. Shows the search UI.
63 *
64 */
65public class SearchActivity extends Activity {
66
67    private static final boolean DBG = false;
68    private static final String TAG = "QSB.SearchActivity";
69    private static final boolean TRACE = false;
70
71    private static final String SCHEME_CORPUS = "qsb.corpus";
72
73    public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS
74            = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS";
75
76    // The string used for privateImeOptions to identify to the IME that it should not show
77    // a microphone button since one already exists in the search dialog.
78    // TODO: This should move to android-common or something.
79    private static final String IME_OPTION_NO_MICROPHONE = "nm";
80
81    // Keys for the saved instance state.
82    private static final String INSTANCE_KEY_CORPUS = "corpus";
83    private static final String INSTANCE_KEY_QUERY = "query";
84
85    // Measures time from for last onCreate()/onNewIntent() call.
86    private LatencyTracker mStartLatencyTracker;
87    // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume().
88    private boolean mStarting;
89    // True if the user has taken some action, e.g. launching a search, voice search,
90    // or suggestions, since QSB was last started.
91    private boolean mTookAction;
92
93    private CorpusSelectionDialog mCorpusSelectionDialog;
94
95    protected SuggestionsAdapter mSuggestionsAdapter;
96
97    // The list adapter for showing results other than query completion
98    // suggestions
99    protected SuggestionsAdapter mResultsAdapter;
100
101    private CorporaObserver mCorporaObserver;
102
103    protected QueryTextView mQueryTextView;
104    // True if the query was empty on the previous call to updateQuery()
105    protected boolean mQueryWasEmpty = true;
106    protected Drawable mQueryTextEmptyBg;
107    protected Drawable mQueryTextNotEmptyBg;
108
109    protected SuggestionsView mSuggestionsView;
110
111    // View that shows the results other than the query completions
112    protected SuggestionsView mResultsView;
113
114    protected boolean mSeparateResults;
115
116    protected ImageButton mSearchGoButton;
117    protected ImageButton mVoiceSearchButton;
118    protected ImageButton mCorpusIndicator;
119
120    protected ImageView mSettingsButton;
121
122    private Corpus mCorpus;
123    private Bundle mAppSearchData;
124    private boolean mUpdateSuggestions;
125
126    private final Handler mHandler = new Handler();
127    private final Runnable mUpdateSuggestionsTask = new Runnable() {
128        public void run() {
129            updateSuggestions(getQuery());
130        }
131    };
132
133    private final Runnable mShowInputMethodTask = new Runnable() {
134        public void run() {
135            showInputMethodForQuery();
136        }
137    };
138
139    private OnDestroyListener mDestroyListener;
140
141    /** Called when the activity is first created. */
142    @Override
143    public void onCreate(Bundle savedInstanceState) {
144        if (TRACE) startMethodTracing();
145        recordStartTime();
146        if (DBG) Log.d(TAG, "onCreate()");
147        super.onCreate(savedInstanceState);
148
149        setContentView();
150
151        SuggestListFocusListener suggestionFocusListener = new SuggestListFocusListener();
152        SuggestionsViewKeyListener suggestionViewKeyListener = new SuggestionsViewKeyListener();
153        InputMethodCloser imeCloser = new InputMethodCloser();
154
155        mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text);
156
157        mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
158        mSuggestionsView.setOnScrollListener(imeCloser);
159        mSuggestionsView.setOnKeyListener(suggestionViewKeyListener);
160        mSuggestionsView.setOnFocusChangeListener(suggestionFocusListener);
161
162        mResultsView = (SuggestionsView) findViewById(R.id.shortcuts);
163        mSeparateResults = mResultsView != null;
164
165        mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter();
166        mSuggestionsAdapter.setSuggestionClickListener(new ClickHandler(mSuggestionsAdapter));
167        mSuggestionsAdapter.setOnFocusChangeListener(suggestionFocusListener);
168
169        if (mSeparateResults) {
170            mResultsAdapter = getQsbApplication().createSuggestionsAdapter();
171            mResultsAdapter.setSuggestionClickListener(new ClickHandler(mResultsAdapter));
172            mResultsAdapter.setOnFocusChangeListener(suggestionFocusListener);
173            mResultsView.setOnScrollListener(imeCloser);
174            mResultsView.setOnFocusChangeListener(suggestionFocusListener);
175            mResultsView.setOnKeyListener(suggestionViewKeyListener);
176        }
177
178        mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
179        mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
180        mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator);
181        mSettingsButton = (ImageView) findViewById(R.id.settings_icon);
182
183        mQueryTextView.addTextChangedListener(new SearchTextWatcher());
184        mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener());
185        mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
186        mQueryTextView.setSuggestionClickListener(new ClickHandler(mSuggestionsAdapter));
187        mQueryTextEmptyBg = mQueryTextView.getBackground();
188
189        if (mCorpusIndicator != null) {
190            mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener());
191        }
192
193        mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
194
195        mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener());
196
197        if (mSettingsButton != null) {
198            mSettingsButton.setOnClickListener(new SettingsButtonClickListener());
199        }
200
201        ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener();
202        mSearchGoButton.setOnKeyListener(buttonsKeyListener);
203        mVoiceSearchButton.setOnKeyListener(buttonsKeyListener);
204        if (mCorpusIndicator != null) {
205            mCorpusIndicator.setOnKeyListener(buttonsKeyListener);
206        }
207
208        mUpdateSuggestions = true;
209
210        // First get setup from intent
211        Intent intent = getIntent();
212        setupFromIntent(intent);
213        // Then restore any saved instance state
214        restoreInstanceState(savedInstanceState);
215
216        mSuggestionsAdapter.registerDataSetObserver(new SuggestionsObserver());
217
218        // Do this at the end, to avoid updating the list view when setSource()
219        // is called.
220        mSuggestionsView.setAdapter(mSuggestionsAdapter);
221
222        if (mSeparateResults) {
223            mResultsAdapter.registerDataSetObserver(new SuggestionsObserver());
224            mResultsView.setAdapter(mResultsAdapter);
225        }
226        mCorporaObserver = new CorporaObserver();
227        getCorpora().registerDataSetObserver(mCorporaObserver);
228    }
229
230    protected void setContentView() {
231        setContentView(R.layout.search_activity);
232    }
233
234    private void startMethodTracing() {
235        File traceDir = getDir("traces", 0);
236        String traceFile = new File(traceDir, "qsb.trace").getAbsolutePath();
237        Debug.startMethodTracing(traceFile);
238    }
239
240    @Override
241    protected void onNewIntent(Intent intent) {
242        if (DBG) Log.d(TAG, "onNewIntent()");
243        recordStartTime();
244        setIntent(intent);
245        setupFromIntent(intent);
246    }
247
248    private void recordStartTime() {
249        mStartLatencyTracker = new LatencyTracker();
250        mStarting = true;
251        mTookAction = false;
252    }
253
254    protected void restoreInstanceState(Bundle savedInstanceState) {
255        if (savedInstanceState == null) return;
256        String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS);
257        String query = savedInstanceState.getString(INSTANCE_KEY_QUERY);
258        setCorpus(corpusName);
259        setQuery(query, false);
260    }
261
262    @Override
263    protected void onSaveInstanceState(Bundle outState) {
264        super.onSaveInstanceState(outState);
265        // We don't save appSearchData, since we always get the value
266        // from the intent and the user can't change it.
267
268        outState.putString(INSTANCE_KEY_CORPUS, getCorpusName());
269        outState.putString(INSTANCE_KEY_QUERY, getQuery());
270    }
271
272    private void setupFromIntent(Intent intent) {
273        if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")");
274        String corpusName = getCorpusNameFromUri(intent.getData());
275        String query = intent.getStringExtra(SearchManager.QUERY);
276        Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA);
277        boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false);
278
279        setCorpus(corpusName);
280        setQuery(query, selectAll);
281        mAppSearchData = appSearchData;
282
283        if (startedIntoCorpusSelectionDialog()) {
284            showCorpusSelectionDialog();
285        }
286    }
287
288    public boolean startedIntoCorpusSelectionDialog() {
289        return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction());
290    }
291
292    /**
293     * Removes corpus selector intent action, so that BACK works normally after
294     * dismissing and reopening the corpus selector.
295     */
296    private void clearStartedIntoCorpusSelectionDialog() {
297        Intent oldIntent = getIntent();
298        if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) {
299            Intent newIntent = new Intent(oldIntent);
300            newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
301            setIntent(newIntent);
302        }
303    }
304
305    public static Uri getCorpusUri(Corpus corpus) {
306        if (corpus == null) return null;
307        return new Uri.Builder()
308                .scheme(SCHEME_CORPUS)
309                .authority(corpus.getName())
310                .build();
311    }
312
313    private String getCorpusNameFromUri(Uri uri) {
314        if (uri == null) return null;
315        if (!SCHEME_CORPUS.equals(uri.getScheme())) return null;
316        return uri.getAuthority();
317    }
318
319    private Corpus getCorpus(String sourceName) {
320        if (sourceName == null) return null;
321        Corpus corpus = getCorpora().getCorpus(sourceName);
322        if (corpus == null) {
323            Log.w(TAG, "Unknown corpus " + sourceName);
324            return null;
325        }
326        return corpus;
327    }
328
329    private void setCorpus(String corpusName) {
330        if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")");
331        mCorpus = getCorpus(corpusName);
332        Drawable sourceIcon;
333        if (mCorpus == null) {
334            sourceIcon = getCorpusViewFactory().getGlobalSearchIcon();
335        } else {
336            sourceIcon = mCorpus.getCorpusIcon();
337        }
338        if (separateResults()) {
339            mSuggestionsAdapter.setPromoter(createSuggestionsPromoter(null));
340            mResultsAdapter.setCorpus(mCorpus);
341            mResultsAdapter.setPromoter(createResultsPromoter(mCorpus));
342        } else {
343            mSuggestionsAdapter.setCorpus(mCorpus);
344            mSuggestionsAdapter.setPromoter(createSuggestionsPromoter(mCorpus));
345        }
346        if (mCorpusIndicator != null) {
347            mCorpusIndicator.setImageDrawable(sourceIcon);
348        }
349
350        updateUi(getQuery().length() == 0);
351    }
352
353    private Promoter createSuggestionsPromoter(Corpus corpus) {
354        if (separateResults()) {
355            return getQsbApplication().createWebPromoter();
356        } else if (corpus == null) {
357            return getQsbApplication().createBlendingPromoter();
358        } else {
359            return getQsbApplication().createSingleCorpusPromoter();
360        }
361    }
362
363    private Promoter createResultsPromoter(Corpus corpus) {
364        if (separateResults()) {
365            if (corpus == null) {
366                return getQsbApplication().createResultsPromoter();
367            } else {
368                return getQsbApplication().createSingleCorpusPromoter();
369            }
370        } else {
371            return null;
372        }
373    }
374
375    private String getCorpusName() {
376        return mCorpus == null ? null : mCorpus.getName();
377    }
378
379    private QsbApplication getQsbApplication() {
380        return QsbApplication.get(this);
381    }
382
383    protected boolean separateResults() {
384        return mSeparateResults;
385    }
386
387    private Config getConfig() {
388        return getQsbApplication().getConfig();
389    }
390
391    private Corpora getCorpora() {
392        return getQsbApplication().getCorpora();
393    }
394
395    private CorpusRanker getCorpusRanker() {
396        return getQsbApplication().getCorpusRanker();
397    }
398
399    private ShortcutRepository getShortcutRepository() {
400        return getQsbApplication().getShortcutRepository();
401    }
402
403    private SuggestionsProvider getSuggestionsProvider() {
404        return getQsbApplication().getSuggestionsProvider();
405    }
406
407    private CorpusViewFactory getCorpusViewFactory() {
408        return getQsbApplication().getCorpusViewFactory();
409    }
410
411    private VoiceSearch getVoiceSearch() {
412        return QsbApplication.get(this).getVoiceSearch();
413    }
414
415    private Logger getLogger() {
416        return getQsbApplication().getLogger();
417    }
418
419    @VisibleForTesting
420    public void setOnDestroyListener(OnDestroyListener l) {
421        mDestroyListener = l;
422    }
423
424    @Override
425    protected void onDestroy() {
426        if (DBG) Log.d(TAG, "onDestroy()");
427        getCorpora().unregisterDataSetObserver(mCorporaObserver);
428        mSuggestionsView.setAdapter(null);  // closes mSuggestionsAdapter
429        if (mResultsView != null) {
430            mResultsView.setAdapter(null);
431        }
432        super.onDestroy();
433        if (mDestroyListener != null) {
434            mDestroyListener.onDestroyed();
435        }
436    }
437
438    @Override
439    protected void onStop() {
440        if (DBG) Log.d(TAG, "onStop()");
441        if (!mTookAction) {
442            // TODO: This gets logged when starting other activities, e.g. by opening the search
443            // settings, or clicking a notification in the status bar.
444            // TODO we should log both sets of suggestions in 2-pane mode
445            getLogger().logExit(getCurrentSuggestions(), getQuery().length());
446        }
447        // Close all open suggestion cursors. The query will be redone in onResume()
448        // if we come back to this activity.
449        mSuggestionsAdapter.setSuggestions(null);
450        if (mResultsAdapter != null) {
451            mResultsAdapter.setSuggestions(null);
452        }
453        getQsbApplication().getShortcutRefresher().reset();
454        dismissCorpusSelectionDialog();
455        super.onStop();
456    }
457
458    @Override
459    protected void onRestart() {
460        if (DBG) Log.d(TAG, "onRestart()");
461        super.onRestart();
462    }
463
464    @Override
465    protected void onResume() {
466        if (DBG) Log.d(TAG, "onResume()");
467        super.onResume();
468        updateSuggestionsBuffered();
469        if (!isCorpusSelectionDialogShowing()) {
470            mQueryTextView.requestFocus();
471        }
472        if (TRACE) Debug.stopMethodTracing();
473    }
474
475    @Override
476    public boolean onCreateOptionsMenu(Menu menu) {
477        super.onCreateOptionsMenu(menu);
478        SearchSettings.addSearchSettingsMenuItem(this, menu);
479        return true;
480    }
481
482    @Override
483    public void onWindowFocusChanged(boolean hasFocus) {
484        super.onWindowFocusChanged(hasFocus);
485        if (hasFocus) {
486            // Launch the IME after a bit
487            mHandler.postDelayed(mShowInputMethodTask, 0);
488        }
489    }
490
491    protected String getQuery() {
492        CharSequence q = mQueryTextView.getText();
493        return q == null ? "" : q.toString();
494    }
495
496    /**
497     * Sets the text in the query box. Does not update the suggestions.
498     */
499    private void setQuery(String query, boolean selectAll) {
500        mUpdateSuggestions = false;
501        mQueryTextView.setText(query);
502        mQueryTextView.setTextSelection(selectAll);
503        mUpdateSuggestions = true;
504    }
505
506    protected void updateUi(boolean queryEmpty) {
507        updateQueryTextView(queryEmpty);
508        updateSearchGoButton(queryEmpty);
509        updateVoiceSearchButton(queryEmpty);
510    }
511
512    private void updateQueryTextView(boolean queryEmpty) {
513        if (queryEmpty) {
514            if (isSearchCorpusWeb()) {
515                mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg);
516                mQueryTextView.setHint(null);
517            } else {
518                if (mQueryTextNotEmptyBg == null) {
519                    mQueryTextNotEmptyBg =
520                            getResources().getDrawable(R.drawable.textfield_search_empty);
521                }
522                mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg);
523                mQueryTextView.setHint(mCorpus.getHint());
524            }
525        } else {
526            mQueryTextView.setBackgroundResource(R.drawable.textfield_search);
527        }
528    }
529
530    private void updateSearchGoButton(boolean queryEmpty) {
531        if (queryEmpty) {
532            mSearchGoButton.setVisibility(View.GONE);
533        } else {
534            mSearchGoButton.setVisibility(View.VISIBLE);
535        }
536    }
537
538    protected void updateVoiceSearchButton(boolean queryEmpty) {
539        if (queryEmpty && getVoiceSearch().shouldShowVoiceSearch(mCorpus)) {
540            mVoiceSearchButton.setVisibility(View.VISIBLE);
541            mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
542        } else {
543            mVoiceSearchButton.setVisibility(View.GONE);
544            mQueryTextView.setPrivateImeOptions(null);
545        }
546    }
547
548    protected void showCorpusSelectionDialog() {
549        if (mCorpusSelectionDialog == null) {
550            mCorpusSelectionDialog = createCorpusSelectionDialog();
551            mCorpusSelectionDialog.setOwnerActivity(this);
552            mCorpusSelectionDialog.setOnDismissListener(new CorpusSelectorDismissListener());
553            mCorpusSelectionDialog.setOnCorpusSelectedListener(new CorpusSelectionListener());
554        }
555        mCorpusSelectionDialog.show(mCorpus);
556    }
557
558    protected CorpusSelectionDialog createCorpusSelectionDialog() {
559        return new CorpusSelectionDialog(this);
560    }
561
562    protected boolean isCorpusSelectionDialogShowing() {
563        return mCorpusSelectionDialog != null && mCorpusSelectionDialog.isShowing();
564    }
565
566    protected void dismissCorpusSelectionDialog() {
567        if (mCorpusSelectionDialog != null) {
568            mCorpusSelectionDialog.dismiss();
569        }
570    }
571
572    /**
573     * @return true if a search was performed as a result of this click, false otherwise.
574     */
575    protected boolean onSearchClicked(int method) {
576        String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' ');
577        if (DBG) Log.d(TAG, "Search clicked, query=" + query);
578
579        // Don't do empty queries
580        if (TextUtils.getTrimmedLength(query) == 0) return false;
581
582        Corpus searchCorpus = getSearchCorpus();
583        if (searchCorpus == null) return false;
584
585        mTookAction = true;
586
587        // Log search start
588        getLogger().logSearch(mCorpus, method, query.length());
589
590        // Create shortcut
591        SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query);
592        if (searchShortcut != null) {
593            ListSuggestionCursor cursor = new ListSuggestionCursor(query);
594            cursor.add(searchShortcut);
595            getShortcutRepository().reportClick(cursor, 0);
596        }
597
598        // Start search
599        Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData);
600        launchIntent(intent);
601        return true;
602    }
603
604    protected void onVoiceSearchClicked() {
605        if (DBG) Log.d(TAG, "Voice Search clicked");
606        Corpus searchCorpus = getSearchCorpus();
607        if (searchCorpus == null) return;
608
609        mTookAction = true;
610
611        // Log voice search start
612        getLogger().logVoiceSearch(searchCorpus);
613
614        // Start voice search
615        Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData);
616        launchIntent(intent);
617    }
618
619    protected void onSettingsClicked() {
620        SearchSettings.launchSettings(this);
621    }
622
623    /**
624     * Gets the corpus to use for any searches. This is the web corpus in "All" mode,
625     * and the selected corpus otherwise.
626     */
627    protected Corpus getSearchCorpus() {
628        if (mCorpus != null) {
629            return mCorpus;
630        } else {
631            Corpus webCorpus = getCorpora().getWebCorpus();
632            if (webCorpus == null) {
633                Log.e(TAG, "No web corpus");
634            }
635            return webCorpus;
636        }
637    }
638
639    /**
640     * Checks if the corpus used for typed searches is the web corpus.
641     */
642    protected boolean isSearchCorpusWeb() {
643        Corpus corpus = getSearchCorpus();
644        return corpus != null && corpus.isWebCorpus();
645    }
646
647    @Deprecated
648    protected SuggestionCursor getCurrentSuggestions() {
649        return mSuggestionsAdapter.getCurrentSuggestions();
650    }
651
652    protected SuggestionCursor getCurrentSuggestions(SuggestionsAdapter adapter, int position) {
653        SuggestionCursor suggestions = adapter.getCurrentSuggestions();
654        if (suggestions == null) {
655            return null;
656        }
657        int count = suggestions.getCount();
658        if (position < 0 || position >= count) {
659            Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count);
660            return null;
661        }
662        suggestions.moveTo(position);
663        return suggestions;
664    }
665
666    private Suggestions getSuggestions() {
667        return mSuggestionsAdapter.getSuggestions();
668    }
669
670    protected Set<Corpus> getCurrentIncludedCorpora() {
671        Suggestions suggestions = getSuggestions();
672        return suggestions == null  ? null : suggestions.getIncludedCorpora();
673    }
674
675    protected void launchIntent(Intent intent) {
676        if (intent == null) {
677            return;
678        }
679        try {
680            startActivity(intent);
681        } catch (RuntimeException ex) {
682            // Since the intents for suggestions specified by suggestion providers,
683            // guard against them not being handled, not allowed, etc.
684            Log.e(TAG, "Failed to start " + intent.toUri(0), ex);
685        }
686    }
687
688    protected boolean launchSuggestion(SuggestionsAdapter adapter, int position) {
689        SuggestionCursor suggestions = getCurrentSuggestions(adapter, position);
690        if (suggestions == null) return false;
691
692        if (DBG) Log.d(TAG, "Launching suggestion " + position);
693        mTookAction = true;
694
695        // Log suggestion click
696        getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(),
697                Logger.SUGGESTION_CLICK_TYPE_LAUNCH);
698
699        // Create shortcut
700        getShortcutRepository().reportClick(suggestions, position);
701
702        // Launch intent
703        suggestions.moveTo(position);
704        Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData);
705        launchIntent(intent);
706
707        return true;
708    }
709
710    protected void clickedQuickContact(SuggestionsAdapter adapter, int position) {
711        SuggestionCursor suggestions = getCurrentSuggestions(adapter, position);
712        if (suggestions == null) return;
713
714        if (DBG) Log.d(TAG, "Used suggestion " + position);
715        mTookAction = true;
716
717        // Log suggestion click
718        getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(),
719                Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT);
720
721        // Create shortcut
722        getShortcutRepository().reportClick(suggestions, position);
723    }
724
725    protected boolean onSuggestionLongClicked(SuggestionsAdapter adapter, int position) {
726        if (DBG) Log.d(TAG, "Long clicked on suggestion " + position);
727        return false;
728    }
729
730    protected boolean onSuggestionKeyDown(SuggestionsAdapter adapter,
731            int position, int keyCode, KeyEvent event) {
732        // Treat enter or search as a click
733        if (       keyCode == KeyEvent.KEYCODE_ENTER
734                || keyCode == KeyEvent.KEYCODE_SEARCH
735                || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
736            return launchSuggestion(adapter, position);
737        }
738
739        return false;
740    }
741
742    protected void refineSuggestion(SuggestionsAdapter adapter, int position) {
743        if (DBG) Log.d(TAG, "query refine clicked, pos " + position);
744        SuggestionCursor suggestions = getCurrentSuggestions(adapter, position);
745        if (suggestions == null) {
746            return;
747        }
748        String query = suggestions.getSuggestionQuery();
749        if (TextUtils.isEmpty(query)) {
750            return;
751        }
752
753        // Log refine click
754        getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(),
755                Logger.SUGGESTION_CLICK_TYPE_REFINE);
756
757        // Put query + space in query text view
758        String queryWithSpace = query + ' ';
759        setQuery(queryWithSpace, false);
760        updateSuggestions(queryWithSpace);
761        mQueryTextView.requestFocus();
762    }
763
764    /**
765     * Hides the input method.
766     */
767    protected void considerHidingInputMethod() {
768        if (getConfig().isKeyboardDismissedOnScroll()) {
769            mQueryTextView.hideInputMethod();
770        }
771    }
772
773    protected void showInputMethodForQuery() {
774        mQueryTextView.showInputMethod();
775    }
776
777    protected void onSuggestionListFocusChange(SuggestionsView suggestions, boolean focused) {
778    }
779
780    protected void onQueryTextViewFocusChange(boolean focused) {
781    }
782
783    /**
784     * Hides the input method when the suggestions get focus.
785     */
786    private class SuggestListFocusListener implements OnFocusChangeListener {
787        public void onFocusChange(View v, boolean focused) {
788            if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
789            if (focused && v instanceof SuggestionsView) {
790                SuggestionsView view = (SuggestionsView) v;
791                considerHidingInputMethod();
792                onSuggestionListFocusChange(view, focused);
793            }
794        }
795    }
796
797    private class QueryTextViewFocusListener implements OnFocusChangeListener {
798        public void onFocusChange(View v, boolean focused) {
799            if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
800            if (focused) {
801                // The query box got focus, show the input method
802                showInputMethodForQuery();
803            }
804            onQueryTextViewFocusChange(focused);
805        }
806    }
807
808    private void updateSuggestionsBuffered() {
809        mHandler.removeCallbacks(mUpdateSuggestionsTask);
810        long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
811        mHandler.postDelayed(mUpdateSuggestionsTask, delay);
812    }
813
814    private void gotSuggestions(Suggestions suggestions) {
815        if (mStarting) {
816            mStarting = false;
817            String source = getIntent().getStringExtra(Search.SOURCE);
818            int latency = mStartLatencyTracker.getLatency();
819            getLogger().logStart(latency, source, mCorpus,
820                    suggestions == null ? null : suggestions.getExpectedCorpora());
821            getQsbApplication().onStartupComplete();
822        }
823    }
824
825    private List<Corpus> getCorporaToQuery() {
826        if (mCorpus == null) {
827            // No corpus selected, use all enabled corpora
828            // TODO: This should be done asynchronously, since it can be expensive
829            return getCorpusRanker().getRankedCorpora();
830        } else {
831            List<Corpus> corpora = new ArrayList<Corpus>();
832            if (separateResults()) {
833                // In two-pane mode, we always need to query the web corpus.
834                Corpus webCorpus = getCorpora().getWebCorpus();
835                if (webCorpus != null && webCorpus != mCorpus) corpora.add(webCorpus);
836            }
837            // Query the selected corpus
838            corpora.add(mCorpus);
839            return corpora;
840        }
841    }
842
843    protected void getShortcutsForQuery(String query, Collection<Corpus> corporaToQuery,
844            final Suggestions suggestions) {
845        ShortcutRepository shortcutRepo = getShortcutRepository();
846        if (shortcutRepo == null) return;
847        Consumer<ShortcutCursor> consumer = Consumers.createAsyncCloseableConsumer(mHandler,
848                new Consumer<ShortcutCursor>() {
849            public boolean consume(ShortcutCursor shortcuts) {
850                suggestions.setShortcuts(shortcuts);
851                return true;
852            }
853        });
854        shortcutRepo.getShortcutsForQuery(query, corporaToQuery, consumer);
855    }
856
857    protected void updateSuggestions(String query) {
858        query = CharMatcher.WHITESPACE.trimLeadingFrom(query);
859        if (DBG) Log.d(TAG, "getSuggestions(\""+query+"\","+mCorpus + ")");
860        getQsbApplication().getSourceTaskExecutor().cancelPendingTasks();
861
862        List<Corpus> corporaToQuery = getCorporaToQuery();
863        Suggestions suggestions = getSuggestionsProvider().getSuggestions(
864                query, corporaToQuery);
865        getShortcutsForQuery(query, corporaToQuery, suggestions);
866
867        // Log start latency if this is the first suggestions update
868        gotSuggestions(suggestions);
869
870        suggestions.acquire();
871        mSuggestionsAdapter.setSuggestions(suggestions);
872        if (mResultsAdapter != null) {
873            suggestions.acquire();
874            mResultsAdapter.setSuggestions(suggestions);
875        }
876    }
877
878    /**
879     * If the input method is in fullscreen mode, and the selector corpus
880     * is All or Web, use the web search suggestions as completions.
881     */
882    protected void updateInputMethodSuggestions() {
883        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
884        if (imm == null || !imm.isFullscreenMode()) return;
885        Suggestions suggestions = mSuggestionsAdapter.getSuggestions();
886        if (suggestions == null) return;
887        CompletionInfo[] completions = webSuggestionsToCompletions(suggestions);
888        if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")");
889        imm.displayCompletions(mQueryTextView, completions);
890    }
891
892    private CompletionInfo[] webSuggestionsToCompletions(Suggestions suggestions) {
893        // TODO: This should also include include web search shortcuts
894        CorpusResult cursor = suggestions.getWebResult();
895        if (cursor == null) return null;
896        int count = cursor.getCount();
897        ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count);
898        boolean usingWebCorpus = isSearchCorpusWeb();
899        for (int i = 0; i < count; i++) {
900            cursor.moveTo(i);
901            if (!usingWebCorpus || cursor.isWebSearchSuggestion()) {
902                String text1 = cursor.getSuggestionText1();
903                completions.add(new CompletionInfo(i, i, text1));
904            }
905        }
906        return completions.toArray(new CompletionInfo[completions.size()]);
907    }
908
909    private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
910        if (!event.isSystem() && !isDpadKey(keyCode)) {
911            if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
912            if (mQueryTextView.requestFocus()) {
913                return mQueryTextView.dispatchKeyEvent(event);
914            }
915        }
916        return false;
917    }
918
919    private boolean isDpadKey(int keyCode) {
920        switch (keyCode) {
921            case KeyEvent.KEYCODE_DPAD_UP:
922            case KeyEvent.KEYCODE_DPAD_DOWN:
923            case KeyEvent.KEYCODE_DPAD_LEFT:
924            case KeyEvent.KEYCODE_DPAD_RIGHT:
925            case KeyEvent.KEYCODE_DPAD_CENTER:
926                return true;
927            default:
928                return false;
929        }
930    }
931
932    /**
933     * Filters the suggestions list when the search text changes.
934     */
935    private class SearchTextWatcher implements TextWatcher {
936        public void afterTextChanged(Editable s) {
937            boolean empty = s.length() == 0;
938            if (empty != mQueryWasEmpty) {
939                mQueryWasEmpty = empty;
940                updateUi(empty);
941            }
942            if (mUpdateSuggestions) {
943                updateSuggestionsBuffered();
944            }
945        }
946
947        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
948        }
949
950        public void onTextChanged(CharSequence s, int start, int before, int count) {
951        }
952    }
953
954    /**
955     * Handles non-text keys in the query text view.
956     */
957    private class QueryTextViewKeyListener implements View.OnKeyListener {
958        public boolean onKey(View view, int keyCode, KeyEvent event) {
959            // Handle IME search action key
960            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
961                // if no action was taken, consume the key event so that the keyboard
962                // remains on screen.
963                return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
964            }
965            return false;
966        }
967    }
968
969    /**
970     * Handles key events on the search and voice search buttons,
971     * by refocusing to EditText.
972     */
973    private class ButtonsKeyListener implements View.OnKeyListener {
974        public boolean onKey(View v, int keyCode, KeyEvent event) {
975            return forwardKeyToQueryTextView(keyCode, event);
976        }
977    }
978
979    /**
980     * Handles key events on the suggestions list view.
981     */
982    private class SuggestionsViewKeyListener implements View.OnKeyListener {
983        public boolean onKey(View v, int keyCode, KeyEvent event) {
984            if (event.getAction() == KeyEvent.ACTION_DOWN
985                    && v instanceof SuggestionsView) {
986                SuggestionsView view = ((SuggestionsView) v);
987                int position = view.getSelectedPosition();
988                if (onSuggestionKeyDown(view.getAdapter(), position, keyCode, event)) {
989                    return true;
990                }
991            }
992            return forwardKeyToQueryTextView(keyCode, event);
993        }
994    }
995
996    private class InputMethodCloser implements SuggestionsView.OnScrollListener {
997
998        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
999                int totalItemCount) {
1000        }
1001
1002        public void onScrollStateChanged(AbsListView view, int scrollState) {
1003            considerHidingInputMethod();
1004        }
1005    }
1006
1007    private class ClickHandler implements SuggestionClickListener {
1008        private final SuggestionsAdapter mAdapter;
1009
1010        public ClickHandler(SuggestionsAdapter suggestionsAdapter) {
1011            mAdapter = suggestionsAdapter;
1012        }
1013
1014        public void onSuggestionQuickContactClicked(int position) {
1015            clickedQuickContact(mAdapter, position);
1016        }
1017
1018        public void onSuggestionClicked(int position) {
1019            launchSuggestion(mAdapter, position);
1020        }
1021
1022        public boolean onSuggestionLongClicked(int position) {
1023            return SearchActivity.this.onSuggestionLongClicked(mAdapter, position);
1024        }
1025
1026        public void onSuggestionQueryRefineClicked(int position) {
1027            refineSuggestion(mAdapter, position);
1028        }
1029    }
1030
1031    /**
1032     * Listens for clicks on the source selector.
1033     */
1034    private class SearchGoButtonClickListener implements View.OnClickListener {
1035        public void onClick(View view) {
1036            onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
1037        }
1038    }
1039
1040    /**
1041     * Listens for clicks on the search button.
1042     */
1043    private class CorpusIndicatorClickListener implements View.OnClickListener {
1044        public void onClick(View view) {
1045            showCorpusSelectionDialog();
1046        }
1047    }
1048
1049    private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
1050        public void onDismiss(DialogInterface dialog) {
1051            if (DBG) Log.d(TAG, "Corpus selector dismissed");
1052            clearStartedIntoCorpusSelectionDialog();
1053        }
1054    }
1055
1056    private class CorpusSelectionListener
1057            implements CorpusSelectionDialog.OnCorpusSelectedListener {
1058        public void onCorpusSelected(String corpusName) {
1059            setCorpus(corpusName);
1060            updateSuggestions(getQuery());
1061            mQueryTextView.requestFocus();
1062            showInputMethodForQuery();
1063        }
1064    }
1065
1066    /**
1067     * Listens for clicks on the voice search button.
1068     */
1069    private class VoiceSearchButtonClickListener implements View.OnClickListener {
1070        public void onClick(View view) {
1071            onVoiceSearchClicked();
1072        }
1073    }
1074
1075    /**
1076     * Listens for clicks on the settings button.
1077     */
1078    private class SettingsButtonClickListener implements View.OnClickListener {
1079        public void onClick(View view) {
1080            onSettingsClicked();
1081        }
1082    }
1083
1084    private class CorporaObserver extends DataSetObserver {
1085        @Override
1086        public void onChanged() {
1087            setCorpus(getCorpusName());
1088            updateSuggestions(getQuery());
1089        }
1090    }
1091
1092    private class SuggestionsObserver extends DataSetObserver {
1093        @Override
1094        public void onChanged() {
1095            updateInputMethodSuggestions();
1096        }
1097    }
1098
1099    public interface OnDestroyListener {
1100        void onDestroyed();
1101    }
1102
1103}
1104