TextToSpeechService.java revision 453c13f77121641a52d8b422a527feb7b3d3b8ab
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.media.AudioAttributes;
21import android.media.AudioSystem;
22import android.net.Uri;
23import android.os.Binder;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.HandlerThread;
27import android.os.IBinder;
28import android.os.Looper;
29import android.os.Message;
30import android.os.MessageQueue;
31import android.os.ParcelFileDescriptor;
32import android.os.RemoteCallbackList;
33import android.os.RemoteException;
34import android.provider.Settings;
35import android.speech.tts.TextToSpeech.Engine;
36import android.text.TextUtils;
37import android.util.Log;
38
39import java.io.FileOutputStream;
40import java.io.IOException;
41import java.util.ArrayList;
42import java.util.HashMap;
43import java.util.HashSet;
44import java.util.List;
45import java.util.Locale;
46import java.util.MissingResourceException;
47import java.util.Set;
48
49
50/**
51 * Abstract base class for TTS engine implementations. The following methods
52 * need to be implemented:
53 * <ul>
54 * <li>{@link #onIsLanguageAvailable}</li>
55 * <li>{@link #onLoadLanguage}</li>
56 * <li>{@link #onGetLanguage}</li>
57 * <li>{@link #onSynthesizeText}</li>
58 * <li>{@link #onStop}</li>
59 * </ul>
60 * The first three deal primarily with language management, and are used to
61 * query the engine for it's support for a given language and indicate to it
62 * that requests in a given language are imminent.
63 *
64 * {@link #onSynthesizeText} is central to the engine implementation. The
65 * implementation should synthesize text as per the request parameters and
66 * return synthesized data via the supplied callback. This class and its helpers
67 * will then consume that data, which might mean queuing it for playback or writing
68 * it to a file or similar. All calls to this method will be on a single thread,
69 * which will be different from the main thread of the service. Synthesis must be
70 * synchronous which means the engine must NOT hold on to the callback or call any
71 * methods on it after the method returns.
72 *
73 * {@link #onStop} tells the engine that it should stop
74 * all ongoing synthesis, if any. Any pending data from the current synthesis
75 * will be discarded.
76 *
77 * {@link #onGetLanguage} is not required as of JELLYBEAN_MR2 (API 18) and later, it is only
78 * called on earlier versions of Android.
79 *
80 * API Level 20 adds support for Voice objects. Voices are an abstraction that allow the TTS
81 * service to expose multiple backends for a single locale. Each one of them can have a different
82 * features set. In order to fully take advantage of voices, an engine should implement
83 * the following methods:
84 * <ul>
85 * <li>{@link #onGetVoices()}</li>
86 * <li>{@link #onIsValidVoiceName(String)}</li>
87 * <li>{@link #onLoadVoice(String)}</li>
88 * <li>{@link #onGetDefaultVoiceNameFor(String, String, String)}</li>
89 * </ul>
90 * The first three methods are siblings of the {@link #onGetLanguage},
91 * {@link #onIsLanguageAvailable} and {@link #onLoadLanguage} methods. The last one,
92 * {@link #onGetDefaultVoiceNameFor(String, String, String)} is a link between locale and voice
93 * based methods. Since API level 21 {@link TextToSpeech#setLanguage} is implemented by
94 * calling {@link TextToSpeech#setVoice} with the voice returned by
95 * {@link #onGetDefaultVoiceNameFor(String, String, String)}.
96 *
97 * If the client uses a voice instead of a locale, {@link SynthesisRequest} will contain the
98 * requested voice name.
99 *
100 * The default implementations of Voice-related methods implement them using the
101 * pre-existing locale-based implementation.
102 */
103public abstract class TextToSpeechService extends Service {
104
105    private static final boolean DBG = false;
106    private static final String TAG = "TextToSpeechService";
107
108    private static final String SYNTH_THREAD_NAME = "SynthThread";
109
110    private SynthHandler mSynthHandler;
111    // A thread and it's associated handler for playing back any audio
112    // associated with this TTS engine. Will handle all requests except synthesis
113    // to file requests, which occur on the synthesis thread.
114    private AudioPlaybackHandler mAudioPlaybackHandler;
115    private TtsEngines mEngineHelper;
116
117    private CallbackMap mCallbacks;
118    private String mPackageName;
119
120    private final Object mVoicesInfoLock = new Object();
121
122    @Override
123    public void onCreate() {
124        if (DBG) Log.d(TAG, "onCreate()");
125        super.onCreate();
126
127        SynthThread synthThread = new SynthThread();
128        synthThread.start();
129        mSynthHandler = new SynthHandler(synthThread.getLooper());
130
131        mAudioPlaybackHandler = new AudioPlaybackHandler();
132        mAudioPlaybackHandler.start();
133
134        mEngineHelper = new TtsEngines(this);
135
136        mCallbacks = new CallbackMap();
137
138        mPackageName = getApplicationInfo().packageName;
139
140        String[] defaultLocale = getSettingsLocale();
141
142        // Load default language
143        onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]);
144    }
145
146    @Override
147    public void onDestroy() {
148        if (DBG) Log.d(TAG, "onDestroy()");
149
150        // Tell the synthesizer to stop
151        mSynthHandler.quit();
152        // Tell the audio playback thread to stop.
153        mAudioPlaybackHandler.quit();
154        // Unregister all callbacks.
155        mCallbacks.kill();
156
157        super.onDestroy();
158    }
159
160    /**
161     * Checks whether the engine supports a given language.
162     *
163     * Can be called on multiple threads.
164     *
165     * Its return values HAVE to be consistent with onLoadLanguage.
166     *
167     * @param lang ISO-3 language code.
168     * @param country ISO-3 country code. May be empty or null.
169     * @param variant Language variant. May be empty or null.
170     * @return Code indicating the support status for the locale.
171     *         One of {@link TextToSpeech#LANG_AVAILABLE},
172     *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
173     *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
174     *         {@link TextToSpeech#LANG_MISSING_DATA}
175     *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
176     */
177    protected abstract int onIsLanguageAvailable(String lang, String country, String variant);
178
179    /**
180     * Returns the language, country and variant currently being used by the TTS engine.
181     *
182     * This method will be called only on Android 4.2 and before (API <= 17). In later versions
183     * this method is not called by the Android TTS framework.
184     *
185     * Can be called on multiple threads.
186     *
187     * @return A 3-element array, containing language (ISO 3-letter code),
188     *         country (ISO 3-letter code) and variant used by the engine.
189     *         The country and variant may be {@code ""}. If country is empty, then variant must
190     *         be empty too.
191     * @see Locale#getISO3Language()
192     * @see Locale#getISO3Country()
193     * @see Locale#getVariant()
194     */
195    protected abstract String[] onGetLanguage();
196
197    /**
198     * Notifies the engine that it should load a speech synthesis language. There is no guarantee
199     * that this method is always called before the language is used for synthesis. It is merely
200     * a hint to the engine that it will probably get some synthesis requests for this language
201     * at some point in the future.
202     *
203     * Can be called on multiple threads.
204     * In <= Android 4.2 (<= API 17) can be called on main and service binder threads.
205     * In > Android 4.2 (> API 17) can be called on main and synthesis threads.
206     *
207     * @param lang ISO-3 language code.
208     * @param country ISO-3 country code. May be empty or null.
209     * @param variant Language variant. May be empty or null.
210     * @return Code indicating the support status for the locale.
211     *         One of {@link TextToSpeech#LANG_AVAILABLE},
212     *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
213     *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
214     *         {@link TextToSpeech#LANG_MISSING_DATA}
215     *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
216     */
217    protected abstract int onLoadLanguage(String lang, String country, String variant);
218
219    /**
220     * Notifies the service that it should stop any in-progress speech synthesis.
221     * This method can be called even if no speech synthesis is currently in progress.
222     *
223     * Can be called on multiple threads, but not on the synthesis thread.
224     */
225    protected abstract void onStop();
226
227    /**
228     * Tells the service to synthesize speech from the given text. This method
229     * should block until the synthesis is finished. Used for requests from V1
230     * clients ({@link android.speech.tts.TextToSpeech}). Called on the synthesis
231     * thread.
232     *
233     * @param request The synthesis request.
234     * @param callback The callback that the engine must use to make data
235     *            available for playback or for writing to a file.
236     */
237    protected abstract void onSynthesizeText(SynthesisRequest request,
238            SynthesisCallback callback);
239
240    /**
241     * Queries the service for a set of features supported for a given language.
242     *
243     * Can be called on multiple threads.
244     *
245     * @param lang ISO-3 language code.
246     * @param country ISO-3 country code. May be empty or null.
247     * @param variant Language variant. May be empty or null.
248     * @return A list of features supported for the given language.
249     */
250    protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) {
251        return new HashSet<String>();
252    }
253
254    private int getExpectedLanguageAvailableStatus(Locale locale) {
255        int expectedStatus = TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE;
256        if (locale.getVariant().isEmpty()) {
257            if (locale.getCountry().isEmpty()) {
258                expectedStatus = TextToSpeech.LANG_AVAILABLE;
259            } else {
260                expectedStatus = TextToSpeech.LANG_COUNTRY_AVAILABLE;
261            }
262        }
263        return expectedStatus;
264    }
265
266    /**
267     * Queries the service for a set of supported voices.
268     *
269     * Can be called on multiple threads.
270     *
271     * The default implementation tries to enumerate all available locales, pass them to
272     * {@link #onIsLanguageAvailable(String, String, String)} and create Voice instances (using
273     * the locale's BCP-47 language tag as the voice name) for the ones that are supported.
274     * Note, that this implementation is suitable only for engines that don't have multiple voices
275     * for a single locale. Also, this implementation won't work with Locales not listed in the
276     * set returned by the {@link Locale#getAvailableLocales()} method.
277     *
278     * @return A list of voices supported.
279     */
280    public List<Voice> onGetVoices() {
281        // Enumerate all locales and check if they are available
282        ArrayList<Voice> voices = new ArrayList<Voice>();
283        for (Locale locale : Locale.getAvailableLocales()) {
284            int expectedStatus = getExpectedLanguageAvailableStatus(locale);
285            try {
286                int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
287                        locale.getISO3Country(), locale.getVariant());
288                if (localeStatus != expectedStatus) {
289                    continue;
290                }
291            } catch (MissingResourceException e) {
292                // Ignore locale without iso 3 codes
293                continue;
294            }
295            Set<String> features = onGetFeaturesForLanguage(locale.getISO3Language(),
296                    locale.getISO3Country(), locale.getVariant());
297            String voiceName = onGetDefaultVoiceNameFor(locale.getISO3Language(),
298                    locale.getISO3Country(), locale.getVariant());
299            voices.add(new Voice(voiceName, locale, Voice.QUALITY_NORMAL,
300                    Voice.LATENCY_NORMAL, false, features));
301        }
302        return voices;
303    }
304
305    /**
306     * Return a name of the default voice for a given locale.
307     *
308     * This method provides a mapping between locales and available voices. This method is
309     * used in {@link TextToSpeech#setLanguage}, which calls this method and then calls
310     * {@link TextToSpeech#setVoice} with the voice returned by this method.
311     *
312     * Also, it's used by {@link TextToSpeech#getDefaultVoice()} to find a default voice for
313     * the default locale.
314     *
315     * @param lang ISO-3 language code.
316     * @param country ISO-3 country code. May be empty or null.
317     * @param variant Language variant. May be empty or null.
318
319     * @return A name of the default voice for a given locale.
320     */
321    public String onGetDefaultVoiceNameFor(String lang, String country, String variant) {
322        int localeStatus = onIsLanguageAvailable(lang, country, variant);
323        Locale iso3Locale = null;
324        switch (localeStatus) {
325            case TextToSpeech.LANG_AVAILABLE:
326                iso3Locale = new Locale(lang);
327                break;
328            case TextToSpeech.LANG_COUNTRY_AVAILABLE:
329                iso3Locale = new Locale(lang, country);
330                break;
331            case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE:
332                iso3Locale = new Locale(lang, country, variant);
333                break;
334            default:
335                return null;
336        }
337        Locale properLocale = TtsEngines.normalizeTTSLocale(iso3Locale);
338        String voiceName = properLocale.toLanguageTag();
339        if (onIsValidVoiceName(voiceName) == TextToSpeech.SUCCESS) {
340            return voiceName;
341        } else {
342            return null;
343        }
344    }
345
346    /**
347     * Notifies the engine that it should load a speech synthesis voice. There is no guarantee
348     * that this method is always called before the voice is used for synthesis. It is merely
349     * a hint to the engine that it will probably get some synthesis requests for this voice
350     * at some point in the future.
351     *
352     * Will be called only on synthesis thread.
353     *
354     * The default implementation creates a Locale from the voice name (by interpreting the name as
355     * a BCP-47 tag for the locale), and passes it to
356     * {@link #onLoadLanguage(String, String, String)}.
357     *
358     * @param voiceName Name of the voice.
359     * @return {@link TextToSpeech#ERROR} or {@link TextToSpeech#SUCCESS}.
360     */
361    public int onLoadVoice(String voiceName) {
362        Locale locale = Locale.forLanguageTag(voiceName);
363        if (locale == null) {
364            return TextToSpeech.ERROR;
365        }
366        int expectedStatus = getExpectedLanguageAvailableStatus(locale);
367        try {
368            int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
369                    locale.getISO3Country(), locale.getVariant());
370            if (localeStatus != expectedStatus) {
371                return TextToSpeech.ERROR;
372            }
373            onLoadLanguage(locale.getISO3Language(),
374                    locale.getISO3Country(), locale.getVariant());
375            return TextToSpeech.SUCCESS;
376        } catch (MissingResourceException e) {
377            return TextToSpeech.ERROR;
378        }
379    }
380
381    /**
382     * Checks whether the engine supports a voice with a given name.
383     *
384     * Can be called on multiple threads.
385     *
386     * The default implementation treats the voice name as a language tag, creating a Locale from
387     * the voice name, and passes it to {@link #onIsLanguageAvailable(String, String, String)}.
388     *
389     * @param voiceName Name of the voice.
390     * @return {@link TextToSpeech#ERROR} or {@link TextToSpeech#SUCCESS}.
391     */
392    public int onIsValidVoiceName(String voiceName) {
393        Locale locale = Locale.forLanguageTag(voiceName);
394        if (locale == null) {
395            return TextToSpeech.ERROR;
396        }
397        int expectedStatus = getExpectedLanguageAvailableStatus(locale);
398        try {
399            int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
400                    locale.getISO3Country(), locale.getVariant());
401            if (localeStatus != expectedStatus) {
402                return TextToSpeech.ERROR;
403            }
404            return TextToSpeech.SUCCESS;
405        } catch (MissingResourceException e) {
406            return TextToSpeech.ERROR;
407        }
408    }
409
410    private int getDefaultSpeechRate() {
411        return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE);
412    }
413
414    private String[] getSettingsLocale() {
415        final Locale locale = mEngineHelper.getLocalePrefForEngine(mPackageName);
416        return TtsEngines.toOldLocaleStringFormat(locale);
417    }
418
419    private int getSecureSettingInt(String name, int defaultValue) {
420        return Settings.Secure.getInt(getContentResolver(), name, defaultValue);
421    }
422
423    /**
424     * Synthesizer thread. This thread is used to run {@link SynthHandler}.
425     */
426    private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler {
427
428        private boolean mFirstIdle = true;
429
430        public SynthThread() {
431            super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_DEFAULT);
432        }
433
434        @Override
435        protected void onLooperPrepared() {
436            getLooper().getQueue().addIdleHandler(this);
437        }
438
439        @Override
440        public boolean queueIdle() {
441            if (mFirstIdle) {
442                mFirstIdle = false;
443            } else {
444                broadcastTtsQueueProcessingCompleted();
445            }
446            return true;
447        }
448
449        private void broadcastTtsQueueProcessingCompleted() {
450            Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED);
451            if (DBG) Log.d(TAG, "Broadcasting: " + i);
452            sendBroadcast(i);
453        }
454    }
455
456    private class SynthHandler extends Handler {
457        private SpeechItem mCurrentSpeechItem = null;
458
459        private ArrayList<Object> mFlushedObjects = new ArrayList<Object>();
460        private boolean mFlushAll;
461
462        public SynthHandler(Looper looper) {
463            super(looper);
464        }
465
466        private void startFlushingSpeechItems(Object callerIdentity) {
467            synchronized (mFlushedObjects) {
468                if (callerIdentity == null) {
469                    mFlushAll = true;
470                } else {
471                    mFlushedObjects.add(callerIdentity);
472                }
473            }
474        }
475        private void endFlushingSpeechItems(Object callerIdentity) {
476            synchronized (mFlushedObjects) {
477                if (callerIdentity == null) {
478                    mFlushAll = false;
479                } else {
480                    mFlushedObjects.remove(callerIdentity);
481                }
482            }
483        }
484        private boolean isFlushed(SpeechItem speechItem) {
485            synchronized (mFlushedObjects) {
486                return mFlushAll || mFlushedObjects.contains(speechItem.getCallerIdentity());
487            }
488        }
489
490        private synchronized SpeechItem getCurrentSpeechItem() {
491            return mCurrentSpeechItem;
492        }
493
494        private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) {
495            SpeechItem old = mCurrentSpeechItem;
496            mCurrentSpeechItem = speechItem;
497            return old;
498        }
499
500        private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) {
501            if (mCurrentSpeechItem != null &&
502                    (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) {
503                SpeechItem current = mCurrentSpeechItem;
504                mCurrentSpeechItem = null;
505                return current;
506            }
507
508            return null;
509        }
510
511        public boolean isSpeaking() {
512            return getCurrentSpeechItem() != null;
513        }
514
515        public void quit() {
516            // Don't process any more speech items
517            getLooper().quit();
518            // Stop the current speech item
519            SpeechItem current = setCurrentSpeechItem(null);
520            if (current != null) {
521                current.stop();
522            }
523            // The AudioPlaybackHandler will be destroyed by the caller.
524        }
525
526        /**
527         * Adds a speech item to the queue.
528         *
529         * Called on a service binder thread.
530         */
531        public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
532            UtteranceProgressDispatcher utterenceProgress = null;
533            if (speechItem instanceof UtteranceProgressDispatcher) {
534                utterenceProgress = (UtteranceProgressDispatcher) speechItem;
535            }
536
537            if (!speechItem.isValid()) {
538                if (utterenceProgress != null) {
539                    utterenceProgress.dispatchOnError(
540                            TextToSpeech.ERROR_INVALID_REQUEST);
541                }
542                return TextToSpeech.ERROR;
543            }
544
545            if (queueMode == TextToSpeech.QUEUE_FLUSH) {
546                stopForApp(speechItem.getCallerIdentity());
547            } else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
548                stopAll();
549            }
550            Runnable runnable = new Runnable() {
551                @Override
552                public void run() {
553                    if (isFlushed(speechItem)) {
554                        speechItem.stop();
555                    } else {
556                        setCurrentSpeechItem(speechItem);
557                        speechItem.play();
558                        setCurrentSpeechItem(null);
559                    }
560                }
561            };
562            Message msg = Message.obtain(this, runnable);
563
564            // The obj is used to remove all callbacks from the given app in
565            // stopForApp(String).
566            //
567            // Note that this string is interned, so the == comparison works.
568            msg.obj = speechItem.getCallerIdentity();
569
570            if (sendMessage(msg)) {
571                return TextToSpeech.SUCCESS;
572            } else {
573                Log.w(TAG, "SynthThread has quit");
574                if (utterenceProgress != null) {
575                    utterenceProgress.dispatchOnError(TextToSpeech.ERROR_SERVICE);
576                }
577                return TextToSpeech.ERROR;
578            }
579        }
580
581        /**
582         * Stops all speech output and removes any utterances still in the queue for
583         * the calling app.
584         *
585         * Called on a service binder thread.
586         */
587        public int stopForApp(final Object callerIdentity) {
588            if (callerIdentity == null) {
589                return TextToSpeech.ERROR;
590            }
591
592            // Flush pending messages from callerIdentity
593            startFlushingSpeechItems(callerIdentity);
594
595            // This stops writing data to the file / or publishing
596            // items to the audio playback handler.
597            //
598            // Note that the current speech item must be removed only if it
599            // belongs to the callingApp, else the item will be "orphaned" and
600            // not stopped correctly if a stop request comes along for the item
601            // from the app it belongs to.
602            SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity);
603            if (current != null) {
604                current.stop();
605            }
606
607            // Remove any enqueued audio too.
608            mAudioPlaybackHandler.stopForApp(callerIdentity);
609
610            // Stop flushing pending messages
611            Runnable runnable = new Runnable() {
612                @Override
613                public void run() {
614                    endFlushingSpeechItems(callerIdentity);
615                }
616            };
617            sendMessage(Message.obtain(this, runnable));
618            return TextToSpeech.SUCCESS;
619        }
620
621        public int stopAll() {
622            // Order to flush pending messages
623            startFlushingSpeechItems(null);
624
625            // Stop the current speech item unconditionally .
626            SpeechItem current = setCurrentSpeechItem(null);
627            if (current != null) {
628                current.stop();
629            }
630            // Remove all pending playback as well.
631            mAudioPlaybackHandler.stop();
632
633            // Message to stop flushing pending messages
634            Runnable runnable = new Runnable() {
635                @Override
636                public void run() {
637                    endFlushingSpeechItems(null);
638                }
639            };
640            sendMessage(Message.obtain(this, runnable));
641
642
643            return TextToSpeech.SUCCESS;
644        }
645    }
646
647    interface UtteranceProgressDispatcher {
648        public void dispatchOnStop();
649        public void dispatchOnSuccess();
650        public void dispatchOnStart();
651        public void dispatchOnError(int errorCode);
652    }
653
654    /** Set of parameters affecting audio output. */
655    static class AudioOutputParams {
656        /**
657         * Audio session identifier. May be used to associate audio playback with one of the
658         * {@link android.media.audiofx.AudioEffect} objects. If not specified by client,
659         * it should be equal to {@link AudioSystem#AUDIO_SESSION_ALLOCATE}.
660         */
661        public final int mSessionId;
662
663        /**
664         * Volume, in the range [0.0f, 1.0f]. The default value is
665         * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f).
666         */
667        public final float mVolume;
668
669        /**
670         * Left/right position of the audio, in the range [-1.0f, 1.0f].
671         * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f).
672         */
673        public final float mPan;
674
675
676        /**
677         * Audio attributes, set by {@link TextToSpeech#setAudioAttributes}
678         * or created from the value of {@link TextToSpeech.Engine#KEY_PARAM_STREAM}.
679         */
680        public final AudioAttributes mAudioAttributes;
681
682        /** Create AudioOutputParams with default values */
683        AudioOutputParams() {
684            mSessionId = AudioSystem.AUDIO_SESSION_ALLOCATE;
685            mVolume = Engine.DEFAULT_VOLUME;
686            mPan = Engine.DEFAULT_PAN;
687            mAudioAttributes = null;
688        }
689
690        AudioOutputParams(int sessionId, float volume, float pan,
691                AudioAttributes audioAttributes) {
692            mSessionId = sessionId;
693            mVolume = volume;
694            mPan = pan;
695            mAudioAttributes = audioAttributes;
696        }
697
698        /** Create AudioOutputParams from A {@link SynthesisRequest#getParams()} bundle */
699        static AudioOutputParams createFromV1ParamsBundle(Bundle paramsBundle,
700                boolean isSpeech) {
701            if (paramsBundle == null) {
702                return new AudioOutputParams();
703            }
704
705            AudioAttributes audioAttributes =
706                    (AudioAttributes) paramsBundle.getParcelable(
707                            Engine.KEY_PARAM_AUDIO_ATTRIBUTES);
708            if (audioAttributes == null) {
709                int streamType = paramsBundle.getInt(
710                        Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM);
711                audioAttributes = (new AudioAttributes.Builder())
712                        .setLegacyStreamType(streamType)
713                        .setContentType((isSpeech ?
714                                AudioAttributes.CONTENT_TYPE_SPEECH :
715                                AudioAttributes.CONTENT_TYPE_SONIFICATION))
716                        .build();
717            }
718
719            return new AudioOutputParams(
720                    paramsBundle.getInt(
721                            Engine.KEY_PARAM_SESSION_ID,
722                            AudioSystem.AUDIO_SESSION_ALLOCATE),
723                    paramsBundle.getFloat(
724                            Engine.KEY_PARAM_VOLUME,
725                            Engine.DEFAULT_VOLUME),
726                    paramsBundle.getFloat(
727                            Engine.KEY_PARAM_PAN,
728                            Engine.DEFAULT_PAN),
729                    audioAttributes);
730        }
731    }
732
733
734    /**
735     * An item in the synth thread queue.
736     */
737    private abstract class SpeechItem {
738        private final Object mCallerIdentity;
739        private final int mCallerUid;
740        private final int mCallerPid;
741        private boolean mStarted = false;
742        private boolean mStopped = false;
743
744        public SpeechItem(Object caller, int callerUid, int callerPid) {
745            mCallerIdentity = caller;
746            mCallerUid = callerUid;
747            mCallerPid = callerPid;
748        }
749
750        public Object getCallerIdentity() {
751            return mCallerIdentity;
752        }
753
754        public int getCallerUid() {
755            return mCallerUid;
756        }
757
758        public int getCallerPid() {
759            return mCallerPid;
760        }
761
762        /**
763         * Checker whether the item is valid. If this method returns false, the item should not
764         * be played.
765         */
766        public abstract boolean isValid();
767
768        /**
769         * Plays the speech item. Blocks until playback is finished.
770         * Must not be called more than once.
771         *
772         * Only called on the synthesis thread.
773         */
774        public void play() {
775            synchronized (this) {
776                if (mStarted) {
777                    throw new IllegalStateException("play() called twice");
778                }
779                mStarted = true;
780            }
781            playImpl();
782        }
783
784        protected abstract void playImpl();
785
786        /**
787         * Stops the speech item.
788         * Must not be called more than once.
789         *
790         * Can be called on multiple threads,  but not on the synthesis thread.
791         */
792        public void stop() {
793            synchronized (this) {
794                if (mStopped) {
795                    throw new IllegalStateException("stop() called twice");
796                }
797                mStopped = true;
798            }
799            stopImpl();
800        }
801
802        protected abstract void stopImpl();
803
804        protected synchronized boolean isStopped() {
805             return mStopped;
806        }
807
808        protected synchronized boolean isStarted() {
809            return mStarted;
810       }
811    }
812
813    /**
814     * An item in the synth thread queue that process utterance (and call back to client about
815     * progress).
816     */
817    private abstract class UtteranceSpeechItem extends SpeechItem
818        implements UtteranceProgressDispatcher  {
819
820        public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) {
821            super(caller, callerUid, callerPid);
822        }
823
824        @Override
825        public void dispatchOnSuccess() {
826            final String utteranceId = getUtteranceId();
827            if (utteranceId != null) {
828                mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId);
829            }
830        }
831
832        @Override
833        public void dispatchOnStop() {
834            final String utteranceId = getUtteranceId();
835            if (utteranceId != null) {
836                mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted());
837            }
838        }
839
840        @Override
841        public void dispatchOnStart() {
842            final String utteranceId = getUtteranceId();
843            if (utteranceId != null) {
844                mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId);
845            }
846        }
847
848        @Override
849        public void dispatchOnError(int errorCode) {
850            final String utteranceId = getUtteranceId();
851            if (utteranceId != null) {
852                mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode);
853            }
854        }
855
856        abstract public String getUtteranceId();
857
858        String getStringParam(Bundle params, String key, String defaultValue) {
859            return params == null ? defaultValue : params.getString(key, defaultValue);
860        }
861
862        int getIntParam(Bundle params, String key, int defaultValue) {
863            return params == null ? defaultValue : params.getInt(key, defaultValue);
864        }
865
866        float getFloatParam(Bundle params, String key, float defaultValue) {
867            return params == null ? defaultValue : params.getFloat(key, defaultValue);
868        }
869    }
870
871    /**
872     * UtteranceSpeechItem for V1 API speech items. V1 API speech items keep
873     * synthesis parameters in a single Bundle passed as parameter. This class
874     * allow subclasses to access them conveniently.
875     */
876    private abstract class SpeechItemV1 extends UtteranceSpeechItem {
877        protected final Bundle mParams;
878        protected final String mUtteranceId;
879
880        SpeechItemV1(Object callerIdentity, int callerUid, int callerPid,
881                Bundle params, String utteranceId) {
882            super(callerIdentity, callerUid, callerPid);
883            mParams = params;
884            mUtteranceId = utteranceId;
885        }
886
887        boolean hasLanguage() {
888            return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null));
889        }
890
891        int getSpeechRate() {
892            return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate());
893        }
894
895        int getPitch() {
896            return getIntParam(mParams, Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH);
897        }
898
899        @Override
900        public String getUtteranceId() {
901            return mUtteranceId;
902        }
903
904        AudioOutputParams getAudioParams() {
905            return AudioOutputParams.createFromV1ParamsBundle(mParams, true);
906        }
907    }
908
909    class SynthesisSpeechItemV1 extends SpeechItemV1 {
910        // Never null.
911        private final CharSequence mText;
912        private final SynthesisRequest mSynthesisRequest;
913        private final String[] mDefaultLocale;
914        // Non null after synthesis has started, and all accesses
915        // guarded by 'this'.
916        private AbstractSynthesisCallback mSynthesisCallback;
917        private final EventLoggerV1 mEventLogger;
918        private final int mCallerUid;
919
920        public SynthesisSpeechItemV1(Object callerIdentity, int callerUid, int callerPid,
921                Bundle params, String utteranceId, CharSequence text) {
922            super(callerIdentity, callerUid, callerPid, params, utteranceId);
923            mText = text;
924            mCallerUid = callerUid;
925            mSynthesisRequest = new SynthesisRequest(mText, mParams);
926            mDefaultLocale = getSettingsLocale();
927            setRequestParams(mSynthesisRequest);
928            mEventLogger = new EventLoggerV1(mSynthesisRequest, callerUid, callerPid,
929                    mPackageName);
930        }
931
932        public CharSequence getText() {
933            return mText;
934        }
935
936        @Override
937        public boolean isValid() {
938            if (mText == null) {
939                Log.e(TAG, "null synthesis text");
940                return false;
941            }
942            if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) {
943                Log.w(TAG, "Text too long: " + mText.length() + " chars");
944                return false;
945            }
946            return true;
947        }
948
949        @Override
950        protected void playImpl() {
951            AbstractSynthesisCallback synthesisCallback;
952            mEventLogger.onRequestProcessingStart();
953            synchronized (this) {
954                // stop() might have been called before we enter this
955                // synchronized block.
956                if (isStopped()) {
957                    return;
958                }
959                mSynthesisCallback = createSynthesisCallback();
960                synthesisCallback = mSynthesisCallback;
961            }
962
963            TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
964
965            // Fix for case where client called .start() & .error(), but did not called .done()
966            if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) {
967                synthesisCallback.done();
968            }
969        }
970
971        protected AbstractSynthesisCallback createSynthesisCallback() {
972            return new PlaybackSynthesisCallback(getAudioParams(),
973                    mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false);
974        }
975
976        private void setRequestParams(SynthesisRequest request) {
977            String voiceName = getVoiceName();
978            request.setLanguage(getLanguage(), getCountry(), getVariant());
979            if (!TextUtils.isEmpty(voiceName)) {
980                request.setVoiceName(getVoiceName());
981            }
982            request.setSpeechRate(getSpeechRate());
983            request.setCallerUid(mCallerUid);
984            request.setPitch(getPitch());
985        }
986
987        @Override
988        protected void stopImpl() {
989            AbstractSynthesisCallback synthesisCallback;
990            synchronized (this) {
991                synthesisCallback = mSynthesisCallback;
992            }
993            if (synthesisCallback != null) {
994                // If the synthesis callback is null, it implies that we haven't
995                // entered the synchronized(this) block in playImpl which in
996                // turn implies that synthesis would not have started.
997                synthesisCallback.stop();
998                TextToSpeechService.this.onStop();
999            } else {
1000                dispatchOnStop();
1001            }
1002        }
1003
1004        private String getCountry() {
1005            if (!hasLanguage()) return mDefaultLocale[1];
1006            return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, "");
1007        }
1008
1009        private String getVariant() {
1010            if (!hasLanguage()) return mDefaultLocale[2];
1011            return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, "");
1012        }
1013
1014        public String getLanguage() {
1015            return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]);
1016        }
1017
1018        public String getVoiceName() {
1019            return getStringParam(mParams, Engine.KEY_PARAM_VOICE_NAME, "");
1020        }
1021    }
1022
1023    private class SynthesisToFileOutputStreamSpeechItemV1 extends SynthesisSpeechItemV1 {
1024        private final FileOutputStream mFileOutputStream;
1025
1026        public SynthesisToFileOutputStreamSpeechItemV1(Object callerIdentity, int callerUid,
1027                int callerPid, Bundle params, String utteranceId, CharSequence text,
1028                FileOutputStream fileOutputStream) {
1029            super(callerIdentity, callerUid, callerPid, params, utteranceId, text);
1030            mFileOutputStream = fileOutputStream;
1031        }
1032
1033        @Override
1034        protected AbstractSynthesisCallback createSynthesisCallback() {
1035            return new FileSynthesisCallback(mFileOutputStream.getChannel(), this, false);
1036        }
1037
1038        @Override
1039        protected void playImpl() {
1040            dispatchOnStart();
1041            super.playImpl();
1042            try {
1043              mFileOutputStream.close();
1044            } catch(IOException e) {
1045              Log.w(TAG, "Failed to close output file", e);
1046            }
1047        }
1048    }
1049
1050    private class AudioSpeechItemV1 extends SpeechItemV1 {
1051        private final AudioPlaybackQueueItem mItem;
1052
1053        public AudioSpeechItemV1(Object callerIdentity, int callerUid, int callerPid,
1054                Bundle params, String utteranceId, Uri uri) {
1055            super(callerIdentity, callerUid, callerPid, params, utteranceId);
1056            mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(),
1057                    TextToSpeechService.this, uri, getAudioParams());
1058        }
1059
1060        @Override
1061        public boolean isValid() {
1062            return true;
1063        }
1064
1065        @Override
1066        protected void playImpl() {
1067            mAudioPlaybackHandler.enqueue(mItem);
1068        }
1069
1070        @Override
1071        protected void stopImpl() {
1072            // Do nothing.
1073        }
1074
1075        @Override
1076        public String getUtteranceId() {
1077            return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null);
1078        }
1079
1080        @Override
1081        AudioOutputParams getAudioParams() {
1082            return AudioOutputParams.createFromV1ParamsBundle(mParams, false);
1083        }
1084    }
1085
1086    private class SilenceSpeechItem extends UtteranceSpeechItem {
1087        private final long mDuration;
1088        private final String mUtteranceId;
1089
1090        public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid,
1091                String utteranceId, long duration) {
1092            super(callerIdentity, callerUid, callerPid);
1093            mUtteranceId = utteranceId;
1094            mDuration = duration;
1095        }
1096
1097        @Override
1098        public boolean isValid() {
1099            return true;
1100        }
1101
1102        @Override
1103        protected void playImpl() {
1104            mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem(
1105                    this, getCallerIdentity(), mDuration));
1106        }
1107
1108        @Override
1109        protected void stopImpl() {
1110
1111        }
1112
1113        @Override
1114        public String getUtteranceId() {
1115            return mUtteranceId;
1116        }
1117    }
1118
1119    /**
1120     * Call {@link TextToSpeechService#onLoadLanguage} on synth thread.
1121     */
1122    private class LoadLanguageItem extends SpeechItem {
1123        private final String mLanguage;
1124        private final String mCountry;
1125        private final String mVariant;
1126
1127        public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid,
1128                String language, String country, String variant) {
1129            super(callerIdentity, callerUid, callerPid);
1130            mLanguage = language;
1131            mCountry = country;
1132            mVariant = variant;
1133        }
1134
1135        @Override
1136        public boolean isValid() {
1137            return true;
1138        }
1139
1140        @Override
1141        protected void playImpl() {
1142            TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant);
1143        }
1144
1145        @Override
1146        protected void stopImpl() {
1147            // No-op
1148        }
1149    }
1150
1151    /**
1152     * Call {@link TextToSpeechService#onLoadLanguage} on synth thread.
1153     */
1154    private class LoadVoiceItem extends SpeechItem {
1155        private final String mVoiceName;
1156
1157        public LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid,
1158                String voiceName) {
1159            super(callerIdentity, callerUid, callerPid);
1160            mVoiceName = voiceName;
1161        }
1162
1163        @Override
1164        public boolean isValid() {
1165            return true;
1166        }
1167
1168        @Override
1169        protected void playImpl() {
1170            TextToSpeechService.this.onLoadVoice(mVoiceName);
1171        }
1172
1173        @Override
1174        protected void stopImpl() {
1175            // No-op
1176        }
1177    }
1178
1179
1180    @Override
1181    public IBinder onBind(Intent intent) {
1182        if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
1183            return mBinder;
1184        }
1185        return null;
1186    }
1187
1188    /**
1189     * Binder returned from {@code #onBind(Intent)}. The methods in this class can be
1190     * called called from several different threads.
1191     */
1192    // NOTE: All calls that are passed in a calling app are interned so that
1193    // they can be used as message objects (which are tested for equality using ==).
1194    private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() {
1195        @Override
1196        public int speak(IBinder caller, CharSequence text, int queueMode, Bundle params,
1197                String utteranceId) {
1198            if (!checkNonNull(caller, text, params)) {
1199                return TextToSpeech.ERROR;
1200            }
1201
1202            SpeechItem item = new SynthesisSpeechItemV1(caller,
1203                    Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text);
1204            return mSynthHandler.enqueueSpeechItem(queueMode, item);
1205        }
1206
1207        @Override
1208        public int synthesizeToFileDescriptor(IBinder caller, CharSequence text, ParcelFileDescriptor
1209                fileDescriptor, Bundle params, String utteranceId) {
1210            if (!checkNonNull(caller, text, fileDescriptor, params)) {
1211                return TextToSpeech.ERROR;
1212            }
1213
1214            // In test env, ParcelFileDescriptor instance may be EXACTLY the same
1215            // one that is used by client. And it will be closed by a client, thus
1216            // preventing us from writing anything to it.
1217            final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd(
1218                    fileDescriptor.detachFd());
1219
1220            SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV1(caller,
1221                    Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text,
1222                    new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor));
1223            return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
1224        }
1225
1226        @Override
1227        public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params,
1228                String utteranceId) {
1229            if (!checkNonNull(caller, audioUri, params)) {
1230                return TextToSpeech.ERROR;
1231            }
1232
1233            SpeechItem item = new AudioSpeechItemV1(caller,
1234                    Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, audioUri);
1235            return mSynthHandler.enqueueSpeechItem(queueMode, item);
1236        }
1237
1238        @Override
1239        public int playSilence(IBinder caller, long duration, int queueMode, String utteranceId) {
1240            if (!checkNonNull(caller)) {
1241                return TextToSpeech.ERROR;
1242            }
1243
1244            SpeechItem item = new SilenceSpeechItem(caller,
1245                    Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, duration);
1246            return mSynthHandler.enqueueSpeechItem(queueMode, item);
1247        }
1248
1249        @Override
1250        public boolean isSpeaking() {
1251            return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking();
1252        }
1253
1254        @Override
1255        public int stop(IBinder caller) {
1256            if (!checkNonNull(caller)) {
1257                return TextToSpeech.ERROR;
1258            }
1259
1260            return mSynthHandler.stopForApp(caller);
1261        }
1262
1263        @Override
1264        public String[] getLanguage() {
1265            return onGetLanguage();
1266        }
1267
1268        @Override
1269        public String[] getClientDefaultLanguage() {
1270            return getSettingsLocale();
1271        }
1272
1273        /*
1274         * If defaults are enforced, then no language is "available" except
1275         * perhaps the default language selected by the user.
1276         */
1277        @Override
1278        public int isLanguageAvailable(String lang, String country, String variant) {
1279            if (!checkNonNull(lang)) {
1280                return TextToSpeech.ERROR;
1281            }
1282
1283            return onIsLanguageAvailable(lang, country, variant);
1284        }
1285
1286        @Override
1287        public String[] getFeaturesForLanguage(String lang, String country, String variant) {
1288            Set<String> features = onGetFeaturesForLanguage(lang, country, variant);
1289            String[] featuresArray = null;
1290            if (features != null) {
1291                featuresArray = new String[features.size()];
1292                features.toArray(featuresArray);
1293            } else {
1294                featuresArray = new String[0];
1295            }
1296            return featuresArray;
1297        }
1298
1299        /*
1300         * There is no point loading a non default language if defaults
1301         * are enforced.
1302         */
1303        @Override
1304        public int loadLanguage(IBinder caller, String lang, String country, String variant) {
1305            if (!checkNonNull(lang)) {
1306                return TextToSpeech.ERROR;
1307            }
1308            int retVal = onIsLanguageAvailable(lang, country, variant);
1309
1310            if (retVal == TextToSpeech.LANG_AVAILABLE ||
1311                    retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
1312                    retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
1313
1314                SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(),
1315                        Binder.getCallingPid(), lang, country, variant);
1316
1317                if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) !=
1318                        TextToSpeech.SUCCESS) {
1319                    return TextToSpeech.ERROR;
1320                }
1321            }
1322            return retVal;
1323        }
1324
1325        @Override
1326        public List<Voice> getVoices() {
1327            return onGetVoices();
1328        }
1329
1330        @Override
1331        public int loadVoice(IBinder caller, String voiceName) {
1332            if (!checkNonNull(voiceName)) {
1333                return TextToSpeech.ERROR;
1334            }
1335            int retVal = onIsValidVoiceName(voiceName);
1336
1337            if (retVal == TextToSpeech.SUCCESS) {
1338                SpeechItem item = new LoadVoiceItem(caller, Binder.getCallingUid(),
1339                        Binder.getCallingPid(), voiceName);
1340                if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) !=
1341                        TextToSpeech.SUCCESS) {
1342                    return TextToSpeech.ERROR;
1343                }
1344            }
1345            return retVal;
1346        }
1347
1348        public String getDefaultVoiceNameFor(String lang, String country, String variant) {
1349            if (!checkNonNull(lang)) {
1350                return null;
1351            }
1352            int retVal = onIsLanguageAvailable(lang, country, variant);
1353
1354            if (retVal == TextToSpeech.LANG_AVAILABLE ||
1355                    retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
1356                    retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
1357                return onGetDefaultVoiceNameFor(lang, country, variant);
1358            } else {
1359                return null;
1360            }
1361        }
1362
1363        @Override
1364        public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
1365            // Note that passing in a null callback is a valid use case.
1366            if (!checkNonNull(caller)) {
1367                return;
1368            }
1369
1370            mCallbacks.setCallback(caller, cb);
1371        }
1372
1373        private String intern(String in) {
1374            // The input parameter will be non null.
1375            return in.intern();
1376        }
1377
1378        private boolean checkNonNull(Object... args) {
1379            for (Object o : args) {
1380                if (o == null) return false;
1381            }
1382            return true;
1383        }
1384    };
1385
1386    private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
1387        private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback
1388                = new HashMap<IBinder, ITextToSpeechCallback>();
1389
1390        public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
1391            synchronized (mCallerToCallback) {
1392                ITextToSpeechCallback old;
1393                if (cb != null) {
1394                    register(cb, caller);
1395                    old = mCallerToCallback.put(caller, cb);
1396                } else {
1397                    old = mCallerToCallback.remove(caller);
1398                }
1399                if (old != null && old != cb) {
1400                    unregister(old);
1401                }
1402            }
1403        }
1404
1405        public void dispatchOnStop(Object callerIdentity, String utteranceId, boolean started) {
1406            ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1407            if (cb == null) return;
1408            try {
1409                cb.onStop(utteranceId, started);
1410            } catch (RemoteException e) {
1411                Log.e(TAG, "Callback onStop failed: " + e);
1412            }
1413        }
1414
1415        public void dispatchOnSuccess(Object callerIdentity, String utteranceId) {
1416            ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1417            if (cb == null) return;
1418            try {
1419                cb.onSuccess(utteranceId);
1420            } catch (RemoteException e) {
1421                Log.e(TAG, "Callback onDone failed: " + e);
1422            }
1423        }
1424
1425        public void dispatchOnStart(Object callerIdentity, String utteranceId) {
1426            ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1427            if (cb == null) return;
1428            try {
1429                cb.onStart(utteranceId);
1430            } catch (RemoteException e) {
1431                Log.e(TAG, "Callback onStart failed: " + e);
1432            }
1433
1434        }
1435
1436        public void dispatchOnError(Object callerIdentity, String utteranceId,
1437                int errorCode) {
1438            ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1439            if (cb == null) return;
1440            try {
1441                cb.onError(utteranceId, errorCode);
1442            } catch (RemoteException e) {
1443                Log.e(TAG, "Callback onError failed: " + e);
1444            }
1445        }
1446
1447        @Override
1448        public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
1449            IBinder caller = (IBinder) cookie;
1450            synchronized (mCallerToCallback) {
1451                mCallerToCallback.remove(caller);
1452            }
1453            //mSynthHandler.stopForApp(caller);
1454        }
1455
1456        @Override
1457        public void kill() {
1458            synchronized (mCallerToCallback) {
1459                mCallerToCallback.clear();
1460                super.kill();
1461            }
1462        }
1463
1464        private ITextToSpeechCallback getCallbackFor(Object caller) {
1465            ITextToSpeechCallback cb;
1466            IBinder asBinder = (IBinder) caller;
1467            synchronized (mCallerToCallback) {
1468                cb = mCallerToCallback.get(asBinder);
1469            }
1470
1471            return cb;
1472        }
1473    }
1474}
1475