SearchActivity.java revision 2617a0177a6088d5aaf381263229bf5a62d2238d
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        LatencyTracker latency = new LatencyTracker(TAG);
569        Suggestions suggestions = getSuggestionsProvider().getSuggestions(query);
570        latency.addEvent("getSuggestions_done");
571        if (!suggestions.isDone()) {
572            suggestions.registerDataSetObserver(new ProgressUpdater(suggestions));
573            startSearchProgress();
574        } else {
575            stopSearchProgress();
576        }
577        mSuggestionsAdapter.setSuggestions(suggestions);
578        latency.addEvent("shortcuts_shown");
579        long userVisibleLatency = latency.getUserVisibleLatency();
580        if (DBG) {
581            Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms.");
582        }
583    }
584
585    private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
586        if (!event.isSystem() && !isDpadKey(keyCode)) {
587            if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
588            if (mQueryTextView.requestFocus()) {
589                return mQueryTextView.dispatchKeyEvent(event);
590            }
591        }
592        return false;
593    }
594
595    private boolean isDpadKey(int keyCode) {
596        switch (keyCode) {
597            case KeyEvent.KEYCODE_DPAD_UP:
598            case KeyEvent.KEYCODE_DPAD_DOWN:
599            case KeyEvent.KEYCODE_DPAD_LEFT:
600            case KeyEvent.KEYCODE_DPAD_RIGHT:
601            case KeyEvent.KEYCODE_DPAD_CENTER:
602                return true;
603            default:
604                return false;
605        }
606    }
607
608    /**
609     * Filters the suggestions list when the search text changes.
610     */
611    private class SearchTextWatcher implements TextWatcher {
612        public void afterTextChanged(Editable s) {
613            if (mUpdateSuggestions) {
614                String query = s == null ? "" : s.toString();
615                setUserQuery(query);
616                updateSuggestions(query);
617            }
618        }
619
620        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
621        }
622
623        public void onTextChanged(CharSequence s, int start, int before, int count) {
624        }
625    }
626
627    /**
628     * Handles non-text keys in the query text view.
629     */
630    private class QueryTextViewKeyListener implements View.OnKeyListener {
631        public boolean onKey(View view, int keyCode, KeyEvent event) {
632            // Handle IME search action key
633            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
634                onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
635            }
636            return false;
637        }
638    }
639
640    /**
641     * Handles key events on the search and voice search buttons,
642     * by refocusing to EditText.
643     */
644    private class ButtonsKeyListener implements View.OnKeyListener {
645        public boolean onKey(View v, int keyCode, KeyEvent event) {
646            return forwardKeyToQueryTextView(keyCode, event);
647        }
648    }
649
650    /**
651     * Handles key events on the suggestions list view.
652     */
653    private class SuggestionsViewKeyListener implements View.OnKeyListener {
654        public boolean onKey(View v, int keyCode, KeyEvent event) {
655            if (event.getAction() == KeyEvent.ACTION_DOWN) {
656                SuggestionPosition suggestion = getSelectedSuggestion();
657                if (suggestion != null) {
658                    if (onSuggestionKeyDown(suggestion, keyCode, event)) {
659                        return true;
660                    }
661                }
662            }
663            return forwardKeyToQueryTextView(keyCode, event);
664        }
665    }
666
667    private class InputMethodCloser implements SuggestionsView.InteractionListener {
668        public void onInteraction() {
669            hideInputMethod();
670        }
671    }
672
673    private class ClickHandler implements SuggestionClickListener {
674       public void onSuggestionClicked(SuggestionPosition suggestion) {
675           launchSuggestion(suggestion);
676       }
677
678       public boolean onSuggestionLongClicked(SuggestionPosition suggestion) {
679           return SearchActivity.this.onSuggestionLongClicked(suggestion);
680       }
681    }
682
683    private class SelectionHandler implements SuggestionSelectionListener {
684        public void onSelectionChanged(SuggestionPosition suggestion) {
685            onSuggestionSelected(suggestion);
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 SourceSelectorClickListener implements View.OnClickListener {
702        public void onClick(View view) {
703            showSourceSelectorDialog();
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}
735