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.AlertDialog;
30import android.app.SearchManager;
31import android.content.DialogInterface;
32import android.content.Intent;
33import android.database.DataSetObserver;
34import android.net.Uri;
35import android.os.Bundle;
36import android.os.Debug;
37import android.os.Handler;
38import android.text.TextUtils;
39import android.util.Log;
40import android.view.Menu;
41import android.view.View;
42import android.widget.Toast;
43
44import java.io.File;
45import java.util.ArrayList;
46import java.util.Collection;
47import java.util.List;
48import java.util.Set;
49
50/**
51 * The main activity for Quick Search Box. Shows the search UI.
52 *
53 */
54public class SearchActivity extends Activity {
55
56    private static final boolean DBG = false;
57    private static final String TAG = "QSB.SearchActivity";
58
59    private static final String SCHEME_CORPUS = "qsb.corpus";
60
61    public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS
62            = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS";
63
64    private static final String INTENT_EXTRA_TRACE_START_UP = "trace_start_up";
65
66    // Keys for the saved instance state.
67    private static final String INSTANCE_KEY_CORPUS = "corpus";
68    private static final String INSTANCE_KEY_QUERY = "query";
69
70    private static final String ACTIVITY_HELP_CONTEXT = "search";
71
72    private boolean mTraceStartUp;
73    // Measures time from for last onCreate()/onNewIntent() call.
74    private LatencyTracker mStartLatencyTracker;
75    // Measures time spent inside onCreate()
76    private LatencyTracker mOnCreateTracker;
77    private int mOnCreateLatency;
78    // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume().
79    private boolean mStarting;
80    // True if the user has taken some action, e.g. launching a search, voice search,
81    // or suggestions, since QSB was last started.
82    private boolean mTookAction;
83
84    private SearchActivityView mSearchActivityView;
85
86    private CorporaObserver mCorporaObserver;
87
88    private Bundle mAppSearchData;
89
90    private final Handler mHandler = new Handler();
91    private final Runnable mUpdateSuggestionsTask = new Runnable() {
92        public void run() {
93            updateSuggestions();
94        }
95    };
96
97    private final Runnable mShowInputMethodTask = new Runnable() {
98        public void run() {
99            mSearchActivityView.showInputMethodForQuery();
100        }
101    };
102
103    private OnDestroyListener mDestroyListener;
104
105    /** Called when the activity is first created. */
106    @Override
107    public void onCreate(Bundle savedInstanceState) {
108        mTraceStartUp = getIntent().hasExtra(INTENT_EXTRA_TRACE_START_UP);
109        if (mTraceStartUp) {
110            String traceFile = new File(getDir("traces", 0), "qsb-start.trace").getAbsolutePath();
111            Log.i(TAG, "Writing start-up trace to " + traceFile);
112            Debug.startMethodTracing(traceFile);
113        }
114        recordStartTime();
115        if (DBG) Log.d(TAG, "onCreate()");
116        super.onCreate(savedInstanceState);
117
118        // This forces the HTTP request to check the users domain to be
119        // sent as early as possible.
120        QsbApplication.get(this).getSearchBaseUrlHelper();
121
122        mSearchActivityView = setupContentView();
123
124        if (getConfig().showScrollingSuggestions()) {
125            mSearchActivityView.setMaxPromotedSuggestions(getConfig().getMaxPromotedSuggestions());
126        } else {
127            mSearchActivityView.limitSuggestionsToViewHeight();
128        }
129        if (getConfig().showScrollingResults()) {
130            mSearchActivityView.setMaxPromotedResults(getConfig().getMaxPromotedResults());
131        } else {
132            mSearchActivityView.limitResultsToViewHeight();
133        }
134
135        mSearchActivityView.setSearchClickListener(new SearchActivityView.SearchClickListener() {
136            public boolean onSearchClicked(int method) {
137                return SearchActivity.this.onSearchClicked(method);
138            }
139        });
140
141        mSearchActivityView.setQueryListener(new SearchActivityView.QueryListener() {
142            public void onQueryChanged() {
143                updateSuggestionsBuffered();
144            }
145        });
146
147        mSearchActivityView.setSuggestionClickListener(new ClickHandler());
148
149        mSearchActivityView.setVoiceSearchButtonClickListener(new View.OnClickListener() {
150            public void onClick(View view) {
151                onVoiceSearchClicked();
152            }
153        });
154
155        View.OnClickListener finishOnClick = new View.OnClickListener() {
156            public void onClick(View v) {
157                finish();
158            }
159        };
160        mSearchActivityView.setExitClickListener(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        getQsbApplication().getHelp().addHelpMenuItem(menu, ACTIVITY_HELP_CONTEXT);
380    }
381
382    @Override
383    public void onWindowFocusChanged(boolean hasFocus) {
384        super.onWindowFocusChanged(hasFocus);
385        if (hasFocus) {
386            // Launch the IME after a bit
387            mHandler.postDelayed(mShowInputMethodTask, 0);
388        }
389    }
390
391    protected String getQuery() {
392        return mSearchActivityView.getQuery();
393    }
394
395    protected void setQuery(String query, boolean selectAll) {
396        mSearchActivityView.setQuery(query, selectAll);
397    }
398
399    public CorpusSelectionDialog getCorpusSelectionDialog() {
400        CorpusSelectionDialog dialog = createCorpusSelectionDialog();
401        dialog.setOwnerActivity(this);
402        dialog.setOnDismissListener(new CorpusSelectorDismissListener());
403        return dialog;
404    }
405
406    protected CorpusSelectionDialog createCorpusSelectionDialog() {
407        return new CorpusSelectionDialog(this, getSettings());
408    }
409
410    /**
411     * @return true if a search was performed as a result of this click, false otherwise.
412     */
413    protected boolean onSearchClicked(int method) {
414        String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' ');
415        if (DBG) Log.d(TAG, "Search clicked, query=" + query);
416
417        // Don't do empty queries
418        if (TextUtils.getTrimmedLength(query) == 0) return false;
419
420        Corpus searchCorpus = getSearchCorpus();
421        if (searchCorpus == null) return false;
422
423        mTookAction = true;
424
425        // Log search start
426        getLogger().logSearch(getCorpus(), method, query.length());
427
428        // Start search
429        startSearch(searchCorpus, query);
430        return true;
431    }
432
433    protected void startSearch(Corpus searchCorpus, String query) {
434        Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData);
435        launchIntent(intent);
436    }
437
438    protected void onVoiceSearchClicked() {
439        if (DBG) Log.d(TAG, "Voice Search clicked");
440        Corpus searchCorpus = getSearchCorpus();
441        if (searchCorpus == null) return;
442
443        mTookAction = true;
444
445        // Log voice search start
446        getLogger().logVoiceSearch(searchCorpus);
447
448        // Start voice search
449        Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData);
450        launchIntent(intent);
451    }
452
453    protected Corpus getSearchCorpus() {
454        return mSearchActivityView.getSearchCorpus();
455    }
456
457    protected SuggestionCursor getCurrentSuggestions() {
458        return mSearchActivityView.getCurrentPromotedSuggestions();
459    }
460
461    protected SuggestionPosition getCurrentSuggestions(SuggestionsAdapter<?> adapter, long id) {
462        SuggestionPosition pos = adapter.getSuggestion(id);
463        if (pos == null) {
464            return null;
465        }
466        SuggestionCursor suggestions = pos.getCursor();
467        int position = pos.getPosition();
468        if (suggestions == null) {
469            return null;
470        }
471        int count = suggestions.getCount();
472        if (position < 0 || position >= count) {
473            Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count);
474            return null;
475        }
476        suggestions.moveTo(position);
477        return pos;
478    }
479
480    protected Set<Corpus> getCurrentIncludedCorpora() {
481        Suggestions suggestions = mSearchActivityView.getSuggestions();
482        return suggestions == null  ? null : suggestions.getIncludedCorpora();
483    }
484
485    protected void launchIntent(Intent intent) {
486        if (DBG) Log.d(TAG, "launchIntent " + intent);
487        if (intent == null) {
488            return;
489        }
490        try {
491            startActivity(intent);
492        } catch (RuntimeException ex) {
493            // Since the intents for suggestions specified by suggestion providers,
494            // guard against them not being handled, not allowed, etc.
495            Log.e(TAG, "Failed to start " + intent.toUri(0), ex);
496        }
497    }
498
499    private boolean launchSuggestion(SuggestionsAdapter<?> adapter, long id) {
500        SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
501        if (suggestion == null) return false;
502
503        if (DBG) Log.d(TAG, "Launching suggestion " + id);
504        mTookAction = true;
505
506        // Log suggestion click
507        getLogger().logSuggestionClick(id, suggestion.getCursor(), getCurrentIncludedCorpora(),
508                Logger.SUGGESTION_CLICK_TYPE_LAUNCH);
509
510        // Create shortcut
511        getShortcutRepository().reportClick(suggestion.getCursor(), suggestion.getPosition());
512
513        // Launch intent
514        launchSuggestion(suggestion.getCursor(), suggestion.getPosition());
515
516        return true;
517    }
518
519    protected void launchSuggestion(SuggestionCursor suggestions, int position) {
520        suggestions.moveTo(position);
521        Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData);
522        launchIntent(intent);
523    }
524
525    protected void removeFromHistoryClicked(final SuggestionsAdapter<?> adapter,
526            final long id) {
527        SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
528        if (suggestion == null) return;
529        CharSequence title = suggestion.getSuggestionText1();
530        AlertDialog dialog = new AlertDialog.Builder(this)
531                .setTitle(title)
532                .setMessage(R.string.remove_from_history)
533                .setPositiveButton(android.R.string.ok,
534                        new DialogInterface.OnClickListener() {
535                            public void onClick(DialogInterface dialog, int which) {
536                                // TODO: what if the suggestions have changed?
537                                removeFromHistory(adapter, id);
538                            }
539                        })
540                .setNegativeButton(android.R.string.cancel, null)
541                .create();
542        dialog.show();
543    }
544
545    protected void removeFromHistory(SuggestionsAdapter<?> adapter, long id) {
546        SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
547        if (suggestion == null) return;
548        removeFromHistory(suggestion.getCursor(), suggestion.getPosition());
549        // TODO: Log to event log?
550    }
551
552    protected void removeFromHistory(SuggestionCursor suggestions, int position) {
553        removeShortcut(suggestions, position);
554        removeFromHistoryDone(true);
555    }
556
557    protected void removeFromHistoryDone(boolean ok) {
558        Log.i(TAG, "Removed query from history, success=" + ok);
559        updateSuggestionsBuffered();
560        if (!ok) {
561            Toast.makeText(this, R.string.remove_from_history_failed, Toast.LENGTH_SHORT).show();
562        }
563    }
564
565    protected void removeShortcut(SuggestionCursor suggestions, int position) {
566        if (suggestions.isSuggestionShortcut()) {
567            if (DBG) Log.d(TAG, "Removing suggestion " + position + " from shortcuts");
568            getShortcutRepository().removeFromHistory(suggestions, position);
569        }
570    }
571
572    protected void clickedQuickContact(SuggestionsAdapter<?> adapter, long id) {
573        SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
574        if (suggestion == null) return;
575
576        if (DBG) Log.d(TAG, "Used suggestion " + suggestion.getPosition());
577        mTookAction = true;
578
579        // Log suggestion click
580        getLogger().logSuggestionClick(id, suggestion.getCursor(), getCurrentIncludedCorpora(),
581                Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT);
582
583        // Create shortcut
584        getShortcutRepository().reportClick(suggestion.getCursor(), suggestion.getPosition());
585    }
586
587    protected void refineSuggestion(SuggestionsAdapter<?> adapter, long id) {
588        if (DBG) Log.d(TAG, "query refine clicked, pos " + id);
589        SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
590        if (suggestion == null) {
591            return;
592        }
593        String query = suggestion.getSuggestionQuery();
594        if (TextUtils.isEmpty(query)) {
595            return;
596        }
597
598        // Log refine click
599        getLogger().logSuggestionClick(id, suggestion.getCursor(), getCurrentIncludedCorpora(),
600                Logger.SUGGESTION_CLICK_TYPE_REFINE);
601
602        // Put query + space in query text view
603        String queryWithSpace = query + ' ';
604        setQuery(queryWithSpace, false);
605        updateSuggestions();
606        mSearchActivityView.focusQueryTextView();
607    }
608
609    private void updateSuggestionsBuffered() {
610        if (DBG) Log.d(TAG, "updateSuggestionsBuffered()");
611        mHandler.removeCallbacks(mUpdateSuggestionsTask);
612        long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
613        mHandler.postDelayed(mUpdateSuggestionsTask, delay);
614    }
615
616    private void gotSuggestions(Suggestions suggestions) {
617        if (mStarting) {
618            mStarting = false;
619            String source = getIntent().getStringExtra(Search.SOURCE);
620            int latency = mStartLatencyTracker.getLatency();
621            getLogger().logStart(mOnCreateLatency, latency, source, getCorpus(),
622                    suggestions == null ? null : suggestions.getExpectedCorpora());
623            getQsbApplication().onStartupComplete();
624        }
625    }
626
627    private void getCorporaToQuery(Consumer<List<Corpus>> consumer) {
628        Corpus corpus = getCorpus();
629        if (corpus == null) {
630            getCorpusRanker().getCorporaInAll(Consumers.createAsyncConsumer(mHandler, consumer));
631        } else {
632            List<Corpus> corpora = new ArrayList<Corpus>();
633            Corpus searchCorpus = getSearchCorpus();
634            if (searchCorpus != null) corpora.add(searchCorpus);
635            consumer.consume(corpora);
636        }
637    }
638
639    protected void getShortcutsForQuery(String query, Collection<Corpus> corporaToQuery,
640            final Suggestions suggestions) {
641        ShortcutRepository shortcutRepo = getShortcutRepository();
642        if (shortcutRepo == null) return;
643        if (query.length() == 0 && !getConfig().showShortcutsForZeroQuery()) {
644            return;
645        }
646        Consumer<ShortcutCursor> consumer = Consumers.createAsyncCloseableConsumer(mHandler,
647                new Consumer<ShortcutCursor>() {
648            public boolean consume(ShortcutCursor shortcuts) {
649                suggestions.setShortcuts(shortcuts);
650                return true;
651            }
652        });
653        shortcutRepo.getShortcutsForQuery(query, corporaToQuery,
654                getSettings().allowWebSearchShortcuts(), consumer);
655    }
656
657    public void updateSuggestions() {
658        if (DBG) Log.d(TAG, "updateSuggestions()");
659        final String query = CharMatcher.WHITESPACE.trimLeadingFrom(getQuery());
660        getQsbApplication().getSourceTaskExecutor().cancelPendingTasks();
661        getCorporaToQuery(new Consumer<List<Corpus>>(){
662            @Override
663            public boolean consume(List<Corpus> corporaToQuery) {
664                updateSuggestions(query, corporaToQuery);
665                return true;
666            }
667        });
668    }
669
670    protected void updateSuggestions(String query, List<Corpus> corporaToQuery) {
671        if (DBG) Log.d(TAG, "updateSuggestions(\"" + query+"\"," + corporaToQuery + ")");
672        Suggestions suggestions = getSuggestionsProvider().getSuggestions(
673                query, corporaToQuery);
674        getShortcutsForQuery(query, corporaToQuery, suggestions);
675
676        // Log start latency if this is the first suggestions update
677        gotSuggestions(suggestions);
678
679        showSuggestions(suggestions);
680    }
681
682    protected void showSuggestions(Suggestions suggestions) {
683        mSearchActivityView.setSuggestions(suggestions);
684    }
685
686    private class ClickHandler implements SuggestionClickListener {
687
688        public void onSuggestionQuickContactClicked(SuggestionsAdapter<?> adapter, long id) {
689            clickedQuickContact(adapter, id);
690        }
691
692        public void onSuggestionClicked(SuggestionsAdapter<?> adapter, long id) {
693            launchSuggestion(adapter, id);
694        }
695
696        public void onSuggestionRemoveFromHistoryClicked(SuggestionsAdapter<?> adapter, long id) {
697            removeFromHistoryClicked(adapter, id);
698        }
699
700        public void onSuggestionQueryRefineClicked(SuggestionsAdapter<?> adapter, long id) {
701            refineSuggestion(adapter, id);
702        }
703    }
704
705    private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
706        public void onDismiss(DialogInterface dialog) {
707            if (DBG) Log.d(TAG, "Corpus selector dismissed");
708            clearStartedIntoCorpusSelectionDialog();
709        }
710    }
711
712    private class CorporaObserver extends DataSetObserver {
713        @Override
714        public void onChanged() {
715            setCorpus(getCorpusName());
716            updateSuggestions();
717        }
718    }
719
720    public interface OnDestroyListener {
721        void onDestroyed();
722    }
723
724}
725