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