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