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