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