SearchActivity.java revision 2dfefcf67c7c28c8337725ef30d623a47638b374
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.Set;
54
55/**
56 * The main activity for Quick Search Box. Shows the search UI.
57 *
58 */
59public class SearchActivity extends Activity {
60
61    private static final boolean DBG = false;
62    private static final String TAG = "QSB.SearchActivity";
63    private static final boolean TRACE = false;
64
65    private static final String SCHEME_CORPUS = "qsb.corpus";
66
67    public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS
68            = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS";
69
70    // The string used for privateImeOptions to identify to the IME that it should not show
71    // a microphone button since one already exists in the search dialog.
72    // TODO: This should move to android-common or something.
73    private static final String IME_OPTION_NO_MICROPHONE = "nm";
74
75    // Keys for the saved instance state.
76    private static final String INSTANCE_KEY_CORPUS = "corpus";
77    private static final String INSTANCE_KEY_QUERY = "query";
78
79    // Measures time from for last onCreate()/onNewIntent() call.
80    private LatencyTracker mStartLatencyTracker;
81    // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume().
82    private boolean mStarting;
83    // True if the user has taken some action, e.g. launching a search, voice search,
84    // or suggestions, since QSB was last started.
85    private boolean mTookAction;
86
87    private CorpusSelectionDialog mCorpusSelectionDialog;
88
89    protected SuggestionsAdapter mSuggestionsAdapter;
90
91    private CorporaObserver mCorporaObserver;
92
93    protected QueryTextView mQueryTextView;
94    // True if the query was empty on the previous call to updateQuery()
95    protected boolean mQueryWasEmpty = true;
96    protected Drawable mQueryTextEmptyBg;
97    protected Drawable mQueryTextNotEmptyBg;
98
99    protected SuggestionsView mSuggestionsView;
100
101    protected ImageButton mSearchGoButton;
102    protected ImageButton mVoiceSearchButton;
103    protected ImageButton mCorpusIndicator;
104
105    private Corpus mCorpus;
106    private Bundle mAppSearchData;
107    private boolean mUpdateSuggestions;
108
109    private final Handler mHandler = new Handler();
110    private final Runnable mUpdateSuggestionsTask = new Runnable() {
111        public void run() {
112            updateSuggestions(getQuery());
113        }
114    };
115
116    private final Runnable mShowInputMethodTask = new Runnable() {
117        public void run() {
118            showInputMethodForQuery();
119        }
120    };
121
122    /** Called when the activity is first created. */
123    @Override
124    public void onCreate(Bundle savedInstanceState) {
125        if (TRACE) startMethodTracing();
126        recordStartTime();
127        if (DBG) Log.d(TAG, "onCreate()");
128        super.onCreate(savedInstanceState);
129
130        setContentView();
131        SuggestListFocusListener suggestionFocusListener = new SuggestListFocusListener();
132        mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter();
133        mSuggestionsAdapter.setSuggestionClickListener(new ClickHandler());
134        mSuggestionsAdapter.setOnFocusChangeListener(suggestionFocusListener);
135
136        mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text);
137        mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
138        mSuggestionsView.setOnScrollListener(new InputMethodCloser());
139        mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener());
140        mSuggestionsView.setOnFocusChangeListener(suggestionFocusListener);
141
142        mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
143        mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
144        mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator);
145
146        mQueryTextView.addTextChangedListener(new SearchTextWatcher());
147        mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener());
148        mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
149        mQueryTextView.setSuggestionClickListener(new ClickHandler());
150        mQueryTextEmptyBg = mQueryTextView.getBackground();
151
152        mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener());
153
154        mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
155
156        mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener());
157
158        ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener();
159        mSearchGoButton.setOnKeyListener(buttonsKeyListener);
160        mVoiceSearchButton.setOnKeyListener(buttonsKeyListener);
161        mCorpusIndicator.setOnKeyListener(buttonsKeyListener);
162
163        mUpdateSuggestions = true;
164
165        // First get setup from intent
166        Intent intent = getIntent();
167        setupFromIntent(intent);
168        // Then restore any saved instance state
169        restoreInstanceState(savedInstanceState);
170
171        mSuggestionsAdapter.registerDataSetObserver(new SuggestionsObserver());
172
173        // Do this at the end, to avoid updating the list view when setSource()
174        // is called.
175        mSuggestionsView.setAdapter(mSuggestionsAdapter);
176
177        mCorporaObserver = new CorporaObserver();
178        getCorpora().registerDataSetObserver(mCorporaObserver);
179    }
180
181    protected void setContentView() {
182        setContentView(R.layout.search_activity);
183    }
184
185    private void startMethodTracing() {
186        File traceDir = getDir("traces", 0);
187        String traceFile = new File(traceDir, "qsb.trace").getAbsolutePath();
188        Debug.startMethodTracing(traceFile);
189    }
190
191    @Override
192    protected void onNewIntent(Intent intent) {
193        if (DBG) Log.d(TAG, "onNewIntent()");
194        recordStartTime();
195        setIntent(intent);
196        setupFromIntent(intent);
197    }
198
199    private void recordStartTime() {
200        mStartLatencyTracker = new LatencyTracker();
201        mStarting = true;
202        mTookAction = false;
203    }
204
205    protected void restoreInstanceState(Bundle savedInstanceState) {
206        if (savedInstanceState == null) return;
207        String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS);
208        String query = savedInstanceState.getString(INSTANCE_KEY_QUERY);
209        setCorpus(corpusName);
210        setQuery(query, false);
211    }
212
213    @Override
214    protected void onSaveInstanceState(Bundle outState) {
215        super.onSaveInstanceState(outState);
216        // We don't save appSearchData, since we always get the value
217        // from the intent and the user can't change it.
218
219        outState.putString(INSTANCE_KEY_CORPUS, getCorpusName());
220        outState.putString(INSTANCE_KEY_QUERY, getQuery());
221    }
222
223    private void setupFromIntent(Intent intent) {
224        if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")");
225        String corpusName = getCorpusNameFromUri(intent.getData());
226        String query = intent.getStringExtra(SearchManager.QUERY);
227        Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA);
228        boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false);
229
230        setCorpus(corpusName);
231        setQuery(query, selectAll);
232        mAppSearchData = appSearchData;
233
234        if (startedIntoCorpusSelectionDialog()) {
235            showCorpusSelectionDialog();
236        }
237    }
238
239    public boolean startedIntoCorpusSelectionDialog() {
240        return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction());
241    }
242
243    /**
244     * Removes corpus selector intent action, so that BACK works normally after
245     * dismissing and reopening the corpus selector.
246     */
247    private void clearStartedIntoCorpusSelectionDialog() {
248        Intent oldIntent = getIntent();
249        if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) {
250            Intent newIntent = new Intent(oldIntent);
251            newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
252            setIntent(newIntent);
253        }
254    }
255
256    public static Uri getCorpusUri(Corpus corpus) {
257        if (corpus == null) return null;
258        return new Uri.Builder()
259                .scheme(SCHEME_CORPUS)
260                .authority(corpus.getName())
261                .build();
262    }
263
264    private String getCorpusNameFromUri(Uri uri) {
265        if (uri == null) return null;
266        if (!SCHEME_CORPUS.equals(uri.getScheme())) return null;
267        return uri.getAuthority();
268    }
269
270    private Corpus getCorpus(String sourceName) {
271        if (sourceName == null) return null;
272        Corpus corpus = getCorpora().getCorpus(sourceName);
273        if (corpus == null) {
274            Log.w(TAG, "Unknown corpus " + sourceName);
275            return null;
276        }
277        return corpus;
278    }
279
280    private void setCorpus(String corpusName) {
281        if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")");
282        mCorpus = getCorpus(corpusName);
283        Drawable sourceIcon;
284        if (mCorpus == null) {
285            sourceIcon = getCorpusViewFactory().getGlobalSearchIcon();
286        } else {
287            sourceIcon = mCorpus.getCorpusIcon();
288        }
289        mSuggestionsAdapter.setCorpus(mCorpus);
290        mCorpusIndicator.setImageDrawable(sourceIcon);
291
292        updateUi(getQuery().length() == 0);
293    }
294
295    private String getCorpusName() {
296        return mCorpus == null ? null : mCorpus.getName();
297    }
298
299    private QsbApplication getQsbApplication() {
300        return QsbApplication.get(this);
301    }
302
303    private Config getConfig() {
304        return getQsbApplication().getConfig();
305    }
306
307    private Corpora getCorpora() {
308        return getQsbApplication().getCorpora();
309    }
310
311    private ShortcutRepository getShortcutRepository() {
312        return getQsbApplication().getShortcutRepository();
313    }
314
315    private SuggestionsProvider getSuggestionsProvider() {
316        return getQsbApplication().getSuggestionsProvider();
317    }
318
319    private CorpusViewFactory getCorpusViewFactory() {
320        return getQsbApplication().getCorpusViewFactory();
321    }
322
323    private VoiceSearch getVoiceSearch() {
324        return QsbApplication.get(this).getVoiceSearch();
325    }
326
327    private Logger getLogger() {
328        return getQsbApplication().getLogger();
329    }
330
331    @Override
332    protected void onDestroy() {
333        if (DBG) Log.d(TAG, "onDestroy()");
334        super.onDestroy();
335        getCorpora().unregisterDataSetObserver(mCorporaObserver);
336        mSuggestionsView.setAdapter(null);  // closes mSuggestionsAdapter
337    }
338
339    @Override
340    protected void onStop() {
341        if (DBG) Log.d(TAG, "onStop()");
342        if (!mTookAction) {
343            // TODO: This gets logged when starting other activities, e.g. by opening he search
344            // settings, or clicking a notification in the status bar.
345            getLogger().logExit(getCurrentSuggestions(), getQuery().length());
346        }
347        // Close all open suggestion cursors. The query will be redone in onResume()
348        // if we come back to this activity.
349        mSuggestionsAdapter.setSuggestions(null);
350        getQsbApplication().getShortcutRefresher().reset();
351        dismissCorpusSelectionDialog();
352        super.onStop();
353    }
354
355    @Override
356    protected void onRestart() {
357        if (DBG) Log.d(TAG, "onRestart()");
358        super.onRestart();
359    }
360
361    @Override
362    protected void onResume() {
363        if (DBG) Log.d(TAG, "onResume()");
364        super.onResume();
365        updateSuggestionsBuffered();
366        if (!isCorpusSelectionDialogShowing()) {
367            mQueryTextView.requestFocus();
368        }
369        if (TRACE) Debug.stopMethodTracing();
370    }
371
372    @Override
373    public boolean onCreateOptionsMenu(Menu menu) {
374        super.onCreateOptionsMenu(menu);
375        SearchSettings.addSearchSettingsMenuItem(this, menu);
376        return true;
377    }
378
379    @Override
380    public void onWindowFocusChanged(boolean hasFocus) {
381        super.onWindowFocusChanged(hasFocus);
382        if (hasFocus) {
383            // Launch the IME after a bit
384            mHandler.postDelayed(mShowInputMethodTask, 0);
385        }
386    }
387
388    protected String getQuery() {
389        CharSequence q = mQueryTextView.getText();
390        return q == null ? "" : q.toString();
391    }
392
393    /**
394     * Sets the text in the query box. Does not update the suggestions.
395     */
396    private void setQuery(String query, boolean selectAll) {
397        mUpdateSuggestions = false;
398        mQueryTextView.setText(query);
399        mQueryTextView.setTextSelection(selectAll);
400        mUpdateSuggestions = true;
401    }
402
403    protected void updateUi(boolean queryEmpty) {
404        updateQueryTextView(queryEmpty);
405        updateSearchGoButton(queryEmpty);
406        updateVoiceSearchButton(queryEmpty);
407    }
408
409    private void updateQueryTextView(boolean queryEmpty) {
410        if (queryEmpty) {
411            if (isSearchCorpusWeb()) {
412                mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg);
413                mQueryTextView.setHint(null);
414            } else {
415                if (mQueryTextNotEmptyBg == null) {
416                    mQueryTextNotEmptyBg =
417                            getResources().getDrawable(R.drawable.textfield_search_empty);
418                }
419                mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg);
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                Logger.SUGGESTION_CLICK_TYPE_LAUNCH);
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 void clickedQuickContact(int position) {
595        SuggestionCursor suggestions = getCurrentSuggestions(position);
596        if (suggestions == null) return;
597
598        if (DBG) Log.d(TAG, "Used suggestion " + position);
599        mTookAction = true;
600
601        // Log suggestion click
602        getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(),
603                Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT);
604
605        // Create shortcut
606        getShortcutRepository().reportClick(suggestions, position);
607    }
608
609    protected boolean onSuggestionLongClicked(int position) {
610        if (DBG) Log.d(TAG, "Long clicked on suggestion " + position);
611        return false;
612    }
613
614    protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) {
615        // Treat enter or search as a click
616        if (       keyCode == KeyEvent.KEYCODE_ENTER
617                || keyCode == KeyEvent.KEYCODE_SEARCH
618                || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
619            return launchSuggestion(position);
620        }
621
622        return false;
623    }
624
625    protected void refineSuggestion(int position) {
626        if (DBG) Log.d(TAG, "query refine clicked, pos " + position);
627        SuggestionCursor suggestions = getCurrentSuggestions(position);
628        if (suggestions == null) {
629            return;
630        }
631        String query = suggestions.getSuggestionQuery();
632        if (TextUtils.isEmpty(query)) {
633            return;
634        }
635
636        // Log refine click
637        getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(),
638                Logger.SUGGESTION_CLICK_TYPE_REFINE);
639
640        // Put query + space in query text view
641        String queryWithSpace = query + ' ';
642        setQuery(queryWithSpace, false);
643        updateSuggestions(queryWithSpace);
644        mQueryTextView.requestFocus();
645    }
646
647    protected int getSelectedPosition() {
648        return mSuggestionsView.getSelectedPosition();
649    }
650
651    /**
652     * Hides the input method.
653     */
654    protected void hideInputMethod() {
655        mQueryTextView.hideInputMethod();
656    }
657
658    protected void showInputMethodForQuery() {
659        mQueryTextView.showInputMethod();
660    }
661
662    protected void onSuggestionListFocusChange(boolean focused) {
663    }
664
665    protected void onQueryTextViewFocusChange(boolean focused) {
666    }
667
668    /**
669     * Hides the input method when the suggestions get focus.
670     */
671    private class SuggestListFocusListener implements OnFocusChangeListener {
672        public void onFocusChange(View v, boolean focused) {
673            if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
674            if (focused) {
675                // The suggestions list got focus, hide the input method
676                hideInputMethod();
677            }
678            onSuggestionListFocusChange(focused);
679        }
680    }
681
682    private class QueryTextViewFocusListener implements OnFocusChangeListener {
683        public void onFocusChange(View v, boolean focused) {
684            if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
685            if (focused) {
686                // The query box got focus, show the input method
687                showInputMethodForQuery();
688            }
689            onQueryTextViewFocusChange(focused);
690        }
691    }
692
693    private int getMaxSuggestions() {
694        Config config = getConfig();
695        return mCorpus == null
696                ? config.getMaxPromotedSuggestions()
697                : config.getMaxResultsPerSource();
698    }
699
700    private void updateSuggestionsBuffered() {
701        mHandler.removeCallbacks(mUpdateSuggestionsTask);
702        long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
703        mHandler.postDelayed(mUpdateSuggestionsTask, delay);
704    }
705
706    protected void updateSuggestions(String query) {
707
708        query = CharMatcher.WHITESPACE.trimLeadingFrom(query);
709        if (DBG) Log.d(TAG, "getSuggestions(\""+query+"\","+mCorpus + ","+getMaxSuggestions()+")");
710        Suggestions suggestions = getSuggestionsProvider().getSuggestions(
711                query, mCorpus, getMaxSuggestions());
712
713        // Log start latency if this is the first suggestions update
714        if (mStarting) {
715            mStarting = false;
716            String source = getIntent().getStringExtra(Search.SOURCE);
717            int latency = mStartLatencyTracker.getLatency();
718            getLogger().logStart(latency, source, mCorpus, suggestions.getExpectedCorpora());
719            getQsbApplication().onStartupComplete();
720        }
721
722        mSuggestionsAdapter.setSuggestions(suggestions);
723    }
724
725    /**
726     * If the input method is in fullscreen mode, and the selector corpus
727     * is All or Web, use the web search suggestions as completions.
728     */
729    protected void updateInputMethodSuggestions() {
730        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
731        if (imm == null || !imm.isFullscreenMode()) return;
732        Suggestions suggestions = mSuggestionsAdapter.getSuggestions();
733        if (suggestions == null) return;
734        SuggestionCursor cursor = suggestions.getPromoted();
735        if (cursor == null) return;
736        CompletionInfo[] completions = webSuggestionsToCompletions(cursor);
737        if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")");
738        imm.displayCompletions(mQueryTextView, completions);
739    }
740
741    private CompletionInfo[] webSuggestionsToCompletions(SuggestionCursor cursor) {
742        int count = cursor.getCount();
743        ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count);
744        boolean usingWebCorpus = isSearchCorpusWeb();
745        for (int i = 0; i < count; i++) {
746            cursor.moveTo(i);
747            if (!usingWebCorpus || cursor.isWebSearchSuggestion()) {
748                String text1 = cursor.getSuggestionText1();
749                completions.add(new CompletionInfo(i, i, text1));
750            }
751        }
752        return completions.toArray(new CompletionInfo[completions.size()]);
753    }
754
755    private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
756        if (!event.isSystem() && !isDpadKey(keyCode)) {
757            if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
758            if (mQueryTextView.requestFocus()) {
759                return mQueryTextView.dispatchKeyEvent(event);
760            }
761        }
762        return false;
763    }
764
765    private boolean isDpadKey(int keyCode) {
766        switch (keyCode) {
767            case KeyEvent.KEYCODE_DPAD_UP:
768            case KeyEvent.KEYCODE_DPAD_DOWN:
769            case KeyEvent.KEYCODE_DPAD_LEFT:
770            case KeyEvent.KEYCODE_DPAD_RIGHT:
771            case KeyEvent.KEYCODE_DPAD_CENTER:
772                return true;
773            default:
774                return false;
775        }
776    }
777
778    /**
779     * Filters the suggestions list when the search text changes.
780     */
781    private class SearchTextWatcher implements TextWatcher {
782        public void afterTextChanged(Editable s) {
783            boolean empty = s.length() == 0;
784            if (empty != mQueryWasEmpty) {
785                mQueryWasEmpty = empty;
786                updateUi(empty);
787            }
788            if (mUpdateSuggestions) {
789                updateSuggestionsBuffered();
790            }
791        }
792
793        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
794        }
795
796        public void onTextChanged(CharSequence s, int start, int before, int count) {
797        }
798    }
799
800    /**
801     * Handles non-text keys in the query text view.
802     */
803    private class QueryTextViewKeyListener implements View.OnKeyListener {
804        public boolean onKey(View view, int keyCode, KeyEvent event) {
805            // Handle IME search action key
806            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
807                // if no action was taken, consume the key event so that the keyboard
808                // remains on screen.
809                return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
810            }
811            return false;
812        }
813    }
814
815    /**
816     * Handles key events on the search and voice search buttons,
817     * by refocusing to EditText.
818     */
819    private class ButtonsKeyListener implements View.OnKeyListener {
820        public boolean onKey(View v, int keyCode, KeyEvent event) {
821            return forwardKeyToQueryTextView(keyCode, event);
822        }
823    }
824
825    /**
826     * Handles key events on the suggestions list view.
827     */
828    private class SuggestionsViewKeyListener implements View.OnKeyListener {
829        public boolean onKey(View v, int keyCode, KeyEvent event) {
830            if (event.getAction() == KeyEvent.ACTION_DOWN) {
831                int position = getSelectedPosition();
832                if (onSuggestionKeyDown(position, keyCode, event)) {
833                    return true;
834                }
835            }
836            return forwardKeyToQueryTextView(keyCode, event);
837        }
838    }
839
840    private class InputMethodCloser implements SuggestionsView.OnScrollListener {
841
842        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
843                int totalItemCount) {
844        }
845
846        public void onScrollStateChanged(AbsListView view, int scrollState) {
847            hideInputMethod();
848        }
849    }
850
851    private class ClickHandler implements SuggestionClickListener {
852       public void onSuggestionClicked(int position) {
853           launchSuggestion(position);
854       }
855
856       public void onSuggestionQuickContactClicked(int position) {
857           clickedQuickContact(position);
858       }
859
860       public boolean onSuggestionLongClicked(int position) {
861           return SearchActivity.this.onSuggestionLongClicked(position);
862       }
863
864       public void onSuggestionQueryRefineClicked(int position) {
865           refineSuggestion(position);
866       }
867    }
868
869    /**
870     * Listens for clicks on the source selector.
871     */
872    private class SearchGoButtonClickListener implements View.OnClickListener {
873        public void onClick(View view) {
874            onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
875        }
876    }
877
878    /**
879     * Listens for clicks on the search button.
880     */
881    private class CorpusIndicatorClickListener implements View.OnClickListener {
882        public void onClick(View view) {
883            showCorpusSelectionDialog();
884        }
885    }
886
887    private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
888        public void onDismiss(DialogInterface dialog) {
889            if (DBG) Log.d(TAG, "Corpus selector dismissed");
890            clearStartedIntoCorpusSelectionDialog();
891        }
892    }
893
894    private class CorpusSelectionListener
895            implements CorpusSelectionDialog.OnCorpusSelectedListener {
896        public void onCorpusSelected(String corpusName) {
897            setCorpus(corpusName);
898            updateSuggestions(getQuery());
899            mQueryTextView.requestFocus();
900            showInputMethodForQuery();
901        }
902    }
903
904    /**
905     * Listens for clicks on the voice search button.
906     */
907    private class VoiceSearchButtonClickListener implements View.OnClickListener {
908        public void onClick(View view) {
909            onVoiceSearchClicked();
910        }
911    }
912
913    private class CorporaObserver extends DataSetObserver {
914        @Override
915        public void onChanged() {
916            setCorpus(getCorpusName());
917            updateSuggestions(getQuery());
918        }
919    }
920
921    private class SuggestionsObserver extends DataSetObserver {
922        @Override
923        public void onChanged() {
924            updateInputMethodSuggestions();
925        }
926    }
927
928}
929