SearchActivity.java revision 36cdac7349a76aa1db70ed7e8fa554ed54ad639d
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
601                || keyCode == KeyEvent.KEYCODE_SEARCH
602                || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
603            return launchSuggestion(position);
604        }
605
606        return false;
607    }
608
609    protected void refineSuggestion(int position) {
610        if (DBG) Log.d(TAG, "query refine clicked, pos " + position);
611        SuggestionCursor suggestions = getCurrentSuggestions(position);
612        if (suggestions == null) {
613            return;
614        }
615        String query = suggestions.getSuggestionQuery();
616        if (TextUtils.isEmpty(query)) {
617            return;
618        }
619
620        // Log refine click
621        getLogger().logRefine(position, suggestions, getCurrentIncludedCorpora());
622
623        // Put query + space in query text view
624        String queryWithSpace = query + ' ';
625        setQuery(queryWithSpace, false);
626        updateSuggestions(queryWithSpace);
627        mQueryTextView.requestFocus();
628    }
629
630    protected int getSelectedPosition() {
631        return mSuggestionsView.getSelectedPosition();
632    }
633
634    /**
635     * Hides the input method.
636     */
637    protected void hideInputMethod() {
638        mQueryTextView.hideInputMethod();
639    }
640
641    protected void showInputMethodForQuery() {
642        mQueryTextView.showInputMethod();
643    }
644
645    protected void onSuggestionListFocusChange(boolean focused) {
646    }
647
648    protected void onQueryTextViewFocusChange(boolean focused) {
649    }
650
651    /**
652     * Hides the input method when the suggestions get focus.
653     */
654    private class SuggestListFocusListener implements OnFocusChangeListener {
655        public void onFocusChange(View v, boolean focused) {
656            if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
657            if (focused) {
658                // The suggestions list got focus, hide the input method
659                hideInputMethod();
660            }
661            onSuggestionListFocusChange(focused);
662        }
663    }
664
665    private class QueryTextViewFocusListener implements OnFocusChangeListener {
666        public void onFocusChange(View v, boolean focused) {
667            if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
668            if (focused) {
669                // The query box got focus, show the input method
670                showInputMethodForQuery();
671            }
672            onQueryTextViewFocusChange(focused);
673        }
674    }
675
676    private int getMaxSuggestions() {
677        Config config = getConfig();
678        return mCorpus == null
679                ? config.getMaxPromotedSuggestions()
680                : config.getMaxResultsPerSource();
681    }
682
683    private void updateSuggestionsBuffered() {
684        mHandler.removeCallbacks(mUpdateSuggestionsTask);
685        long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
686        mHandler.postDelayed(mUpdateSuggestionsTask, delay);
687    }
688
689    protected void updateSuggestions(String query) {
690
691        query = CharMatcher.WHITESPACE.trimLeadingFrom(query);
692        if (DBG) Log.d(TAG, "getSuggestions(\""+query+"\","+mCorpus + ","+getMaxSuggestions()+")");
693        Suggestions suggestions = getSuggestionsProvider().getSuggestions(
694                query, mCorpus, getMaxSuggestions());
695
696        // Log start latency if this is the first suggestions update
697        if (mStarting) {
698            mStarting = false;
699            String source = getIntent().getStringExtra(Search.SOURCE);
700            int latency = mStartLatencyTracker.getLatency();
701            getLogger().logStart(latency, source, mCorpus, suggestions.getExpectedCorpora());
702            getQsbApplication().onStartupComplete();
703        }
704
705        mSuggestionsAdapter.setSuggestions(suggestions);
706    }
707
708    /**
709     * If the input method is in fullscreen mode, and the selector corpus
710     * is All or Web, use the web search suggestions as completions.
711     */
712    protected void updateInputMethodSuggestions() {
713        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
714        if (imm == null || !imm.isFullscreenMode()) return;
715        Suggestions suggestions = mSuggestionsAdapter.getSuggestions();
716        if (suggestions == null) return;
717        SuggestionCursor cursor = suggestions.getPromoted();
718        if (cursor == null) return;
719        CompletionInfo[] completions = webSuggestionsToCompletions(cursor);
720        if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")");
721        imm.displayCompletions(mQueryTextView, completions);
722    }
723
724    private CompletionInfo[] webSuggestionsToCompletions(SuggestionCursor cursor) {
725        int count = cursor.getCount();
726        ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count);
727        boolean usingWebCorpus = isSearchCorpusWeb();
728        for (int i = 0; i < count; i++) {
729            cursor.moveTo(i);
730            if (!usingWebCorpus || cursor.isWebSearchSuggestion()) {
731                String text1 = cursor.getSuggestionText1();
732                completions.add(new CompletionInfo(i, i, text1));
733            }
734        }
735        return completions.toArray(new CompletionInfo[completions.size()]);
736    }
737
738    private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
739        if (!event.isSystem() && !isDpadKey(keyCode)) {
740            if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
741            if (mQueryTextView.requestFocus()) {
742                return mQueryTextView.dispatchKeyEvent(event);
743            }
744        }
745        return false;
746    }
747
748    private boolean isDpadKey(int keyCode) {
749        switch (keyCode) {
750            case KeyEvent.KEYCODE_DPAD_UP:
751            case KeyEvent.KEYCODE_DPAD_DOWN:
752            case KeyEvent.KEYCODE_DPAD_LEFT:
753            case KeyEvent.KEYCODE_DPAD_RIGHT:
754            case KeyEvent.KEYCODE_DPAD_CENTER:
755                return true;
756            default:
757                return false;
758        }
759    }
760
761    /**
762     * Filters the suggestions list when the search text changes.
763     */
764    private class SearchTextWatcher implements TextWatcher {
765        public void afterTextChanged(Editable s) {
766            boolean empty = s.length() == 0;
767            if (empty != mQueryWasEmpty) {
768                mQueryWasEmpty = empty;
769                updateUi(empty);
770            }
771            if (mUpdateSuggestions) {
772                updateSuggestionsBuffered();
773            }
774        }
775
776        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
777        }
778
779        public void onTextChanged(CharSequence s, int start, int before, int count) {
780        }
781    }
782
783    /**
784     * Handles non-text keys in the query text view.
785     */
786    private class QueryTextViewKeyListener implements View.OnKeyListener {
787        public boolean onKey(View view, int keyCode, KeyEvent event) {
788            // Handle IME search action key
789            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
790                // if no action was taken, consume the key event so that the keyboard
791                // remains on screen.
792                return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
793            }
794            return false;
795        }
796    }
797
798    /**
799     * Handles key events on the search and voice search buttons,
800     * by refocusing to EditText.
801     */
802    private class ButtonsKeyListener implements View.OnKeyListener {
803        public boolean onKey(View v, int keyCode, KeyEvent event) {
804            return forwardKeyToQueryTextView(keyCode, event);
805        }
806    }
807
808    /**
809     * Handles key events on the suggestions list view.
810     */
811    private class SuggestionsViewKeyListener implements View.OnKeyListener {
812        public boolean onKey(View v, int keyCode, KeyEvent event) {
813            if (event.getAction() == KeyEvent.ACTION_DOWN) {
814                int position = getSelectedPosition();
815                if (onSuggestionKeyDown(position, keyCode, event)) {
816                    return true;
817                }
818            }
819            return forwardKeyToQueryTextView(keyCode, event);
820        }
821    }
822
823    private class InputMethodCloser implements SuggestionsView.OnScrollListener {
824
825        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
826                int totalItemCount) {
827        }
828
829        public void onScrollStateChanged(AbsListView view, int scrollState) {
830            hideInputMethod();
831        }
832    }
833
834    private class ClickHandler implements SuggestionClickListener {
835       public void onSuggestionClicked(int position) {
836           launchSuggestion(position);
837       }
838
839       public boolean onSuggestionLongClicked(int position) {
840           return SearchActivity.this.onSuggestionLongClicked(position);
841       }
842
843       public void onSuggestionQueryRefineClicked(int position) {
844           refineSuggestion(position);
845       }
846    }
847
848    /**
849     * Listens for clicks on the source selector.
850     */
851    private class SearchGoButtonClickListener implements View.OnClickListener {
852        public void onClick(View view) {
853            onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
854        }
855    }
856
857    /**
858     * Listens for clicks on the search button.
859     */
860    private class CorpusIndicatorClickListener implements View.OnClickListener {
861        public void onClick(View view) {
862            showCorpusSelectionDialog();
863        }
864    }
865
866    private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
867        public void onDismiss(DialogInterface dialog) {
868            if (DBG) Log.d(TAG, "Corpus selector dismissed");
869            clearStartedIntoCorpusSelectionDialog();
870        }
871    }
872
873    private class CorpusSelectionListener
874            implements CorpusSelectionDialog.OnCorpusSelectedListener {
875        public void onCorpusSelected(String corpusName) {
876            setCorpus(corpusName);
877            updateSuggestions(getQuery());
878            mQueryTextView.requestFocus();
879            showInputMethodForQuery();
880        }
881    }
882
883    /**
884     * Listens for clicks on the voice search button.
885     */
886    private class VoiceSearchButtonClickListener implements View.OnClickListener {
887        public void onClick(View view) {
888            onVoiceSearchClicked();
889        }
890    }
891
892    private class CorporaObserver extends DataSetObserver {
893        @Override
894        public void onChanged() {
895            setCorpus(getCorpusName());
896            updateSuggestions(getQuery());
897        }
898    }
899
900    private class SuggestionsObserver extends DataSetObserver {
901        @Override
902        public void onChanged() {
903            updateInputMethodSuggestions();
904        }
905    }
906
907}
908