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