SearchActivity.java revision dd6d9a1a8d559c87f54412eb4e6569ed62193d60
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.SuggestionClickListener;
22import com.android.quicksearchbox.ui.SuggestionSelectionListener;
23import com.android.quicksearchbox.ui.SuggestionsAdapter;
24import com.android.quicksearchbox.ui.SuggestionsFooter;
25import com.android.quicksearchbox.ui.SuggestionsView;
26
27import android.app.Activity;
28import android.app.SearchManager;
29import android.content.DialogInterface;
30import android.content.Intent;
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.ViewGroup;
44import android.view.View.OnFocusChangeListener;
45import android.view.inputmethod.InputMethodManager;
46import android.widget.AbsListView;
47import android.widget.EditText;
48import android.widget.ImageButton;
49
50import java.io.File;
51import java.util.Collection;
52import java.util.Collections;
53import java.util.List;
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_USER_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    protected EditText mQueryTextView;
92    // True if the query was empty on the previous call to updateQuery()
93    protected boolean mQueryWasEmpty = true;
94
95    protected SuggestionsView mSuggestionsView;
96    protected SuggestionsFooter mSuggestionsFooter;
97
98    protected ImageButton mSearchGoButton;
99    protected ImageButton mVoiceSearchButton;
100    protected ImageButton mCorpusIndicator;
101
102    private Launcher mLauncher;
103
104    private Corpus mCorpus;
105    private Bundle mAppSearchData;
106    private boolean mUpdateSuggestions;
107    private String mUserQuery;
108    private boolean mSelectAll;
109
110    private final Handler mHandler = new Handler();
111    private final Runnable mUpdateSuggestionsTask = new Runnable() {
112        public void run() {
113            updateSuggestions(getQuery());
114        }
115    };
116
117    private final Runnable mShowInputMethodTask = new Runnable() {
118        public void run() {
119            showInputMethodForQuery();
120        }
121    };
122
123    /** Called when the activity is first created. */
124    @Override
125    public void onCreate(Bundle savedInstanceState) {
126        if (TRACE) startMethodTracing();
127        recordStartTime();
128        if (DBG) Log.d(TAG, "onCreate()");
129        super.onCreate(savedInstanceState);
130
131        setContentView(R.layout.search_activity);
132
133        mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter();
134
135        mQueryTextView = (EditText) findViewById(R.id.search_src_text);
136        mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
137        mSuggestionsView.setSuggestionClickListener(new ClickHandler());
138        mSuggestionsView.setSuggestionSelectionListener(new SelectionHandler());
139        mSuggestionsView.setOnScrollListener(new InputMethodCloser());
140        mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener());
141        mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener());
142
143        mSuggestionsFooter = getQsbApplication().createSuggestionsFooter();
144        ViewGroup footerFrame = (ViewGroup) findViewById(R.id.footer);
145        mSuggestionsFooter.addToContainer(footerFrame);
146
147        mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
148        mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
149        mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator);
150
151        mLauncher = new Launcher(this);
152
153        mQueryTextView.addTextChangedListener(new SearchTextWatcher());
154        mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener());
155        mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
156
157        mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener());
158
159        mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
160
161        mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener());
162
163        ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener();
164        mSearchGoButton.setOnKeyListener(buttonsKeyListener);
165        mVoiceSearchButton.setOnKeyListener(buttonsKeyListener);
166        mCorpusIndicator.setOnKeyListener(buttonsKeyListener);
167
168        mUpdateSuggestions = true;
169
170        // First get setup from intent
171        Intent intent = getIntent();
172        setupFromIntent(intent);
173        // Then restore any saved instance state
174        restoreInstanceState(savedInstanceState);
175
176        // Do this at the end, to avoid updating the list view when setSource()
177        // is called.
178        mSuggestionsView.setAdapter(mSuggestionsAdapter);
179        mSuggestionsFooter.setAdapter(mSuggestionsAdapter);
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        recordStartTime();
191        setIntent(intent);
192        setupFromIntent(intent);
193    }
194
195    private void recordStartTime() {
196        mStartLatencyTracker = new LatencyTracker();
197        mStarting = true;
198        mTookAction = false;
199    }
200
201    protected void restoreInstanceState(Bundle savedInstanceState) {
202        if (savedInstanceState == null) return;
203        String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS);
204        String query = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
205        setCorpus(getCorpus(corpusName));
206        setUserQuery(query);
207    }
208
209    @Override
210    protected void onSaveInstanceState(Bundle outState) {
211        super.onSaveInstanceState(outState);
212        // We don't save appSearchData, since we always get the value
213        // from the intent and the user can't change it.
214
215        String corpusName = mCorpus == null ? null : mCorpus.getName();
216        outState.putString(INSTANCE_KEY_CORPUS, corpusName);
217        outState.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
218    }
219
220    private void setupFromIntent(Intent intent) {
221        if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")");
222        Corpus corpus = getCorpusFromUri(intent.getData());
223        String query = intent.getStringExtra(SearchManager.QUERY);
224        Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA);
225
226        setCorpus(corpus);
227        setUserQuery(query);
228        mSelectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false);
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 Corpus getCorpusFromUri(Uri uri) {
262        if (uri == null) return null;
263        if (!SCHEME_CORPUS.equals(uri.getScheme())) return null;
264        String name = uri.getAuthority();
265        return getCorpus(name);
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(Corpus corpus) {
279        if (DBG) Log.d(TAG, "setCorpus(" + corpus + ")");
280        mCorpus = corpus;
281        Drawable sourceIcon;
282        if (corpus == null) {
283            sourceIcon = getCorpusViewFactory().getGlobalSearchIcon();
284        } else {
285            sourceIcon = corpus.getCorpusIcon();
286        }
287        mSuggestionsAdapter.setCorpus(corpus);
288        mCorpusIndicator.setImageDrawable(sourceIcon);
289
290        updateUi(getQuery().length() == 0);
291    }
292
293    private QsbApplication getQsbApplication() {
294        return (QsbApplication) getApplication();
295    }
296
297    private Config getConfig() {
298        return getQsbApplication().getConfig();
299    }
300
301    private Corpora getCorpora() {
302        return getQsbApplication().getCorpora();
303    }
304
305    private ShortcutRepository getShortcutRepository() {
306        return getQsbApplication().getShortcutRepository();
307    }
308
309    private SuggestionsProvider getSuggestionsProvider() {
310        return getQsbApplication().getSuggestionsProvider();
311    }
312
313    private CorpusRanker getCorpusRanker() {
314        return getQsbApplication().getCorpusRanker();
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        mSuggestionsFooter.setAdapter(null);
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 onResume() {
351        if (DBG) Log.d(TAG, "onResume()");
352        super.onResume();
353        setQuery(mUserQuery, mSelectAll);
354        // Only select everything the first time after creating the activity.
355        mSelectAll = false;
356        updateSuggestionsBuffered();
357        if (!isCorpusSelectionDialogShowing()) {
358            mQueryTextView.requestFocus();
359        }
360        if (TRACE) Debug.stopMethodTracing();
361    }
362
363    @Override
364    public boolean onCreateOptionsMenu(Menu menu) {
365        super.onCreateOptionsMenu(menu);
366        SearchSettings.addSearchSettingsMenuItem(this, menu);
367        return true;
368    }
369
370    @Override
371    public void onWindowFocusChanged(boolean hasFocus) {
372        super.onWindowFocusChanged(hasFocus);
373        if (hasFocus) {
374            // Launch the IME after a bit
375            mHandler.postDelayed(mShowInputMethodTask, 0);
376        }
377    }
378
379    /**
380     * Sets the query as typed by the user. Does not update the suggestions
381     * or the text in the query box.
382     */
383    protected void setUserQuery(String userQuery) {
384        if (userQuery == null) userQuery = "";
385        mUserQuery = userQuery;
386    }
387
388    protected String getQuery() {
389        CharSequence q = mQueryTextView.getText();
390        return q == null ? "" : q.toString();
391    }
392
393    /**
394     * Restores the query entered by the user.
395     */
396    private void restoreUserQuery() {
397        if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'");
398        setQuery(mUserQuery, false);
399    }
400
401    /**
402     * Sets the text in the query box. Does not update the suggestions,
403     * and does not change the saved user-entered query.
404     * {@link #restoreUserQuery()} will restore the query to the last
405     * user-entered query.
406     */
407    private void setQuery(String query, boolean selectAll) {
408        mUpdateSuggestions = false;
409        mQueryTextView.setText(query);
410        setTextSelection(selectAll);
411        mUpdateSuggestions = true;
412    }
413
414    /**
415     * Sets the text selection in the query text view.
416     *
417     * @param selectAll If {@code true}, selects the entire query.
418     *        If {@false}, no characters are selected, and the cursor is placed
419     *        at the end of the query.
420     */
421    private void setTextSelection(boolean selectAll) {
422        if (selectAll) {
423            mQueryTextView.selectAll();
424        } else {
425            mQueryTextView.setSelection(mQueryTextView.length());
426        }
427    }
428
429    private void updateUi(boolean queryEmpty) {
430        updateQueryTextView(queryEmpty);
431        updateSearchGoButton(queryEmpty);
432        updateVoiceSearchButton(queryEmpty);
433    }
434
435    private void updateQueryTextView(boolean queryEmpty) {
436        if (queryEmpty) {
437            if (mCorpus == null || mCorpus.isWebCorpus()) {
438                mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty_google);
439                mQueryTextView.setHint(null);
440            } else {
441                mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty);
442                mQueryTextView.setHint(mCorpus.getHint());
443            }
444        } else {
445            mQueryTextView.setBackgroundResource(R.drawable.textfield_search);
446        }
447    }
448
449    private void updateSearchGoButton(boolean queryEmpty) {
450        if (queryEmpty) {
451            mSearchGoButton.setVisibility(View.GONE);
452        } else {
453            mSearchGoButton.setVisibility(View.VISIBLE);
454        }
455    }
456
457    protected void updateVoiceSearchButton(boolean queryEmpty) {
458        if (queryEmpty && mLauncher.shouldShowVoiceSearch(mCorpus)) {
459            mVoiceSearchButton.setVisibility(View.VISIBLE);
460            mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
461        } else {
462            mVoiceSearchButton.setVisibility(View.GONE);
463            mQueryTextView.setPrivateImeOptions(null);
464        }
465    }
466
467    protected void showCorpusSelectionDialog() {
468        if (mCorpusSelectionDialog == null) {
469            mCorpusSelectionDialog = new CorpusSelectionDialog(this);
470            mCorpusSelectionDialog.setOwnerActivity(this);
471            mCorpusSelectionDialog.setOnDismissListener(new CorpusSelectorDismissListener());
472            mCorpusSelectionDialog.setOnCorpusSelectedListener(new CorpusSelectionListener());
473        }
474        mCorpusSelectionDialog.show(mCorpus);
475    }
476
477    protected boolean isCorpusSelectionDialogShowing() {
478        return mCorpusSelectionDialog != null && mCorpusSelectionDialog.isShowing();
479    }
480
481    protected void dismissCorpusSelectionDialog() {
482        if (mCorpusSelectionDialog != null) {
483            mCorpusSelectionDialog.dismiss();
484        }
485    }
486
487    /**
488     * @return true if a search was performed as a result of this click, false otherwise.
489     */
490    protected boolean onSearchClicked(int method) {
491        String query = getQuery();
492        if (DBG) Log.d(TAG, "Search clicked, query=" + query);
493
494        // Don't do empty queries
495        if (TextUtils.getTrimmedLength(query) == 0) return false;
496
497        Corpus searchCorpus = mLauncher.getSearchCorpus(getCorpora(), mCorpus);
498        if (searchCorpus == null) return false;
499
500        mTookAction = true;
501
502        // Log search start
503        getLogger().logSearch(mCorpus, method, query.length());
504
505        // Create shortcut
506        SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query);
507        if (searchShortcut != null) {
508            DataSuggestionCursor cursor = new DataSuggestionCursor(query);
509            cursor.add(searchShortcut);
510            getShortcutRepository().reportClick(cursor, 0);
511        }
512
513        // Start search
514        Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData);
515        mLauncher.launchIntent(intent);
516        return true;
517    }
518
519    protected void onVoiceSearchClicked() {
520        if (DBG) Log.d(TAG, "Voice Search clicked");
521        Corpus searchCorpus = mLauncher.getSearchCorpus(getCorpora(), mCorpus);
522        if (searchCorpus == null) return;
523
524        mTookAction = true;
525
526        // Log voice search start
527        getLogger().logVoiceSearch(searchCorpus);
528
529        // Start voice search
530        Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData);
531        mLauncher.launchIntent(intent);
532    }
533
534    protected SuggestionCursor getCurrentSuggestions() {
535        return mSuggestionsAdapter.getCurrentSuggestions();
536    }
537
538    protected boolean launchSuggestion(int position) {
539        SuggestionCursor suggestions = getCurrentSuggestions();
540        if (position < 0 || position >= suggestions.getCount()) {
541            Log.w(TAG, "Tried to launch invalid suggestion " + position);
542            return false;
543        }
544
545        if (DBG) Log.d(TAG, "Launching suggestion " + position);
546        mTookAction = true;
547
548        // Log suggestion click
549        Collection<Corpus> corpora = mSuggestionsAdapter.getSuggestions().getIncludedCorpora();
550        getLogger().logSuggestionClick(position, suggestions, corpora);
551
552        // Create shortcut
553        getShortcutRepository().reportClick(suggestions, position);
554
555        // Launch intent
556        Intent intent = mLauncher.getSuggestionIntent(suggestions, position, mAppSearchData);
557        mLauncher.launchIntent(intent);
558
559        return true;
560    }
561
562    protected boolean onSuggestionLongClicked(int position) {
563        if (DBG) Log.d(TAG, "Long clicked on suggestion " + position);
564        return false;
565    }
566
567    protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) {
568        // Treat enter or search as a click
569        if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
570            return launchSuggestion(position);
571        }
572
573        if (keyCode == KeyEvent.KEYCODE_DPAD_UP && position == 0) {
574            // Moved up from the top suggestion, restore the user query and focus query box
575            if (DBG) Log.d(TAG, "Up and out");
576            restoreUserQuery();
577            return false;  // let the framework handle the move
578        }
579
580        if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
581                || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
582            // Moved left / right from a suggestion, keep current query, move
583            // focus to query box, and move cursor to far left / right
584            if (DBG) Log.d(TAG, "Left/right on a suggestion");
585            String query = getQuery();
586            int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : query.length();
587            mQueryTextView.setSelection(cursorPos);
588            mQueryTextView.requestFocus();
589            updateSuggestions(query);
590            return true;
591        }
592
593        return false;
594    }
595
596    protected void onSourceSelected() {
597        if (DBG) Log.d(TAG, "No suggestion selected");
598        restoreUserQuery();
599    }
600
601    protected int getSelectedPosition() {
602        return mSuggestionsView.getSelectedPosition();
603    }
604
605    /**
606     * Hides the input method.
607     */
608    protected void hideInputMethod() {
609        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
610        if (imm != null) {
611            imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0);
612        }
613    }
614
615    protected void showInputMethodForQuery() {
616        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
617        if (imm != null) {
618            imm.showSoftInput(mQueryTextView, 0);
619        }
620    }
621
622    /**
623     * Hides the input method when the suggestions get focus.
624     */
625    private class SuggestListFocusListener implements OnFocusChangeListener {
626        public void onFocusChange(View v, boolean focused) {
627            if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
628            if (focused) {
629                // The suggestions list got focus, hide the input method
630                hideInputMethod();
631            }
632        }
633    }
634
635    private class QueryTextViewFocusListener implements OnFocusChangeListener {
636        public void onFocusChange(View v, boolean focused) {
637            if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
638            if (focused) {
639                // The query box got focus, show the input method
640                showInputMethodForQuery();
641            }
642        }
643    }
644
645    private List<Corpus> getCorporaToQuery() {
646        if (mCorpus == null) {
647            return getCorpusRanker().getRankedCorpora();
648        } else {
649            return Collections.singletonList(mCorpus);
650        }
651    }
652
653    private int getMaxSuggestions() {
654        Config config = getConfig();
655        return mCorpus == null
656                ? config.getMaxPromotedSuggestions()
657                : config.getMaxResultsPerSource();
658    }
659
660    private void updateSuggestionsBuffered() {
661        mHandler.removeCallbacks(mUpdateSuggestionsTask);
662        long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
663        mHandler.postDelayed(mUpdateSuggestionsTask, delay);
664    }
665
666    private void updateSuggestions(String query) {
667        // Log start latency if this is the first suggestions update
668        if (mStarting) {
669            mStarting = false;
670            String source = getIntent().getStringExtra(Search.SOURCE);
671            List<Corpus> rankedCorpora = getCorpusRanker().getRankedCorpora();
672            int latency = mStartLatencyTracker.getLatency();
673            getLogger().logStart(latency, source, mCorpus, rankedCorpora);
674        }
675
676        query = ltrim(query);
677        List<Corpus> corporaToQuery = getCorporaToQuery();
678        Suggestions suggestions = getSuggestionsProvider().getSuggestions(
679                query, corporaToQuery, getMaxSuggestions());
680        mSuggestionsAdapter.setSuggestions(suggestions);
681    }
682
683    private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
684        if (!event.isSystem() && !isDpadKey(keyCode)) {
685            if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
686            if (mQueryTextView.requestFocus()) {
687                return mQueryTextView.dispatchKeyEvent(event);
688            }
689        }
690        return false;
691    }
692
693    private boolean isDpadKey(int keyCode) {
694        switch (keyCode) {
695            case KeyEvent.KEYCODE_DPAD_UP:
696            case KeyEvent.KEYCODE_DPAD_DOWN:
697            case KeyEvent.KEYCODE_DPAD_LEFT:
698            case KeyEvent.KEYCODE_DPAD_RIGHT:
699            case KeyEvent.KEYCODE_DPAD_CENTER:
700                return true;
701            default:
702                return false;
703        }
704    }
705
706    /**
707     * Filters the suggestions list when the search text changes.
708     */
709    private class SearchTextWatcher implements TextWatcher {
710        public void afterTextChanged(Editable s) {
711            boolean empty = s.length() == 0;
712            if (empty != mQueryWasEmpty) {
713                mQueryWasEmpty = empty;
714                updateUi(empty);
715            }
716            if (mUpdateSuggestions) {
717                String query = s == null ? "" : s.toString();
718                setUserQuery(query);
719                updateSuggestionsBuffered();
720            }
721        }
722
723        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
724        }
725
726        public void onTextChanged(CharSequence s, int start, int before, int count) {
727        }
728    }
729
730    /**
731     * Handles non-text keys in the query text view.
732     */
733    private class QueryTextViewKeyListener implements View.OnKeyListener {
734        public boolean onKey(View view, int keyCode, KeyEvent event) {
735            // Handle IME search action key
736            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
737                // if no action was taken, consume the key event so that the keyboard
738                // remains on screen.
739                return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
740            }
741            return false;
742        }
743    }
744
745    /**
746     * Handles key events on the search and voice search buttons,
747     * by refocusing to EditText.
748     */
749    private class ButtonsKeyListener implements View.OnKeyListener {
750        public boolean onKey(View v, int keyCode, KeyEvent event) {
751            return forwardKeyToQueryTextView(keyCode, event);
752        }
753    }
754
755    /**
756     * Handles key events on the suggestions list view.
757     */
758    private class SuggestionsViewKeyListener implements View.OnKeyListener {
759        public boolean onKey(View v, int keyCode, KeyEvent event) {
760            if (event.getAction() == KeyEvent.ACTION_DOWN) {
761                int position = getSelectedPosition();
762                if (onSuggestionKeyDown(position, keyCode, event)) {
763                        return true;
764                }
765            }
766            return forwardKeyToQueryTextView(keyCode, event);
767        }
768    }
769
770    private class InputMethodCloser implements SuggestionsView.OnScrollListener {
771
772        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
773                int totalItemCount) {
774        }
775
776        public void onScrollStateChanged(AbsListView view, int scrollState) {
777            hideInputMethod();
778        }
779    }
780
781    private class ClickHandler implements SuggestionClickListener {
782       public void onSuggestionClicked(int position) {
783           launchSuggestion(position);
784       }
785
786       public boolean onSuggestionLongClicked(int position) {
787           return SearchActivity.this.onSuggestionLongClicked(position);
788       }
789
790       public void onSuggestionQueryRefineClicked(int position) {
791           if (DBG) Log.d(TAG, "query refine clicked, pos " + position);
792           SuggestionCursor suggestions = getCurrentSuggestions();
793           if (suggestions != null) {
794               suggestions.moveTo(position);
795               String query = suggestions.getSuggestionQuery();
796               if (!TextUtils.isEmpty(query)) {
797                   query += " ";
798                   setUserQuery(query);
799                   setQuery(query, false);
800                   updateSuggestions(query);
801               }
802           }
803       }
804    }
805
806    private class SelectionHandler implements SuggestionSelectionListener {
807        public void onSuggestionSelected(int position) {
808            SuggestionCursor suggestions = getCurrentSuggestions();
809            suggestions.moveTo(position);
810            String displayQuery = suggestions.getSuggestionDisplayQuery();
811            if (TextUtils.isEmpty(displayQuery)) {
812                restoreUserQuery();
813            } else {
814                setQuery(displayQuery, false);
815            }
816        }
817
818        public void onNothingSelected() {
819                // This happens when a suggestion has been selected with the
820                // dpad / trackball and then a different UI element is touched.
821                // Do nothing, since we want to keep the query of the selection
822                // in the search box.
823        }
824    }
825
826    /**
827     * Listens for clicks on the source selector.
828     */
829    private class SearchGoButtonClickListener implements View.OnClickListener {
830        public void onClick(View view) {
831            onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
832        }
833    }
834
835    /**
836     * Listens for clicks on the search button.
837     */
838    private class CorpusIndicatorClickListener implements View.OnClickListener {
839        public void onClick(View view) {
840            showCorpusSelectionDialog();
841        }
842    }
843
844    private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
845        public void onDismiss(DialogInterface dialog) {
846            if (DBG) Log.d(TAG, "Corpus selector dismissed");
847            clearStartedIntoCorpusSelectionDialog();
848        }
849    }
850
851    private class CorpusSelectionListener
852            implements CorpusSelectionDialog.OnCorpusSelectedListener {
853        public void onCorpusSelected(Corpus corpus) {
854            setCorpus(corpus);
855            updateSuggestions(getQuery());
856            mQueryTextView.requestFocus();
857            showInputMethodForQuery();
858        }
859    }
860
861    /**
862     * Listens for clicks on the voice search button.
863     */
864    private class VoiceSearchButtonClickListener implements View.OnClickListener {
865        public void onClick(View view) {
866            onVoiceSearchClicked();
867        }
868    }
869
870    private static String ltrim(String text) {
871        int start = 0;
872        int length = text.length();
873        while (start < length && Character.isWhitespace(text.charAt(start))) {
874            start++;
875        }
876        return start > 0 ? text.substring(start, length) : text;
877    }
878
879}
880