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