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