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