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