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