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