TextToSpeechService.java revision 8d1fc2403b8277e68d7816b2bbf05464a4c7a58a
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package android.speech.tts;
17
18import android.app.Service;
19import android.content.Intent;
20import android.net.Uri;
21import android.os.Bundle;
22import android.os.ConditionVariable;
23import android.os.Handler;
24import android.os.HandlerThread;
25import android.os.IBinder;
26import android.os.Looper;
27import android.os.Message;
28import android.os.MessageQueue;
29import android.os.RemoteCallbackList;
30import android.os.RemoteException;
31import android.provider.Settings;
32import android.speech.tts.TextToSpeech.Engine;
33import android.text.TextUtils;
34import android.util.Log;
35
36import java.io.File;
37import java.io.IOException;
38import java.util.HashMap;
39import java.util.Locale;
40
41
42/**
43 * Abstract base class for TTS engine implementations.
44 */
45public abstract class TextToSpeechService extends Service {
46
47    private static final boolean DBG = false;
48    private static final String TAG = "TextToSpeechService";
49
50    private static final int MAX_SPEECH_ITEM_CHAR_LENGTH = 4000;
51    private static final String SYNTH_THREAD_NAME = "SynthThread";
52
53    private SynthHandler mSynthHandler;
54    // A thread and it's associated handler for playing back any audio
55    // associated with this TTS engine. Will handle all requests except synthesis
56    // to file requests, which occur on the synthesis thread.
57    private AudioPlaybackHandler mAudioPlaybackHandler;
58
59    private CallbackMap mCallbacks;
60
61    private int mDefaultAvailability = TextToSpeech.LANG_NOT_SUPPORTED;
62
63    @Override
64    public void onCreate() {
65        if (DBG) Log.d(TAG, "onCreate()");
66        super.onCreate();
67
68        SynthThread synthThread = new SynthThread();
69        synthThread.start();
70        mSynthHandler = new SynthHandler(synthThread.getLooper());
71
72        HandlerThread audioTrackThread = new HandlerThread("TTS.audioTrackThread");
73        audioTrackThread.start();
74        mAudioPlaybackHandler = new AudioPlaybackHandler(audioTrackThread.getLooper());
75
76        mCallbacks = new CallbackMap();
77
78        // Load default language
79        mDefaultAvailability = onLoadLanguage(getDefaultLanguage(),
80                getDefaultCountry(), getDefaultVariant());
81    }
82
83    @Override
84    public void onDestroy() {
85        if (DBG) Log.d(TAG, "onDestroy()");
86
87        // Tell the synthesizer to stop
88        mSynthHandler.quit();
89        // Tell the audio playback thread to stop.
90        mAudioPlaybackHandler.quit();
91        // Unregister all callbacks.
92        mCallbacks.kill();
93
94        super.onDestroy();
95    }
96
97    /**
98     * Checks whether the engine supports a given language.
99     *
100     * Can be called on multiple threads.
101     *
102     * @param lang ISO-3 language code.
103     * @param country ISO-3 country code. May be empty or null.
104     * @param variant Language variant. May be empty or null.
105     * @return Code indicating the support status for the locale.
106     *         One of {@link TextToSpeech#LANG_AVAILABLE},
107     *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
108     *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
109     *         {@link TextToSpeech#LANG_MISSING_DATA}
110     *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
111     */
112    protected abstract int onIsLanguageAvailable(String lang, String country, String variant);
113
114    /**
115     * Returns the language, country and variant currently being used by the TTS engine.
116     *
117     * Can be called on multiple threads.
118     *
119     * @return A 3-element array, containing language (ISO 3-letter code),
120     *         country (ISO 3-letter code) and variant used by the engine.
121     *         The country and variant may be {@code ""}. If country is empty, then variant must
122     *         be empty too.
123     * @see Locale#getISO3Language()
124     * @see Locale#getISO3Country()
125     * @see Locale#getVariant()
126     */
127    protected abstract String[] onGetLanguage();
128
129    /**
130     * Notifies the engine that it should load a speech synthesis language. There is no guarantee
131     * that this method is always called before the language is used for synthesis. It is merely
132     * a hint to the engine that it will probably get some synthesis requests for this language
133     * at some point in the future.
134     *
135     * Can be called on multiple threads.
136     *
137     * @param lang ISO-3 language code.
138     * @param country ISO-3 country code. May be empty or null.
139     * @param variant Language variant. May be empty or null.
140     * @return Code indicating the support status for the locale.
141     *         One of {@link TextToSpeech#LANG_AVAILABLE},
142     *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
143     *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
144     *         {@link TextToSpeech#LANG_MISSING_DATA}
145     *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
146     */
147    protected abstract int onLoadLanguage(String lang, String country, String variant);
148
149    /**
150     * Notifies the service that it should stop any in-progress speech synthesis.
151     * This method can be called even if no speech synthesis is currently in progress.
152     *
153     * Can be called on multiple threads, but not on the synthesis thread.
154     */
155    protected abstract void onStop();
156
157    /**
158     * Tells the service to synthesize speech from the given text. This method should
159     * block until the synthesis is finished.
160     *
161     * Called on the synthesis thread.
162     *
163     * @param request The synthesis request. The method should use the methods in the request
164     *         object to communicate the results of the synthesis.
165     */
166    protected abstract void onSynthesizeText(SynthesisRequest request);
167
168    private boolean areDefaultsEnforced() {
169        return getSecureSettingInt(Settings.Secure.TTS_USE_DEFAULTS,
170                TextToSpeech.Engine.USE_DEFAULTS) == 1;
171    }
172
173    private int getDefaultSpeechRate() {
174        return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE);
175    }
176
177    private String getDefaultLanguage() {
178        return getSecureSettingString(Settings.Secure.TTS_DEFAULT_LANG,
179                Locale.getDefault().getISO3Language());
180    }
181
182    private String getDefaultCountry() {
183        return getSecureSettingString(Settings.Secure.TTS_DEFAULT_COUNTRY,
184                Locale.getDefault().getISO3Country());
185    }
186
187    private String getDefaultVariant() {
188        return getSecureSettingString(Settings.Secure.TTS_DEFAULT_VARIANT,
189                Locale.getDefault().getVariant());
190    }
191
192    private int getSecureSettingInt(String name, int defaultValue) {
193        return Settings.Secure.getInt(getContentResolver(), name, defaultValue);
194    }
195
196    private String getSecureSettingString(String name, String defaultValue) {
197        String value = Settings.Secure.getString(getContentResolver(), name);
198        return value != null ? value : defaultValue;
199    }
200
201    /**
202     * Synthesizer thread. This thread is used to run {@link SynthHandler}.
203     */
204    private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler {
205
206        private boolean mFirstIdle = true;
207
208        public SynthThread() {
209            super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_AUDIO);
210        }
211
212        @Override
213        protected void onLooperPrepared() {
214            getLooper().getQueue().addIdleHandler(this);
215        }
216
217        @Override
218        public boolean queueIdle() {
219            if (mFirstIdle) {
220                mFirstIdle = false;
221            } else {
222                broadcastTtsQueueProcessingCompleted();
223            }
224            return true;
225        }
226
227        private void broadcastTtsQueueProcessingCompleted() {
228            Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED);
229            if (DBG) Log.d(TAG, "Broadcasting: " + i);
230            sendBroadcast(i);
231        }
232    }
233
234    private class SynthHandler extends Handler {
235
236        private SpeechItem mCurrentSpeechItem = null;
237
238        public SynthHandler(Looper looper) {
239            super(looper);
240        }
241
242        private synchronized SpeechItem getCurrentSpeechItem() {
243            return mCurrentSpeechItem;
244        }
245
246        private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) {
247            SpeechItem old = mCurrentSpeechItem;
248            mCurrentSpeechItem = speechItem;
249            return old;
250        }
251
252        public boolean isSpeaking() {
253            return getCurrentSpeechItem() != null;
254        }
255
256        public void quit() {
257            // Don't process any more speech items
258            getLooper().quit();
259            // Stop the current speech item
260            SpeechItem current = setCurrentSpeechItem(null);
261            if (current != null) {
262                current.stop();
263            }
264        }
265
266        /**
267         * Adds a speech item to the queue.
268         *
269         * Called on a service binder thread.
270         */
271        public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
272            if (!speechItem.isValid()) {
273                return TextToSpeech.ERROR;
274            }
275            // TODO: The old code also supported the undocumented queueMode == 2,
276            // which clears out all pending items from the calling app, as well as all
277            // non-file items from other apps.
278            if (queueMode == TextToSpeech.QUEUE_FLUSH) {
279                stop(speechItem.getCallingApp());
280            }
281            Runnable runnable = new Runnable() {
282                @Override
283                public void run() {
284                    setCurrentSpeechItem(speechItem);
285                    speechItem.play();
286                    setCurrentSpeechItem(null);
287                }
288            };
289            Message msg = Message.obtain(this, runnable);
290            // The obj is used to remove all callbacks from the given app in stop(String).
291            msg.obj = speechItem.getCallingApp();
292            if (sendMessage(msg)) {
293                return TextToSpeech.SUCCESS;
294            } else {
295                Log.w(TAG, "SynthThread has quit");
296                return TextToSpeech.ERROR;
297            }
298        }
299
300        /**
301         * Stops all speech output and removes any utterances still in the queue for
302         * the calling app.
303         *
304         * Called on a service binder thread.
305         */
306        public int stop(String callingApp) {
307            if (TextUtils.isEmpty(callingApp)) {
308                return TextToSpeech.ERROR;
309            }
310            removeCallbacksAndMessages(callingApp);
311            SpeechItem current = setCurrentSpeechItem(null);
312            if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) {
313                current.stop();
314            }
315
316            return TextToSpeech.SUCCESS;
317        }
318    }
319
320    interface UtteranceCompletedDispatcher {
321        public void dispatchUtteranceCompleted();
322    }
323
324    /**
325     * An item in the synth thread queue.
326     */
327    private abstract class SpeechItem implements UtteranceCompletedDispatcher {
328        private final String mCallingApp;
329        protected final Bundle mParams;
330        private boolean mStarted = false;
331        private boolean mStopped = false;
332
333        public SpeechItem(String callingApp, Bundle params) {
334            mCallingApp = callingApp;
335            mParams = params;
336        }
337
338        public String getCallingApp() {
339            return mCallingApp;
340        }
341
342        /**
343         * Checker whether the item is valid. If this method returns false, the item should not
344         * be played.
345         */
346        public abstract boolean isValid();
347
348        /**
349         * Plays the speech item. Blocks until playback is finished.
350         * Must not be called more than once.
351         *
352         * Only called on the synthesis thread.
353         *
354         * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}.
355         */
356        public int play() {
357            synchronized (this) {
358                if (mStarted) {
359                    throw new IllegalStateException("play() called twice");
360                }
361                mStarted = true;
362            }
363            return playImpl();
364        }
365
366        /**
367         * Stops the speech item.
368         * Must not be called more than once.
369         *
370         * Can be called on multiple threads,  but not on the synthesis thread.
371         */
372        public void stop() {
373            synchronized (this) {
374                if (mStopped) {
375                    throw new IllegalStateException("stop() called twice");
376                }
377                mStopped = true;
378            }
379            stopImpl();
380        }
381
382        public void dispatchUtteranceCompleted() {
383            final String utteranceId = getUtteranceId();
384            if (!TextUtils.isEmpty(utteranceId)) {
385                mCallbacks.dispatchUtteranceCompleted(getCallingApp(), utteranceId);
386            }
387        }
388
389        protected abstract int playImpl();
390
391        protected abstract void stopImpl();
392
393        public int getStreamType() {
394            return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM);
395        }
396
397        public float getVolume() {
398            return getFloatParam(Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME);
399        }
400
401        public float getPan() {
402            return getFloatParam(Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN);
403        }
404
405        public String getUtteranceId() {
406            return getStringParam(Engine.KEY_PARAM_UTTERANCE_ID, null);
407        }
408
409        protected String getStringParam(String key, String defaultValue) {
410            return mParams == null ? defaultValue : mParams.getString(key, defaultValue);
411        }
412
413        protected int getIntParam(String key, int defaultValue) {
414            return mParams == null ? defaultValue : mParams.getInt(key, defaultValue);
415        }
416
417        protected float getFloatParam(String key, float defaultValue) {
418            return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue);
419        }
420    }
421
422    class SynthesisSpeechItem extends SpeechItem {
423        private final String mText;
424        private SynthesisRequest mSynthesisRequest;
425
426        public SynthesisSpeechItem(String callingApp, Bundle params, String text) {
427            super(callingApp, params);
428            mText = text;
429        }
430
431        public String getText() {
432            return mText;
433        }
434
435        @Override
436        public boolean isValid() {
437            if (TextUtils.isEmpty(mText)) {
438                Log.w(TAG, "Got empty text");
439                return false;
440            }
441            if (mText.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH){
442                Log.w(TAG, "Text too long: " + mText.length() + " chars");
443                return false;
444            }
445            return true;
446        }
447
448        @Override
449        protected int playImpl() {
450            SynthesisRequest synthesisRequest;
451            synchronized (this) {
452                mSynthesisRequest = createSynthesisRequest();
453                synthesisRequest = mSynthesisRequest;
454            }
455            setRequestParams(synthesisRequest);
456            TextToSpeechService.this.onSynthesizeText(synthesisRequest);
457            return synthesisRequest.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR;
458        }
459
460        protected SynthesisRequest createSynthesisRequest() {
461            return new PlaybackSynthesisRequest(mText, mParams,
462                    getStreamType(), getVolume(), getPan(), mAudioPlaybackHandler,
463                    this);
464        }
465
466        private void setRequestParams(SynthesisRequest request) {
467            if (areDefaultsEnforced()) {
468                request.setLanguage(getDefaultLanguage(), getDefaultCountry(), getDefaultVariant());
469                request.setSpeechRate(getDefaultSpeechRate());
470            } else {
471                request.setLanguage(getLanguage(), getCountry(), getVariant());
472                request.setSpeechRate(getSpeechRate());
473            }
474            request.setPitch(getPitch());
475        }
476
477        @Override
478        protected void stopImpl() {
479            SynthesisRequest synthesisRequest;
480            synchronized (this) {
481                synthesisRequest = mSynthesisRequest;
482            }
483            synthesisRequest.stop();
484            TextToSpeechService.this.onStop();
485        }
486
487        public String getLanguage() {
488            return getStringParam(Engine.KEY_PARAM_LANGUAGE, getDefaultLanguage());
489        }
490
491        private boolean hasLanguage() {
492            return !TextUtils.isEmpty(getStringParam(Engine.KEY_PARAM_LANGUAGE, null));
493        }
494
495        private String getCountry() {
496            if (!hasLanguage()) return getDefaultCountry();
497            return getStringParam(Engine.KEY_PARAM_COUNTRY, "");
498        }
499
500        private String getVariant() {
501            if (!hasLanguage()) return getDefaultVariant();
502            return getStringParam(Engine.KEY_PARAM_VARIANT, "");
503        }
504
505        private int getSpeechRate() {
506            return getIntParam(Engine.KEY_PARAM_RATE, getDefaultSpeechRate());
507        }
508
509        private int getPitch() {
510            return getIntParam(Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH);
511        }
512    }
513
514    private class SynthesisToFileSpeechItem extends SynthesisSpeechItem {
515        private final File mFile;
516
517        public SynthesisToFileSpeechItem(String callingApp, Bundle params, String text,
518                File file) {
519            super(callingApp, params, text);
520            mFile = file;
521        }
522
523        @Override
524        public boolean isValid() {
525            if (!super.isValid()) {
526                return false;
527            }
528            return checkFile(mFile);
529        }
530
531        @Override
532        protected SynthesisRequest createSynthesisRequest() {
533            return new FileSynthesisRequest(getText(), mParams, mFile);
534        }
535
536        @Override
537        protected int playImpl() {
538            int status = super.playImpl();
539            if (status == TextToSpeech.SUCCESS) {
540                dispatchUtteranceCompleted();
541            }
542            return status;
543        }
544
545        /**
546         * Checks that the given file can be used for synthesis output.
547         */
548        private boolean checkFile(File file) {
549            try {
550                if (file.exists()) {
551                    Log.v(TAG, "File " + file + " exists, deleting.");
552                    if (!file.delete()) {
553                        Log.e(TAG, "Failed to delete " + file);
554                        return false;
555                    }
556                }
557                if (!file.createNewFile()) {
558                    Log.e(TAG, "Can't create file " + file);
559                    return false;
560                }
561                if (!file.delete()) {
562                    Log.e(TAG, "Failed to delete " + file);
563                    return false;
564                }
565                return true;
566            } catch (IOException e) {
567                Log.e(TAG, "Can't use " + file + " due to exception " + e);
568                return false;
569            }
570        }
571    }
572
573    private class AudioSpeechItem extends SpeechItem {
574
575        private final BlockingMediaPlayer mPlayer;
576        private AudioMessageParams mToken;
577
578        public AudioSpeechItem(String callingApp, Bundle params, Uri uri) {
579            super(callingApp, params);
580            mPlayer = new BlockingMediaPlayer(TextToSpeechService.this, uri, getStreamType());
581        }
582
583        @Override
584        public boolean isValid() {
585            return true;
586        }
587
588        @Override
589        protected int playImpl() {
590            mToken = new AudioMessageParams(this, mPlayer);
591            mAudioPlaybackHandler.enqueueAudio(mToken);
592            return TextToSpeech.SUCCESS;
593        }
594
595        @Override
596        protected void stopImpl() {
597            if (mToken != null) {
598                mAudioPlaybackHandler.stop(mToken);
599            }
600        }
601    }
602
603    private class SilenceSpeechItem extends SpeechItem {
604        private final long mDuration;
605        private SilenceMessageParams mToken;
606
607        public SilenceSpeechItem(String callingApp, Bundle params, long duration) {
608            super(callingApp, params);
609            mDuration = duration;
610        }
611
612        @Override
613        public boolean isValid() {
614            return true;
615        }
616
617        @Override
618        protected int playImpl() {
619            mToken = new SilenceMessageParams(this, mDuration);
620            mAudioPlaybackHandler.enqueueSilence(mToken);
621            return TextToSpeech.SUCCESS;
622        }
623
624        @Override
625        protected void stopImpl() {
626            if (mToken != null) {
627                mAudioPlaybackHandler.stop(mToken);
628            }
629        }
630    }
631
632    @Override
633    public IBinder onBind(Intent intent) {
634        if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
635            return mBinder;
636        }
637        return null;
638    }
639
640    /**
641     * Binder returned from {@code #onBind(Intent)}. The methods in this class can be
642     * called called from several different threads.
643     */
644    private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() {
645
646        public int speak(String callingApp, String text, int queueMode, Bundle params) {
647            SpeechItem item = new SynthesisSpeechItem(callingApp, params, text);
648            return mSynthHandler.enqueueSpeechItem(queueMode, item);
649        }
650
651        public int synthesizeToFile(String callingApp, String text, String filename,
652                Bundle params) {
653            File file = new File(filename);
654            SpeechItem item = new SynthesisToFileSpeechItem(callingApp, params, text, file);
655            return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
656        }
657
658        public int playAudio(String callingApp, Uri audioUri, int queueMode, Bundle params) {
659            SpeechItem item = new AudioSpeechItem(callingApp, params, audioUri);
660            return mSynthHandler.enqueueSpeechItem(queueMode, item);
661        }
662
663        public int playSilence(String callingApp, long duration, int queueMode, Bundle params) {
664            SpeechItem item = new SilenceSpeechItem(callingApp, params, duration);
665            return mSynthHandler.enqueueSpeechItem(queueMode, item);
666        }
667
668        public boolean isSpeaking() {
669            return mSynthHandler.isSpeaking();
670        }
671
672        public int stop(String callingApp) {
673            return mSynthHandler.stop(callingApp);
674        }
675
676        public String[] getLanguage() {
677            return onGetLanguage();
678        }
679
680        /*
681         * If defaults are enforced, then no language is "available" except
682         * perhaps the default language selected by the user.
683         */
684        public int isLanguageAvailable(String lang, String country, String variant) {
685            if (areDefaultsEnforced()) {
686                if (isDefault(lang, country, variant)) {
687                    return mDefaultAvailability;
688                } else {
689                    return TextToSpeech.LANG_NOT_SUPPORTED;
690                }
691            }
692            return onIsLanguageAvailable(lang, country, variant);
693        }
694
695        /*
696         * There is no point loading a non default language if defaults
697         * are enforced.
698         */
699        public int loadLanguage(String lang, String country, String variant) {
700            if (areDefaultsEnforced()) {
701                if (isDefault(lang, country, variant)) {
702                    return mDefaultAvailability;
703                } else {
704                    return TextToSpeech.LANG_NOT_SUPPORTED;
705                }
706            }
707            return onLoadLanguage(lang, country, variant);
708        }
709
710        public void setCallback(String packageName, ITextToSpeechCallback cb) {
711            mCallbacks.setCallback(packageName, cb);
712        }
713
714        private boolean isDefault(String lang, String country, String variant) {
715            return Locale.getDefault().equals(new Locale(lang, country, variant));
716        }
717    };
718
719    private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
720
721        private final HashMap<String, ITextToSpeechCallback> mAppToCallback
722                = new HashMap<String, ITextToSpeechCallback>();
723
724        public void setCallback(String packageName, ITextToSpeechCallback cb) {
725            synchronized (mAppToCallback) {
726                ITextToSpeechCallback old;
727                if (cb != null) {
728                    register(cb, packageName);
729                    old = mAppToCallback.put(packageName, cb);
730                } else {
731                    old = mAppToCallback.remove(packageName);
732                }
733                if (old != null && old != cb) {
734                    unregister(old);
735                }
736            }
737        }
738
739        public void dispatchUtteranceCompleted(String packageName, String utteranceId) {
740            ITextToSpeechCallback cb;
741            synchronized (mAppToCallback) {
742                cb = mAppToCallback.get(packageName);
743            }
744            if (cb == null) return;
745            try {
746                cb.utteranceCompleted(utteranceId);
747            } catch (RemoteException e) {
748                Log.e(TAG, "Callback failed: " + e);
749            }
750        }
751
752        @Override
753        public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
754            String packageName = (String) cookie;
755            synchronized (mAppToCallback) {
756                mAppToCallback.remove(packageName);
757            }
758            mSynthHandler.stop(packageName);
759        }
760
761        @Override
762        public void kill() {
763            synchronized (mAppToCallback) {
764                mAppToCallback.clear();
765                super.kill();
766            }
767        }
768
769    }
770
771}
772