SearchActivity.java revision fde948e69f59589cf0d217ea414af7947de600bb
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            showSourceSelectorDialog();
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        super.onStop();
283    }
284
285    @Override
286    protected void onResume() {
287        if (DBG) Log.d(TAG, "onResume()");
288        super.onResume();
289        setQuery(mUserQuery, mSelectAll);
290        // Only select everything the first time after creating the activity.
291        mSelectAll = false;
292        updateSuggestions(mUserQuery);
293        mQueryTextView.requestFocus();
294        if (mStarting) {
295            mStarting = false;
296            // Start up latency should not exceed 2^31 ms (~ 25 days). Note that
297            // SystemClock.uptimeMillis() does not advance during deep sleep.
298            int latency = (int) (SystemClock.uptimeMillis() - mStartTime);
299            String source = getIntent().getStringExtra(Search.SOURCE);
300            getLogger().logStart(latency, source, mCorpus,
301                    getSuggestionsProvider().getOrderedCorpora());
302        }
303    }
304
305    @Override
306    public boolean onCreateOptionsMenu(Menu menu) {
307        super.onCreateOptionsMenu(menu);
308
309        Intent settings = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS);
310        settings.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
311        // Don't show activity chooser if there are multiple search settings activities,
312        // e.g. from different QSB implementations.
313        settings.setPackage(this.getPackageName());
314        menu.add(Menu.NONE, Menu.NONE, 0, R.string.menu_settings)
315                .setIcon(android.R.drawable.ic_menu_preferences).setAlphabeticShortcut('P')
316                .setIntent(settings);
317
318        return true;
319    }
320
321    /**
322     * Sets the query as typed by the user. Does not update the suggestions
323     * or the text in the query box.
324     */
325    protected void setUserQuery(String userQuery) {
326        if (userQuery == null) userQuery = "";
327        mUserQuery = userQuery;
328    }
329
330    protected void setAppSearchData(Bundle appSearchData) {
331        mAppSearchData = appSearchData;
332        mLauncher.setAppSearchData(appSearchData);
333    }
334
335    protected String getQuery() {
336        CharSequence q = mQueryTextView.getText();
337        return q == null ? "" : q.toString();
338    }
339
340    /**
341     * Restores the query entered by the user.
342     */
343    private void restoreUserQuery() {
344        if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'");
345        setQuery(mUserQuery, false);
346    }
347
348    /**
349     * Sets the text in the query box. Does not update the suggestions,
350     * and does not change the saved user-entered query.
351     * {@link #restoreUserQuery()} will restore the query to the last
352     * user-entered query.
353     */
354    private void setQuery(String query, boolean selectAll) {
355        mUpdateSuggestions = false;
356        mQueryTextView.setText(query);
357        setTextSelection(selectAll);
358        mUpdateSuggestions = true;
359    }
360
361    /**
362     * Sets the text selection in the query text view.
363     *
364     * @param selectAll If {@code true}, selects the entire query.
365     *        If {@false}, no characters are selected, and the cursor is placed
366     *        at the end of the query.
367     */
368    private void setTextSelection(boolean selectAll) {
369        if (selectAll) {
370            mQueryTextView.setSelection(0, mQueryTextView.length());
371        } else {
372            mQueryTextView.setSelection(mQueryTextView.length());
373        }
374    }
375
376    protected void showSourceSelectorDialog() {
377        showDialog(CORPUS_SELECTION_DIALOG);
378    }
379
380
381    @Override
382    protected Dialog onCreateDialog(int id, Bundle args) {
383        switch (id) {
384            case CORPUS_SELECTION_DIALOG:
385                return createCorpusSelectionDialog();
386            default:
387                throw new IllegalArgumentException("Unknown dialog: " + id);
388        }
389    }
390
391    @Override
392    protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
393        switch (id) {
394            case CORPUS_SELECTION_DIALOG:
395                prepareCorpusSelectionDialog((CorpusSelectionDialog) dialog);
396                break;
397            default:
398                throw new IllegalArgumentException("Unknown dialog: " + id);
399        }
400    }
401
402    protected CorpusSelectionDialog createCorpusSelectionDialog() {
403        CorpusSelectionDialog dialog = new CorpusSelectionDialog(this);
404        dialog.setOwnerActivity(this);
405        return dialog;
406    }
407
408    protected void prepareCorpusSelectionDialog(CorpusSelectionDialog dialog) {
409        dialog.setCorpus(mCorpus);
410        dialog.setQuery(getQuery());
411        dialog.setAppData(mAppSearchData);
412    }
413
414    protected void onSearchClicked(int method) {
415        String query = getQuery();
416        if (DBG) Log.d(TAG, "Search clicked, query=" + query);
417        mTookAction = true;
418        getLogger().logSearch(mCorpus, method, query.length());
419        mLauncher.startSearch(mCorpus, query);
420    }
421
422    protected void onVoiceSearchClicked() {
423        if (DBG) Log.d(TAG, "Voice Search clicked");
424        mTookAction = true;
425        getLogger().logVoiceSearch(mCorpus);
426
427        // TODO: should this start voice search in the current source?
428        mLauncher.startVoiceSearch(mCorpus);
429    }
430
431    protected SuggestionCursor getSuggestions() {
432        return mSuggestionsAdapter.getCurrentSuggestions();
433    }
434
435    protected boolean launchSuggestion(int position) {
436        if (DBG) Log.d(TAG, "Launching suggestion " + position);
437        mTookAction = true;
438        SuggestionCursor suggestions = getSuggestions();
439        // TODO: This should be just the queried sources, but currently
440        // all sources are queried
441        ArrayList<Corpus> corpora = getCorpusRanker().rankCorpora(getCorpora().getEnabledCorpora());
442        getLogger().logSuggestionClick(position, suggestions, corpora);
443
444        mLauncher.launchSuggestion(suggestions, position);
445        getShortcutRepository().reportClick(suggestions, position);
446        return true;
447    }
448
449    protected boolean onSuggestionLongClicked(int position) {
450        if (DBG) Log.d(TAG, "Long clicked on suggestion " + position);
451        return false;
452    }
453
454    protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) {
455        // Treat enter or search as a click
456        if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
457            return launchSuggestion(position);
458        }
459
460        if (keyCode == KeyEvent.KEYCODE_DPAD_UP
461                && mSuggestionsView.getSelectedItemPosition() == 0) {
462            // Moved up from the top suggestion, restore the user query and focus query box
463            if (DBG) Log.d(TAG, "Up and out");
464            restoreUserQuery();
465            return false;  // let the framework handle the move
466        }
467
468        if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
469                || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
470            // Moved left / right from a suggestion, keep current query, move
471            // focus to query box, and move cursor to far left / right
472            if (DBG) Log.d(TAG, "Left/right on a suggestion");
473            int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView.length();
474            mQueryTextView.setSelection(cursorPos);
475            mQueryTextView.requestFocus();
476            // TODO: should we modify the list selection?
477            return true;
478        }
479
480        return false;
481    }
482
483    protected void onSourceSelected() {
484        if (DBG) Log.d(TAG, "No suggestion selected");
485        restoreUserQuery();
486    }
487
488    protected int getSelectedPosition() {
489        return mSuggestionsView.getSelectedPosition();
490    }
491
492    /**
493     * Hides the input method.
494     */
495    protected void hideInputMethod() {
496        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
497        if (imm != null) {
498            imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0);
499        }
500    }
501
502    protected void showInputMethodForQuery() {
503        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
504        if (imm != null) {
505            imm.showSoftInput(mQueryTextView, 0);
506        }
507    }
508
509    /**
510     * Hides the input method when the suggestions get focus.
511     */
512    private class SuggestListFocusListener implements OnFocusChangeListener {
513        public void onFocusChange(View v, boolean focused) {
514            if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
515            if (focused) {
516                // The suggestions list got focus, hide the input method
517                hideInputMethod();
518            }
519        }
520    }
521
522    private class QueryTextViewFocusListener implements OnFocusChangeListener {
523        public void onFocusChange(View v, boolean focused) {
524            if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
525            if (focused) {
526                // The query box got focus, show the input method if the
527                // query box got focus?
528                showInputMethodForQuery();
529            }
530        }
531    }
532
533    private void startSearchProgress() {
534        // TODO: Cache animation between calls?
535        mSearchGoButton.setImageResource(R.drawable.searching);
536        Animatable animation = (Animatable) mSearchGoButton.getDrawable();
537        animation.start();
538    }
539
540    private void stopSearchProgress() {
541        Drawable animation = mSearchGoButton.getDrawable();
542        if (animation instanceof Animatable) {
543            // TODO: Is this needed, or is it done automatically when the
544            // animation is removed?
545            ((Animatable) animation).stop();
546        }
547        mSearchGoButton.setImageResource(R.drawable.ic_btn_search);
548    }
549
550    private void updateSuggestions(String query) {
551        query = ltrim(query);
552        LatencyTracker latency = new LatencyTracker(TAG);
553        Suggestions suggestions = getSuggestionsProvider().getSuggestions(query);
554        latency.addEvent("getSuggestions_done");
555        if (!suggestions.isDone()) {
556            suggestions.registerDataSetObserver(new ProgressUpdater(suggestions));
557            startSearchProgress();
558        } else {
559            stopSearchProgress();
560        }
561        mSuggestionsAdapter.setSuggestions(suggestions);
562        latency.addEvent("shortcuts_shown");
563        long userVisibleLatency = latency.getUserVisibleLatency();
564        if (DBG) {
565            Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms.");
566        }
567    }
568
569    private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
570        if (!event.isSystem() && !isDpadKey(keyCode)) {
571            if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
572            if (mQueryTextView.requestFocus()) {
573                return mQueryTextView.dispatchKeyEvent(event);
574            }
575        }
576        return false;
577    }
578
579    private boolean isDpadKey(int keyCode) {
580        switch (keyCode) {
581            case KeyEvent.KEYCODE_DPAD_UP:
582            case KeyEvent.KEYCODE_DPAD_DOWN:
583            case KeyEvent.KEYCODE_DPAD_LEFT:
584            case KeyEvent.KEYCODE_DPAD_RIGHT:
585            case KeyEvent.KEYCODE_DPAD_CENTER:
586                return true;
587            default:
588                return false;
589        }
590    }
591
592    /**
593     * Filters the suggestions list when the search text changes.
594     */
595    private class SearchTextWatcher implements TextWatcher {
596        public void afterTextChanged(Editable s) {
597            if (mUpdateSuggestions) {
598                String query = s == null ? "" : s.toString();
599                setUserQuery(query);
600                updateSuggestions(query);
601            }
602        }
603
604        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
605        }
606
607        public void onTextChanged(CharSequence s, int start, int before, int count) {
608        }
609    }
610
611    /**
612     * Handles non-text keys in the query text view.
613     */
614    private class QueryTextViewKeyListener implements View.OnKeyListener {
615        public boolean onKey(View view, int keyCode, KeyEvent event) {
616            // Handle IME search action key
617            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
618                onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
619            }
620            return false;
621        }
622    }
623
624    /**
625     * Handles key events on the search and voice search buttons,
626     * by refocusing to EditText.
627     */
628    private class ButtonsKeyListener implements View.OnKeyListener {
629        public boolean onKey(View v, int keyCode, KeyEvent event) {
630            return forwardKeyToQueryTextView(keyCode, event);
631        }
632    }
633
634    /**
635     * Handles key events on the suggestions list view.
636     */
637    private class SuggestionsViewKeyListener implements View.OnKeyListener {
638        public boolean onKey(View v, int keyCode, KeyEvent event) {
639            if (event.getAction() == KeyEvent.ACTION_DOWN) {
640                int position = getSelectedPosition();
641                if (onSuggestionKeyDown(position, keyCode, event)) {
642                        return true;
643                }
644            }
645            return forwardKeyToQueryTextView(keyCode, event);
646        }
647    }
648
649    private class InputMethodCloser implements SuggestionsView.InteractionListener {
650        public void onInteraction() {
651            hideInputMethod();
652        }
653    }
654
655    private class ClickHandler implements SuggestionClickListener {
656       public void onSuggestionClicked(int position) {
657           launchSuggestion(position);
658       }
659
660       public boolean onSuggestionLongClicked(int position) {
661           return SearchActivity.this.onSuggestionLongClicked(position);
662       }
663    }
664
665    private class SelectionHandler implements SuggestionSelectionListener {
666        public void onSuggestionSelected(int position) {
667            SuggestionCursor suggestions = getSuggestions();
668            suggestions.moveTo(position);
669            String displayQuery = suggestions.getSuggestionDisplayQuery();
670            if (TextUtils.isEmpty(displayQuery)) {
671                restoreUserQuery();
672            } else {
673                setQuery(displayQuery, false);
674            }
675        }
676
677        public void onNothingSelected() {
678                // This happens when a suggestion has been selected with the
679                // dpad / trackball and then a different UI element is touched.
680                // Do nothing, since we want to keep the query of the selection
681                // in the search box.
682        }
683    }
684
685    /**
686     * Listens for clicks on the source selector.
687     */
688    private class SearchGoButtonClickListener implements View.OnClickListener {
689        public void onClick(View view) {
690            onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
691        }
692    }
693
694    /**
695     * Listens for clicks on the search button.
696     */
697    private class CorpusIndicatorClickListener implements View.OnClickListener {
698        public void onClick(View view) {
699            showSourceSelectorDialog();
700        }
701    }
702
703    /**
704     * Listens for clicks on the voice search button.
705     */
706    private class VoiceSearchButtonClickListener implements View.OnClickListener {
707        public void onClick(View view) {
708            onVoiceSearchClicked();
709        }
710    }
711
712    /**
713     * Updates the progress bar when the suggestions adapter changes its progress.
714     */
715    private class ProgressUpdater extends DataSetObserver {
716        private final Suggestions mSuggestions;
717
718        public ProgressUpdater(Suggestions suggestions) {
719            mSuggestions = suggestions;
720        }
721
722        @Override
723        public void onChanged() {
724            if (mSuggestions.isDone()) {
725                stopSearchProgress();
726            }
727        }
728    }
729
730    private static String ltrim(String text) {
731        int start = 0;
732        int length = text.length();
733        while (start < length && Character.isWhitespace(text.charAt(start))) {
734            start++;
735        }
736        return start > 0 ? text.substring(start, length) : text;
737    }
738
739}
740