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