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