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