SearchActivity.java revision d6b9bd42f22582a8ccec657bd6d715465de98ec9
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    protected void onSearchClicked(int method) {
488        String query = getQuery();
489        if (DBG) Log.d(TAG, "Search clicked, query=" + query);
490
491        // Don't do empty queries
492        if (TextUtils.getTrimmedLength(query) == 0) return;
493
494        Corpus searchCorpus = mLauncher.getSearchCorpus(getCorpora(), mCorpus);
495        if (searchCorpus == null) return;
496
497        mTookAction = true;
498
499        // Log search start
500        getLogger().logSearch(mCorpus, method, query.length());
501
502        // Create shortcut
503        SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query);
504        if (searchShortcut != null) {
505            DataSuggestionCursor cursor = new DataSuggestionCursor(query);
506            cursor.add(searchShortcut);
507            getShortcutRepository().reportClick(cursor, 0);
508        }
509
510        // Start search
511        Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData);
512        mLauncher.launchIntent(intent);
513    }
514
515    protected void onVoiceSearchClicked() {
516        if (DBG) Log.d(TAG, "Voice Search clicked");
517        Corpus searchCorpus = mLauncher.getSearchCorpus(getCorpora(), mCorpus);
518        if (searchCorpus == null) return;
519
520        mTookAction = true;
521
522        // Log voice search start
523        getLogger().logVoiceSearch(searchCorpus);
524
525        // Start voice search
526        Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData);
527        mLauncher.launchIntent(intent);
528    }
529
530    protected SuggestionCursor getCurrentSuggestions() {
531        return mSuggestionsAdapter.getCurrentSuggestions();
532    }
533
534    protected boolean launchSuggestion(int position) {
535        SuggestionCursor suggestions = getCurrentSuggestions();
536        if (position < 0 || position >= suggestions.getCount()) {
537            Log.w(TAG, "Tried to launch invalid suggestion " + position);
538            return false;
539        }
540
541        if (DBG) Log.d(TAG, "Launching suggestion " + position);
542        mTookAction = true;
543
544        // Log suggestion click
545        Collection<Corpus> corpora = mSuggestionsAdapter.getSuggestions().getIncludedCorpora();
546        getLogger().logSuggestionClick(position, suggestions, corpora);
547
548        // Create shortcut
549        getShortcutRepository().reportClick(suggestions, position);
550
551        // Launch intent
552        Intent intent = mLauncher.getSuggestionIntent(suggestions, position, mAppSearchData);
553        mLauncher.launchIntent(intent);
554
555        return true;
556    }
557
558    protected boolean onSuggestionLongClicked(int position) {
559        if (DBG) Log.d(TAG, "Long clicked on suggestion " + position);
560        return false;
561    }
562
563    protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) {
564        // Treat enter or search as a click
565        if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
566            return launchSuggestion(position);
567        }
568
569        if (keyCode == KeyEvent.KEYCODE_DPAD_UP && position == 0) {
570            // Moved up from the top suggestion, restore the user query and focus query box
571            if (DBG) Log.d(TAG, "Up and out");
572            restoreUserQuery();
573            return false;  // let the framework handle the move
574        }
575
576        if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
577                || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
578            // Moved left / right from a suggestion, keep current query, move
579            // focus to query box, and move cursor to far left / right
580            if (DBG) Log.d(TAG, "Left/right on a suggestion");
581            String query = getQuery();
582            int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : query.length();
583            mQueryTextView.setSelection(cursorPos);
584            mQueryTextView.requestFocus();
585            updateSuggestions(query);
586            return true;
587        }
588
589        return false;
590    }
591
592    protected void onSourceSelected() {
593        if (DBG) Log.d(TAG, "No suggestion selected");
594        restoreUserQuery();
595    }
596
597    protected int getSelectedPosition() {
598        return mSuggestionsView.getSelectedPosition();
599    }
600
601    /**
602     * Hides the input method.
603     */
604    protected void hideInputMethod() {
605        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
606        if (imm != null) {
607            imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0);
608        }
609    }
610
611    protected void showInputMethodForQuery() {
612        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
613        if (imm != null) {
614            imm.showSoftInput(mQueryTextView, 0);
615        }
616    }
617
618    /**
619     * Hides the input method when the suggestions get focus.
620     */
621    private class SuggestListFocusListener implements OnFocusChangeListener {
622        public void onFocusChange(View v, boolean focused) {
623            if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
624            if (focused) {
625                // The suggestions list got focus, hide the input method
626                hideInputMethod();
627            }
628        }
629    }
630
631    private class QueryTextViewFocusListener implements OnFocusChangeListener {
632        public void onFocusChange(View v, boolean focused) {
633            if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
634            if (focused) {
635                // The query box got focus, show the input method
636                showInputMethodForQuery();
637            }
638        }
639    }
640
641    private List<Corpus> getCorporaToQuery() {
642        if (mCorpus == null) {
643            return getCorpusRanker().getRankedCorpora();
644        } else {
645            return Collections.singletonList(mCorpus);
646        }
647    }
648
649    private int getMaxSuggestions() {
650        Config config = getConfig();
651        return mCorpus == null
652                ? config.getMaxPromotedSuggestions()
653                : config.getMaxResultsPerSource();
654    }
655
656    private void updateSuggestionsBuffered() {
657        mHandler.removeCallbacks(mUpdateSuggestionsTask);
658        long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
659        mHandler.postDelayed(mUpdateSuggestionsTask, delay);
660    }
661
662    private void updateSuggestions(String query) {
663        // Log start latency if this is the first suggestions update
664        if (mStarting) {
665            mStarting = false;
666            String source = getIntent().getStringExtra(Search.SOURCE);
667            List<Corpus> rankedCorpora = getCorpusRanker().getRankedCorpora();
668            int latency = mStartLatencyTracker.getLatency();
669            getLogger().logStart(latency, source, mCorpus, rankedCorpora);
670        }
671
672        query = ltrim(query);
673        List<Corpus> corporaToQuery = getCorporaToQuery();
674        Suggestions suggestions = getSuggestionsProvider().getSuggestions(
675                query, corporaToQuery, getMaxSuggestions());
676        mSuggestionsAdapter.setSuggestions(suggestions);
677    }
678
679    private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
680        if (!event.isSystem() && !isDpadKey(keyCode)) {
681            if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
682            if (mQueryTextView.requestFocus()) {
683                return mQueryTextView.dispatchKeyEvent(event);
684            }
685        }
686        return false;
687    }
688
689    private boolean isDpadKey(int keyCode) {
690        switch (keyCode) {
691            case KeyEvent.KEYCODE_DPAD_UP:
692            case KeyEvent.KEYCODE_DPAD_DOWN:
693            case KeyEvent.KEYCODE_DPAD_LEFT:
694            case KeyEvent.KEYCODE_DPAD_RIGHT:
695            case KeyEvent.KEYCODE_DPAD_CENTER:
696                return true;
697            default:
698                return false;
699        }
700    }
701
702    /**
703     * Filters the suggestions list when the search text changes.
704     */
705    private class SearchTextWatcher implements TextWatcher {
706        public void afterTextChanged(Editable s) {
707            boolean empty = s.length() == 0;
708            if (empty != mQueryWasEmpty) {
709                mQueryWasEmpty = empty;
710                updateUi(empty);
711            }
712            if (mUpdateSuggestions) {
713                String query = s == null ? "" : s.toString();
714                setUserQuery(query);
715                updateSuggestionsBuffered();
716            }
717        }
718
719        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
720        }
721
722        public void onTextChanged(CharSequence s, int start, int before, int count) {
723        }
724    }
725
726    /**
727     * Handles non-text keys in the query text view.
728     */
729    private class QueryTextViewKeyListener implements View.OnKeyListener {
730        public boolean onKey(View view, int keyCode, KeyEvent event) {
731            // Handle IME search action key
732            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
733                onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
734                // if no action was taken, consume the key event so that the keyboard
735                // remains on screen.
736                return !mTookAction;
737            }
738            return false;
739        }
740    }
741
742    /**
743     * Handles key events on the search and voice search buttons,
744     * by refocusing to EditText.
745     */
746    private class ButtonsKeyListener implements View.OnKeyListener {
747        public boolean onKey(View v, int keyCode, KeyEvent event) {
748            return forwardKeyToQueryTextView(keyCode, event);
749        }
750    }
751
752    /**
753     * Handles key events on the suggestions list view.
754     */
755    private class SuggestionsViewKeyListener implements View.OnKeyListener {
756        public boolean onKey(View v, int keyCode, KeyEvent event) {
757            if (event.getAction() == KeyEvent.ACTION_DOWN) {
758                int position = getSelectedPosition();
759                if (onSuggestionKeyDown(position, keyCode, event)) {
760                        return true;
761                }
762            }
763            return forwardKeyToQueryTextView(keyCode, event);
764        }
765    }
766
767    private class InputMethodCloser implements SuggestionsView.OnScrollListener {
768
769        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
770                int totalItemCount) {
771        }
772
773        public void onScrollStateChanged(AbsListView view, int scrollState) {
774            hideInputMethod();
775        }
776    }
777
778    private class ClickHandler implements SuggestionClickListener {
779       public void onSuggestionClicked(int position) {
780           launchSuggestion(position);
781       }
782
783       public boolean onSuggestionLongClicked(int position) {
784           return SearchActivity.this.onSuggestionLongClicked(position);
785       }
786
787       public void onSuggestionQueryRefineClicked(int position) {
788           if (DBG) Log.d(TAG, "query refine clicked, pos " + position);
789           SuggestionCursor suggestions = getCurrentSuggestions();
790           if (suggestions != null) {
791               suggestions.moveTo(position);
792               String query = suggestions.getSuggestionQuery();
793               if (!TextUtils.isEmpty(query)) {
794                   query += " ";
795                   setUserQuery(query);
796                   setQuery(query, false);
797                   updateSuggestions(query);
798               }
799           }
800       }
801    }
802
803    private class SelectionHandler implements SuggestionSelectionListener {
804        public void onSuggestionSelected(int position) {
805            SuggestionCursor suggestions = getCurrentSuggestions();
806            suggestions.moveTo(position);
807            String displayQuery = suggestions.getSuggestionDisplayQuery();
808            if (TextUtils.isEmpty(displayQuery)) {
809                restoreUserQuery();
810            } else {
811                setQuery(displayQuery, false);
812            }
813        }
814
815        public void onNothingSelected() {
816                // This happens when a suggestion has been selected with the
817                // dpad / trackball and then a different UI element is touched.
818                // Do nothing, since we want to keep the query of the selection
819                // in the search box.
820        }
821    }
822
823    /**
824     * Listens for clicks on the source selector.
825     */
826    private class SearchGoButtonClickListener implements View.OnClickListener {
827        public void onClick(View view) {
828            onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
829        }
830    }
831
832    /**
833     * Listens for clicks on the search button.
834     */
835    private class CorpusIndicatorClickListener implements View.OnClickListener {
836        public void onClick(View view) {
837            showCorpusSelectionDialog();
838        }
839    }
840
841    private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
842        public void onDismiss(DialogInterface dialog) {
843            if (DBG) Log.d(TAG, "Corpus selector dismissed");
844            clearStartedIntoCorpusSelectionDialog();
845        }
846    }
847
848    private class CorpusSelectionListener
849            implements CorpusSelectionDialog.OnCorpusSelectedListener {
850        public void onCorpusSelected(Corpus corpus) {
851            setCorpus(corpus);
852            updateSuggestions(getQuery());
853            mQueryTextView.requestFocus();
854            showInputMethodForQuery();
855        }
856    }
857
858    /**
859     * Listens for clicks on the voice search button.
860     */
861    private class VoiceSearchButtonClickListener implements View.OnClickListener {
862        public void onClick(View view) {
863            onVoiceSearchClicked();
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