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