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