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