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