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