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