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