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