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