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