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