SearchActivity.java revision f5e1499b48ac06de4e6e6460a7f14c3a7ac4eadd
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    protected void onSuggestionListFocusChange(boolean focused) {
608    }
609
610    protected void onQueryTextViewFocusChange(boolean focused) {
611    }
612
613    /**
614     * Hides the input method when the suggestions get focus.
615     */
616    private class SuggestListFocusListener implements OnFocusChangeListener {
617        public void onFocusChange(View v, boolean focused) {
618            if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
619            if (focused) {
620                // The suggestions list got focus, hide the input method
621                hideInputMethod();
622            }
623            onSuggestionListFocusChange(focused);
624        }
625    }
626
627    private class QueryTextViewFocusListener implements OnFocusChangeListener {
628        public void onFocusChange(View v, boolean focused) {
629            if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
630            if (focused) {
631                // The query box got focus, show the input method
632                showInputMethodForQuery();
633            }
634            onQueryTextViewFocusChange(focused);
635        }
636    }
637
638    private int getMaxSuggestions() {
639        Config config = getConfig();
640        return mCorpus == null
641                ? config.getMaxPromotedSuggestions()
642                : config.getMaxResultsPerSource();
643    }
644
645    private void updateSuggestionsBuffered() {
646        mHandler.removeCallbacks(mUpdateSuggestionsTask);
647        long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
648        mHandler.postDelayed(mUpdateSuggestionsTask, delay);
649    }
650
651    protected void updateSuggestions(String query) {
652
653        query = ltrim(query);
654        Suggestions suggestions = getSuggestionsProvider().getSuggestions(
655                query, mCorpus, getMaxSuggestions());
656
657        // Log start latency if this is the first suggestions update
658        if (mStarting) {
659            mStarting = false;
660            String source = getIntent().getStringExtra(Search.SOURCE);
661            int latency = mStartLatencyTracker.getLatency();
662            getLogger().logStart(latency, source, mCorpus, suggestions.getExpectedCorpora());
663        }
664
665        mSuggestionsAdapter.setSuggestions(suggestions);
666    }
667
668    private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
669        if (!event.isSystem() && !isDpadKey(keyCode)) {
670            if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
671            if (mQueryTextView.requestFocus()) {
672                return mQueryTextView.dispatchKeyEvent(event);
673            }
674        }
675        return false;
676    }
677
678    private boolean isDpadKey(int keyCode) {
679        switch (keyCode) {
680            case KeyEvent.KEYCODE_DPAD_UP:
681            case KeyEvent.KEYCODE_DPAD_DOWN:
682            case KeyEvent.KEYCODE_DPAD_LEFT:
683            case KeyEvent.KEYCODE_DPAD_RIGHT:
684            case KeyEvent.KEYCODE_DPAD_CENTER:
685                return true;
686            default:
687                return false;
688        }
689    }
690
691    /**
692     * Filters the suggestions list when the search text changes.
693     */
694    private class SearchTextWatcher implements TextWatcher {
695        public void afterTextChanged(Editable s) {
696            boolean empty = s.length() == 0;
697            if (empty != mQueryWasEmpty) {
698                mQueryWasEmpty = empty;
699                updateUi(empty);
700            }
701            if (mUpdateSuggestions) {
702                updateSuggestionsBuffered();
703            }
704        }
705
706        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
707        }
708
709        public void onTextChanged(CharSequence s, int start, int before, int count) {
710        }
711    }
712
713    /**
714     * Handles non-text keys in the query text view.
715     */
716    private class QueryTextViewKeyListener implements View.OnKeyListener {
717        public boolean onKey(View view, int keyCode, KeyEvent event) {
718            // Handle IME search action key
719            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
720                // if no action was taken, consume the key event so that the keyboard
721                // remains on screen.
722                return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
723            }
724            return false;
725        }
726    }
727
728    /**
729     * Handles key events on the search and voice search buttons,
730     * by refocusing to EditText.
731     */
732    private class ButtonsKeyListener implements View.OnKeyListener {
733        public boolean onKey(View v, int keyCode, KeyEvent event) {
734            return forwardKeyToQueryTextView(keyCode, event);
735        }
736    }
737
738    /**
739     * Handles key events on the suggestions list view.
740     */
741    private class SuggestionsViewKeyListener implements View.OnKeyListener {
742        public boolean onKey(View v, int keyCode, KeyEvent event) {
743            if (event.getAction() == KeyEvent.ACTION_DOWN) {
744                int position = getSelectedPosition();
745                if (onSuggestionKeyDown(position, keyCode, event)) {
746                    return true;
747                }
748            }
749            return forwardKeyToQueryTextView(keyCode, event);
750        }
751    }
752
753    private class InputMethodCloser implements SuggestionsView.OnScrollListener {
754
755        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
756                int totalItemCount) {
757        }
758
759        public void onScrollStateChanged(AbsListView view, int scrollState) {
760            hideInputMethod();
761        }
762    }
763
764    private class ClickHandler implements SuggestionClickListener {
765       public void onSuggestionClicked(int position) {
766           launchSuggestion(position);
767       }
768
769       public boolean onSuggestionLongClicked(int position) {
770           return SearchActivity.this.onSuggestionLongClicked(position);
771       }
772
773       public void onSuggestionQueryRefineClicked(int position) {
774           if (DBG) Log.d(TAG, "query refine clicked, pos " + position);
775           SuggestionCursor suggestions = getCurrentSuggestions();
776           if (suggestions != null) {
777               suggestions.moveTo(position);
778               String query = suggestions.getSuggestionQuery();
779               if (!TextUtils.isEmpty(query)) {
780                   query += " ";
781                   setQuery(query, false);
782                   updateSuggestions(query);
783               }
784           }
785       }
786    }
787
788    /**
789     * Listens for clicks on the source selector.
790     */
791    private class SearchGoButtonClickListener implements View.OnClickListener {
792        public void onClick(View view) {
793            onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
794        }
795    }
796
797    /**
798     * Listens for clicks on the search button.
799     */
800    private class CorpusIndicatorClickListener implements View.OnClickListener {
801        public void onClick(View view) {
802            showCorpusSelectionDialog();
803        }
804    }
805
806    private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
807        public void onDismiss(DialogInterface dialog) {
808            if (DBG) Log.d(TAG, "Corpus selector dismissed");
809            clearStartedIntoCorpusSelectionDialog();
810        }
811    }
812
813    private class CorpusSelectionListener
814            implements CorpusSelectionDialog.OnCorpusSelectedListener {
815        public void onCorpusSelected(String corpusName) {
816            setCorpus(corpusName);
817            updateSuggestions(getQuery());
818            mQueryTextView.requestFocus();
819            showInputMethodForQuery();
820        }
821    }
822
823    /**
824     * Listens for clicks on the voice search button.
825     */
826    private class VoiceSearchButtonClickListener implements View.OnClickListener {
827        public void onClick(View view) {
828            onVoiceSearchClicked();
829        }
830    }
831
832    private class CorporaObserver extends DataSetObserver {
833        @Override
834        public void onChanged() {
835            setCorpus(getCorpusName());
836            updateSuggestions(getQuery());
837        }
838    }
839
840    private static String ltrim(String text) {
841        int start = 0;
842        int length = text.length();
843        while (start < length && Character.isWhitespace(text.charAt(start))) {
844            start++;
845        }
846        return start > 0 ? text.substring(start, length) : text;
847    }
848
849}
850