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