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