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