SearchActivity.java revision 3109221113ff01d16dfa7d0844f93dccb7dd643b
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.SearchActivityView;
21import com.android.quicksearchbox.ui.SuggestionClickListener;
22import com.android.quicksearchbox.ui.SuggestionsAdapter;
23import com.android.quicksearchbox.util.Consumer;
24import com.android.quicksearchbox.util.Consumers;
25import com.google.common.annotations.VisibleForTesting;
26import com.google.common.base.CharMatcher;
27
28import android.app.Activity;
29import android.app.SearchManager;
30import android.content.DialogInterface;
31import android.content.Intent;
32import android.database.DataSetObserver;
33import android.net.Uri;
34import android.os.Bundle;
35import android.os.Debug;
36import android.os.Handler;
37import android.text.TextUtils;
38import android.util.Log;
39import android.view.Menu;
40import android.view.MenuInflater;
41import android.view.MenuItem;
42import android.view.View;
43import android.widget.Toast;
44
45import java.io.File;
46import java.util.ArrayList;
47import java.util.Collection;
48import java.util.List;
49import java.util.Set;
50
51/**
52 * The main activity for Quick Search Box. Shows the search UI.
53 *
54 */
55public class SearchActivity extends Activity {
56
57    private static final boolean DBG = false;
58    private static final String TAG = "QSB.SearchActivity";
59
60    private static final String SCHEME_CORPUS = "qsb.corpus";
61
62    public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS
63            = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS";
64
65    private static final String INTENT_EXTRA_TRACE_START_UP = "trace_start_up";
66
67    // Keys for the saved instance state.
68    private static final String INSTANCE_KEY_CORPUS = "corpus";
69    private static final String INSTANCE_KEY_QUERY = "query";
70
71    private boolean mTraceStartUp;
72    // Measures time from for last onCreate()/onNewIntent() call.
73    private LatencyTracker mStartLatencyTracker;
74    // Measures time spent inside onCreate()
75    private LatencyTracker mOnCreateTracker;
76    private int mOnCreateLatency;
77    // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume().
78    private boolean mStarting;
79    // True if the user has taken some action, e.g. launching a search, voice search,
80    // or suggestions, since QSB was last started.
81    private boolean mTookAction;
82
83    private SearchActivityView mSearchActivityView;
84
85    private CorporaObserver mCorporaObserver;
86
87    private Bundle mAppSearchData;
88
89    private final Handler mHandler = new Handler();
90    private final Runnable mUpdateSuggestionsTask = new Runnable() {
91        public void run() {
92            updateSuggestions();
93        }
94    };
95
96    private final Runnable mShowInputMethodTask = new Runnable() {
97        public void run() {
98            mSearchActivityView.showInputMethodForQuery();
99        }
100    };
101
102    private OnDestroyListener mDestroyListener;
103
104    /** Called when the activity is first created. */
105    @Override
106    public void onCreate(Bundle savedInstanceState) {
107        mTraceStartUp = getIntent().hasExtra(INTENT_EXTRA_TRACE_START_UP);
108        if (mTraceStartUp) {
109            String traceFile = new File(getDir("traces", 0), "qsb-start.trace").getAbsolutePath();
110            Log.i(TAG, "Writing start-up trace to " + traceFile);
111            Debug.startMethodTracing(traceFile);
112        }
113        recordStartTime();
114        if (DBG) Log.d(TAG, "onCreate()");
115        super.onCreate(savedInstanceState);
116
117        // This forces the HTTP request to check the users domain to be
118        // sent as early as possible.
119        QsbApplication.get(this).getSearchBaseUrlHelper();
120
121        mSearchActivityView = setupContentView();
122
123        if (getConfig().showScrollingSuggestions()) {
124            mSearchActivityView.setMaxPromotedSuggestions(getConfig().getMaxPromotedSuggestions());
125        } else {
126            mSearchActivityView.limitSuggestionsToViewHeight();
127        }
128        if (getConfig().showScrollingResults()) {
129            mSearchActivityView.setMaxPromotedResults(getConfig().getMaxPromotedResults());
130        } else {
131            mSearchActivityView.limitResultsToViewHeight();
132        }
133
134        mSearchActivityView.setSearchClickListener(new SearchActivityView.SearchClickListener() {
135            public boolean onSearchClicked(int method) {
136                return SearchActivity.this.onSearchClicked(method);
137            }
138        });
139
140        mSearchActivityView.setQueryListener(new SearchActivityView.QueryListener() {
141            public void onQueryChanged() {
142                updateSuggestionsBuffered();
143            }
144        });
145
146        mSearchActivityView.setSuggestionClickListener(new ClickHandler());
147
148        mSearchActivityView.setVoiceSearchButtonClickListener(new View.OnClickListener() {
149            public void onClick(View view) {
150                onVoiceSearchClicked();
151            }
152        });
153
154        View.OnClickListener finishOnClick = new View.OnClickListener() {
155            public void onClick(View v) {
156                finish();
157            }
158        };
159        mSearchActivityView.setExitClickListener(finishOnClick);
160        mSearchActivityView.setEmptySpaceClickListener(finishOnClick);
161
162        // First get setup from intent
163        Intent intent = getIntent();
164        setupFromIntent(intent);
165        // Then restore any saved instance state
166        restoreInstanceState(savedInstanceState);
167
168        // Do this at the end, to avoid updating the list view when setSource()
169        // is called.
170        mSearchActivityView.start();
171
172        mCorporaObserver = new CorporaObserver();
173        getCorpora().registerDataSetObserver(mCorporaObserver);
174        recordOnCreateDone();
175    }
176
177    protected SearchActivityView setupContentView() {
178        setContentView(R.layout.search_activity);
179        return (SearchActivityView) findViewById(R.id.search_activity_view);
180    }
181
182    protected SearchActivityView getSearchActivityView() {
183        return mSearchActivityView;
184    }
185
186    @Override
187    protected void onNewIntent(Intent intent) {
188        if (DBG) Log.d(TAG, "onNewIntent()");
189        recordStartTime();
190        setIntent(intent);
191        setupFromIntent(intent);
192    }
193
194    private void recordStartTime() {
195        mStartLatencyTracker = new LatencyTracker();
196        mOnCreateTracker = new LatencyTracker();
197        mStarting = true;
198        mTookAction = false;
199    }
200
201    private void recordOnCreateDone() {
202        mOnCreateLatency = mOnCreateTracker.getLatency();
203    }
204
205    protected void restoreInstanceState(Bundle savedInstanceState) {
206        if (savedInstanceState == null) return;
207        String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS);
208        String query = savedInstanceState.getString(INSTANCE_KEY_QUERY);
209        setCorpus(corpusName);
210        setQuery(query, false);
211    }
212
213    @Override
214    protected void onSaveInstanceState(Bundle outState) {
215        super.onSaveInstanceState(outState);
216        // We don't save appSearchData, since we always get the value
217        // from the intent and the user can't change it.
218
219        outState.putString(INSTANCE_KEY_CORPUS, getCorpusName());
220        outState.putString(INSTANCE_KEY_QUERY, getQuery());
221    }
222
223    private void setupFromIntent(Intent intent) {
224        if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")");
225        String corpusName = getCorpusNameFromUri(intent.getData());
226        String query = intent.getStringExtra(SearchManager.QUERY);
227        Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA);
228        boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false);
229
230        setCorpus(corpusName);
231        setQuery(query, selectAll);
232        mAppSearchData = appSearchData;
233
234        if (startedIntoCorpusSelectionDialog()) {
235            mSearchActivityView.showCorpusSelectionDialog();
236        }
237    }
238
239    public boolean startedIntoCorpusSelectionDialog() {
240        return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction());
241    }
242
243    /**
244     * Removes corpus selector intent action, so that BACK works normally after
245     * dismissing and reopening the corpus selector.
246     */
247    public void clearStartedIntoCorpusSelectionDialog() {
248        Intent oldIntent = getIntent();
249        if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) {
250            Intent newIntent = new Intent(oldIntent);
251            newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
252            setIntent(newIntent);
253        }
254    }
255
256    public static Uri getCorpusUri(Corpus corpus) {
257        if (corpus == null) return null;
258        return new Uri.Builder()
259                .scheme(SCHEME_CORPUS)
260                .authority(corpus.getName())
261                .build();
262    }
263
264    private String getCorpusNameFromUri(Uri uri) {
265        if (uri == null) return null;
266        if (!SCHEME_CORPUS.equals(uri.getScheme())) return null;
267        return uri.getAuthority();
268    }
269
270    private Corpus getCorpus() {
271        return mSearchActivityView.getCorpus();
272    }
273
274    private String getCorpusName() {
275        return mSearchActivityView.getCorpusName();
276    }
277
278    private void setCorpus(String name) {
279        mSearchActivityView.setCorpus(name);
280    }
281
282    private QsbApplication getQsbApplication() {
283        return QsbApplication.get(this);
284    }
285
286    private Config getConfig() {
287        return getQsbApplication().getConfig();
288    }
289
290    protected SearchSettings getSettings() {
291        return getQsbApplication().getSettings();
292    }
293
294    private Corpora getCorpora() {
295        return getQsbApplication().getCorpora();
296    }
297
298    private CorpusRanker getCorpusRanker() {
299        return getQsbApplication().getCorpusRanker();
300    }
301
302    private ShortcutRepository getShortcutRepository() {
303        return getQsbApplication().getShortcutRepository();
304    }
305
306    private SuggestionsProvider getSuggestionsProvider() {
307        return getQsbApplication().getSuggestionsProvider();
308    }
309
310    private Logger getLogger() {
311        return getQsbApplication().getLogger();
312    }
313
314    @VisibleForTesting
315    public void setOnDestroyListener(OnDestroyListener l) {
316        mDestroyListener = l;
317    }
318
319    @Override
320    protected void onDestroy() {
321        if (DBG) Log.d(TAG, "onDestroy()");
322        getCorpora().unregisterDataSetObserver(mCorporaObserver);
323        mSearchActivityView.destroy();
324        super.onDestroy();
325        if (mDestroyListener != null) {
326            mDestroyListener.onDestroyed();
327        }
328    }
329
330    @Override
331    protected void onStop() {
332        if (DBG) Log.d(TAG, "onStop()");
333        if (!mTookAction) {
334            // TODO: This gets logged when starting other activities, e.g. by opening the search
335            // settings, or clicking a notification in the status bar.
336            // TODO we should log both sets of suggestions in 2-pane mode
337            getLogger().logExit(getCurrentSuggestions(), getQuery().length());
338        }
339        // Close all open suggestion cursors. The query will be redone in onResume()
340        // if we come back to this activity.
341        mSearchActivityView.clearSuggestions();
342        getQsbApplication().getShortcutRefresher().reset();
343        mSearchActivityView.onStop();
344        super.onStop();
345    }
346
347    @Override
348    protected void onPause() {
349        if (DBG) Log.d(TAG, "onPause()");
350        mSearchActivityView.onPause();
351        super.onPause();
352    }
353
354    @Override
355    protected void onRestart() {
356        if (DBG) Log.d(TAG, "onRestart()");
357        super.onRestart();
358    }
359
360    @Override
361    protected void onResume() {
362        if (DBG) Log.d(TAG, "onResume()");
363        super.onResume();
364        updateSuggestionsBuffered();
365        mSearchActivityView.onResume();
366        if (mTraceStartUp) Debug.stopMethodTracing();
367    }
368
369    @Override
370    public boolean onPrepareOptionsMenu(Menu menu) {
371        // Since the menu items are dynamic, we recreate the menu every time.
372        menu.clear();
373        createMenuItems(menu, true);
374        return true;
375    }
376
377    public void createMenuItems(Menu menu, boolean showDisabled) {
378        getSettings().addMenuItems(menu, showDisabled);
379        addHelpMenuItem(menu);
380    }
381
382    private void addHelpMenuItem(Menu menu) {
383        Intent helpIntent = getHelpIntent();
384        if (helpIntent != null) {
385            MenuInflater inflater = new MenuInflater(this);
386            inflater.inflate(R.menu.help, menu);
387            MenuItem item = menu.findItem(R.id.menu_help);
388            item.setIntent(helpIntent);
389        }
390    }
391
392    private Intent getHelpIntent() {
393        String helpUrl = getConfig().getHelpUrl();
394        if (TextUtils.isEmpty(helpUrl)) return null;
395        return new Intent(Intent.ACTION_VIEW, Uri.parse(helpUrl));
396    }
397
398    @Override
399    public void onWindowFocusChanged(boolean hasFocus) {
400        super.onWindowFocusChanged(hasFocus);
401        if (hasFocus) {
402            // Launch the IME after a bit
403            mHandler.postDelayed(mShowInputMethodTask, 0);
404        }
405    }
406
407    protected String getQuery() {
408        return mSearchActivityView.getQuery();
409    }
410
411    protected void setQuery(String query, boolean selectAll) {
412        mSearchActivityView.setQuery(query, selectAll);
413    }
414
415    public CorpusSelectionDialog getCorpusSelectionDialog() {
416        CorpusSelectionDialog dialog = createCorpusSelectionDialog();
417        dialog.setOwnerActivity(this);
418        dialog.setOnDismissListener(new CorpusSelectorDismissListener());
419        return dialog;
420    }
421
422    protected CorpusSelectionDialog createCorpusSelectionDialog() {
423        return new CorpusSelectionDialog(this, getSettings());
424    }
425
426    /**
427     * @return true if a search was performed as a result of this click, false otherwise.
428     */
429    protected boolean onSearchClicked(int method) {
430        String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' ');
431        if (DBG) Log.d(TAG, "Search clicked, query=" + query);
432
433        // Don't do empty queries
434        if (TextUtils.getTrimmedLength(query) == 0) return false;
435
436        Corpus searchCorpus = getSearchCorpus();
437        if (searchCorpus == null) return false;
438
439        mTookAction = true;
440
441        // Log search start
442        getLogger().logSearch(getCorpus(), method, query.length());
443
444        // Start search
445        startSearch(searchCorpus, query);
446        return true;
447    }
448
449    protected void startSearch(Corpus searchCorpus, String query) {
450        Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData);
451        launchIntent(intent);
452    }
453
454    protected void onVoiceSearchClicked() {
455        if (DBG) Log.d(TAG, "Voice Search clicked");
456        Corpus searchCorpus = getSearchCorpus();
457        if (searchCorpus == null) return;
458
459        mTookAction = true;
460
461        // Log voice search start
462        getLogger().logVoiceSearch(searchCorpus);
463
464        // Start voice search
465        Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData);
466        launchIntent(intent);
467    }
468
469    protected Corpus getSearchCorpus() {
470        return mSearchActivityView.getSearchCorpus();
471    }
472
473    protected SuggestionCursor getCurrentSuggestions() {
474        return mSearchActivityView.getCurrentPromotedSuggestions();
475    }
476
477    protected SuggestionPosition getCurrentSuggestions(SuggestionsAdapter<?> adapter, long id) {
478        SuggestionPosition pos = adapter.getSuggestion(id);
479        if (pos == null) {
480            return null;
481        }
482        SuggestionCursor suggestions = pos.getCursor();
483        int position = pos.getPosition();
484        if (suggestions == null) {
485            return null;
486        }
487        int count = suggestions.getCount();
488        if (position < 0 || position >= count) {
489            Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count);
490            return null;
491        }
492        suggestions.moveTo(position);
493        return pos;
494    }
495
496    protected Set<Corpus> getCurrentIncludedCorpora() {
497        Suggestions suggestions = mSearchActivityView.getSuggestions();
498        return suggestions == null  ? null : suggestions.getIncludedCorpora();
499    }
500
501    protected void launchIntent(Intent intent) {
502        if (DBG) Log.d(TAG, "launchIntent " + intent);
503        if (intent == null) {
504            return;
505        }
506        try {
507            startActivity(intent);
508        } catch (RuntimeException ex) {
509            // Since the intents for suggestions specified by suggestion providers,
510            // guard against them not being handled, not allowed, etc.
511            Log.e(TAG, "Failed to start " + intent.toUri(0), ex);
512        }
513    }
514
515    private boolean launchSuggestion(SuggestionsAdapter<?> adapter, long id) {
516        SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
517        if (suggestion == null) return false;
518
519        if (DBG) Log.d(TAG, "Launching suggestion " + id);
520        mTookAction = true;
521
522        // Log suggestion click
523        getLogger().logSuggestionClick(id, suggestion.getCursor(), getCurrentIncludedCorpora(),
524                Logger.SUGGESTION_CLICK_TYPE_LAUNCH);
525
526        // Create shortcut
527        getShortcutRepository().reportClick(suggestion.getCursor(), suggestion.getPosition());
528
529        // Launch intent
530        launchSuggestion(suggestion.getCursor(), suggestion.getPosition());
531
532        return true;
533    }
534
535    protected void launchSuggestion(SuggestionCursor suggestions, int position) {
536        suggestions.moveTo(position);
537        Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData);
538        launchIntent(intent);
539    }
540
541    protected void removeFromHistory(SuggestionsAdapter<?> adapter, long id) {
542        SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
543        if (suggestion == null) return;
544        removeFromHistory(suggestion.getCursor(), suggestion.getPosition());
545        // TODO: Log to event log?
546    }
547
548    protected void removeFromHistory(SuggestionCursor suggestions, int position) {
549        removeShortcut(suggestions, position);
550        removeFromHistoryDone(true);
551    }
552
553    protected void removeFromHistoryDone(boolean ok) {
554        Log.i(TAG, "Removed query from history, success=" + ok);
555        updateSuggestionsBuffered();
556        if (!ok) {
557            Toast.makeText(this, R.string.remove_from_history_failed, Toast.LENGTH_SHORT).show();
558        }
559    }
560
561    protected void removeShortcut(SuggestionCursor suggestions, int position) {
562        if (suggestions.isSuggestionShortcut()) {
563            if (DBG) Log.d(TAG, "Removing suggestion " + position + " from shortcuts");
564            getShortcutRepository().removeFromHistory(suggestions, position);
565        }
566    }
567
568    protected void clickedQuickContact(SuggestionsAdapter<?> adapter, long id) {
569        SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
570        if (suggestion == null) return;
571
572        if (DBG) Log.d(TAG, "Used suggestion " + suggestion.getPosition());
573        mTookAction = true;
574
575        // Log suggestion click
576        getLogger().logSuggestionClick(id, suggestion.getCursor(), getCurrentIncludedCorpora(),
577                Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT);
578
579        // Create shortcut
580        getShortcutRepository().reportClick(suggestion.getCursor(), suggestion.getPosition());
581    }
582
583    protected void refineSuggestion(SuggestionsAdapter<?> adapter, long id) {
584        if (DBG) Log.d(TAG, "query refine clicked, pos " + id);
585        SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
586        if (suggestion == null) {
587            return;
588        }
589        String query = suggestion.getSuggestionQuery();
590        if (TextUtils.isEmpty(query)) {
591            return;
592        }
593
594        // Log refine click
595        getLogger().logSuggestionClick(id, suggestion.getCursor(), getCurrentIncludedCorpora(),
596                Logger.SUGGESTION_CLICK_TYPE_REFINE);
597
598        // Put query + space in query text view
599        String queryWithSpace = query + ' ';
600        setQuery(queryWithSpace, false);
601        updateSuggestions();
602        mSearchActivityView.focusQueryTextView();
603    }
604
605    private void updateSuggestionsBuffered() {
606        if (DBG) Log.d(TAG, "updateSuggestionsBuffered()");
607        mHandler.removeCallbacks(mUpdateSuggestionsTask);
608        long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
609        mHandler.postDelayed(mUpdateSuggestionsTask, delay);
610    }
611
612    private void gotSuggestions(Suggestions suggestions) {
613        if (mStarting) {
614            mStarting = false;
615            String source = getIntent().getStringExtra(Search.SOURCE);
616            int latency = mStartLatencyTracker.getLatency();
617            getLogger().logStart(mOnCreateLatency, latency, source, getCorpus(),
618                    suggestions == null ? null : suggestions.getExpectedCorpora());
619            getQsbApplication().onStartupComplete();
620        }
621    }
622
623    private void getCorporaToQuery(Consumer<List<Corpus>> consumer) {
624        Corpus corpus = getCorpus();
625        if (corpus == null) {
626            getCorpusRanker().getCorporaInAll(Consumers.createAsyncConsumer(mHandler, consumer));
627        } else {
628            List<Corpus> corpora = new ArrayList<Corpus>();
629            Corpus searchCorpus = getSearchCorpus();
630            if (searchCorpus != null) corpora.add(searchCorpus);
631            consumer.consume(corpora);
632        }
633    }
634
635    protected void getShortcutsForQuery(String query, Collection<Corpus> corporaToQuery,
636            final Suggestions suggestions) {
637        ShortcutRepository shortcutRepo = getShortcutRepository();
638        if (shortcutRepo == null) return;
639        if (query.length() == 0 && !getConfig().showShortcutsForZeroQuery()) {
640            return;
641        }
642        Consumer<ShortcutCursor> consumer = Consumers.createAsyncCloseableConsumer(mHandler,
643                new Consumer<ShortcutCursor>() {
644            public boolean consume(ShortcutCursor shortcuts) {
645                suggestions.setShortcuts(shortcuts);
646                return true;
647            }
648        });
649        shortcutRepo.getShortcutsForQuery(query, corporaToQuery,
650                getSettings().allowWebSearchShortcuts(), consumer);
651    }
652
653    public void updateSuggestions() {
654        if (DBG) Log.d(TAG, "updateSuggestions()");
655        final String query = CharMatcher.WHITESPACE.trimLeadingFrom(getQuery());
656        getQsbApplication().getSourceTaskExecutor().cancelPendingTasks();
657        getCorporaToQuery(new Consumer<List<Corpus>>(){
658            @Override
659            public boolean consume(List<Corpus> corporaToQuery) {
660                updateSuggestions(query, corporaToQuery);
661                return true;
662            }
663        });
664    }
665
666    protected void updateSuggestions(String query, List<Corpus> corporaToQuery) {
667        if (DBG) Log.d(TAG, "updateSuggestions(\"" + query+"\"," + corporaToQuery + ")");
668        Suggestions suggestions = getSuggestionsProvider().getSuggestions(
669                query, corporaToQuery);
670        getShortcutsForQuery(query, corporaToQuery, suggestions);
671
672        // Log start latency if this is the first suggestions update
673        gotSuggestions(suggestions);
674
675        showSuggestions(suggestions);
676    }
677
678    protected void showSuggestions(Suggestions suggestions) {
679        mSearchActivityView.setSuggestions(suggestions);
680    }
681
682    private class ClickHandler implements SuggestionClickListener {
683
684        public void onSuggestionQuickContactClicked(SuggestionsAdapter<?> adapter, long id) {
685            clickedQuickContact(adapter, id);
686        }
687
688        public void onSuggestionClicked(SuggestionsAdapter<?> adapter, long id) {
689            launchSuggestion(adapter, id);
690        }
691
692        public void onSuggestionRemoveFromHistoryClicked(SuggestionsAdapter<?> adapter, long id) {
693            removeFromHistory(adapter, id);
694        }
695
696        public void onSuggestionQueryRefineClicked(SuggestionsAdapter<?> adapter, long id) {
697            refineSuggestion(adapter, id);
698        }
699    }
700
701    private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
702        public void onDismiss(DialogInterface dialog) {
703            if (DBG) Log.d(TAG, "Corpus selector dismissed");
704            clearStartedIntoCorpusSelectionDialog();
705        }
706    }
707
708    private class CorporaObserver extends DataSetObserver {
709        @Override
710        public void onChanged() {
711            setCorpus(getCorpusName());
712            updateSuggestions();
713        }
714    }
715
716    public interface OnDestroyListener {
717        void onDestroyed();
718    }
719
720}
721