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