TextToSpeechService.java revision 68e2af55d65d2e61fbf8096eccaa2e4ca02b6c5a
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                speechItem.dispatchOnError();
310                return TextToSpeech.ERROR;
311            }
312
313            if (queueMode == TextToSpeech.QUEUE_FLUSH) {
314                stopForApp(speechItem.getCallingApp());
315            } else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
316                stopAll();
317            }
318            Runnable runnable = new Runnable() {
319                @Override
320                public void run() {
321                    setCurrentSpeechItem(speechItem);
322                    speechItem.play();
323                    setCurrentSpeechItem(null);
324                }
325            };
326            Message msg = Message.obtain(this, runnable);
327            // The obj is used to remove all callbacks from the given app in
328            // stopForApp(String).
329            //
330            // Note that this string is interned, so the == comparison works.
331            msg.obj = speechItem.getCallingApp();
332            if (sendMessage(msg)) {
333                return TextToSpeech.SUCCESS;
334            } else {
335                Log.w(TAG, "SynthThread has quit");
336                speechItem.dispatchOnError();
337                return TextToSpeech.ERROR;
338            }
339        }
340
341        /**
342         * Stops all speech output and removes any utterances still in the queue for
343         * the calling app.
344         *
345         * Called on a service binder thread.
346         */
347        public int stopForApp(String callingApp) {
348            if (TextUtils.isEmpty(callingApp)) {
349                return TextToSpeech.ERROR;
350            }
351
352            removeCallbacksAndMessages(callingApp);
353            // This stops writing data to the file / or publishing
354            // items to the audio playback handler.
355            //
356            // Note that the current speech item must be removed only if it
357            // belongs to the callingApp, else the item will be "orphaned" and
358            // not stopped correctly if a stop request comes along for the item
359            // from the app it belongs to.
360            SpeechItem current = maybeRemoveCurrentSpeechItem(callingApp);
361            if (current != null) {
362                current.stop();
363            }
364
365            // Remove any enqueued audio too.
366            mAudioPlaybackHandler.removePlaybackItems(callingApp);
367
368            return TextToSpeech.SUCCESS;
369        }
370
371        public int stopAll() {
372            // Stop the current speech item unconditionally.
373            SpeechItem current = setCurrentSpeechItem(null);
374            if (current != null) {
375                current.stop();
376            }
377            // Remove all other items from the queue.
378            removeCallbacksAndMessages(null);
379            // Remove all pending playback as well.
380            mAudioPlaybackHandler.removeAllItems();
381
382            return TextToSpeech.SUCCESS;
383        }
384    }
385
386    interface UtteranceProgressDispatcher {
387        public void dispatchOnDone();
388        public void dispatchOnStart();
389        public void dispatchOnError();
390    }
391
392    /**
393     * An item in the synth thread queue.
394     */
395    private abstract class SpeechItem implements UtteranceProgressDispatcher {
396        private final String mCallingApp;
397        protected final Bundle mParams;
398        private boolean mStarted = false;
399        private boolean mStopped = false;
400
401        public SpeechItem(String callingApp, Bundle params) {
402            mCallingApp = callingApp;
403            mParams = params;
404        }
405
406        public String getCallingApp() {
407            return mCallingApp;
408        }
409
410        /**
411         * Checker whether the item is valid. If this method returns false, the item should not
412         * be played.
413         */
414        public abstract boolean isValid();
415
416        /**
417         * Plays the speech item. Blocks until playback is finished.
418         * Must not be called more than once.
419         *
420         * Only called on the synthesis thread.
421         *
422         * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}.
423         */
424        public int play() {
425            synchronized (this) {
426                if (mStarted) {
427                    throw new IllegalStateException("play() called twice");
428                }
429                mStarted = true;
430            }
431            return playImpl();
432        }
433
434        /**
435         * Stops the speech item.
436         * Must not be called more than once.
437         *
438         * Can be called on multiple threads,  but not on the synthesis thread.
439         */
440        public void stop() {
441            synchronized (this) {
442                if (mStopped) {
443                    throw new IllegalStateException("stop() called twice");
444                }
445                mStopped = true;
446            }
447            stopImpl();
448        }
449
450        @Override
451        public void dispatchOnDone() {
452            final String utteranceId = getUtteranceId();
453            if (utteranceId != null) {
454                mCallbacks.dispatchOnDone(getCallingApp(), utteranceId);
455            }
456        }
457
458        @Override
459        public void dispatchOnStart() {
460            final String utteranceId = getUtteranceId();
461            if (utteranceId != null) {
462                mCallbacks.dispatchOnStart(getCallingApp(), utteranceId);
463            }
464        }
465
466        @Override
467        public void dispatchOnError() {
468            final String utteranceId = getUtteranceId();
469            if (utteranceId != null) {
470                mCallbacks.dispatchOnError(getCallingApp(), utteranceId);
471            }
472        }
473
474        protected synchronized boolean isStopped() {
475             return mStopped;
476        }
477
478        protected abstract int playImpl();
479
480        protected abstract void stopImpl();
481
482        public int getStreamType() {
483            return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM);
484        }
485
486        public float getVolume() {
487            return getFloatParam(Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME);
488        }
489
490        public float getPan() {
491            return getFloatParam(Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN);
492        }
493
494        public String getUtteranceId() {
495            return getStringParam(Engine.KEY_PARAM_UTTERANCE_ID, null);
496        }
497
498        protected String getStringParam(String key, String defaultValue) {
499            return mParams == null ? defaultValue : mParams.getString(key, defaultValue);
500        }
501
502        protected int getIntParam(String key, int defaultValue) {
503            return mParams == null ? defaultValue : mParams.getInt(key, defaultValue);
504        }
505
506        protected float getFloatParam(String key, float defaultValue) {
507            return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue);
508        }
509    }
510
511    class SynthesisSpeechItem extends SpeechItem {
512        // Never null.
513        private final String mText;
514        private final SynthesisRequest mSynthesisRequest;
515        private final String[] mDefaultLocale;
516        // Non null after synthesis has started, and all accesses
517        // guarded by 'this'.
518        private AbstractSynthesisCallback mSynthesisCallback;
519        private final EventLogger mEventLogger;
520
521        public SynthesisSpeechItem(String callingApp, Bundle params, String text) {
522            super(callingApp, params);
523            mText = text;
524            mSynthesisRequest = new SynthesisRequest(mText, mParams);
525            mDefaultLocale = getSettingsLocale();
526            setRequestParams(mSynthesisRequest);
527            mEventLogger = new EventLogger(mSynthesisRequest, getCallingApp(), mPackageName);
528        }
529
530        public String getText() {
531            return mText;
532        }
533
534        @Override
535        public boolean isValid() {
536            if (mText == null) {
537                Log.wtf(TAG, "Got null text");
538                return false;
539            }
540            if (mText.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH) {
541                Log.w(TAG, "Text too long: " + mText.length() + " chars");
542                return false;
543            }
544            return true;
545        }
546
547        @Override
548        protected int playImpl() {
549            AbstractSynthesisCallback synthesisCallback;
550            mEventLogger.onRequestProcessingStart();
551            synchronized (this) {
552                // stop() might have been called before we enter this
553                // synchronized block.
554                if (isStopped()) {
555                    return TextToSpeech.ERROR;
556                }
557                mSynthesisCallback = createSynthesisCallback();
558                synthesisCallback = mSynthesisCallback;
559            }
560            TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
561            return synthesisCallback.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR;
562        }
563
564        protected AbstractSynthesisCallback createSynthesisCallback() {
565            return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(),
566                    mAudioPlaybackHandler, this, getCallingApp(), mEventLogger);
567        }
568
569        private void setRequestParams(SynthesisRequest request) {
570            request.setLanguage(getLanguage(), getCountry(), getVariant());
571            request.setSpeechRate(getSpeechRate());
572
573            request.setPitch(getPitch());
574        }
575
576        @Override
577        protected void stopImpl() {
578            AbstractSynthesisCallback synthesisCallback;
579            synchronized (this) {
580                synthesisCallback = mSynthesisCallback;
581            }
582            if (synthesisCallback != null) {
583                // If the synthesis callback is null, it implies that we haven't
584                // entered the synchronized(this) block in playImpl which in
585                // turn implies that synthesis would not have started.
586                synthesisCallback.stop();
587                TextToSpeechService.this.onStop();
588            }
589        }
590
591        public String getLanguage() {
592            return getStringParam(Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]);
593        }
594
595        private boolean hasLanguage() {
596            return !TextUtils.isEmpty(getStringParam(Engine.KEY_PARAM_LANGUAGE, null));
597        }
598
599        private String getCountry() {
600            if (!hasLanguage()) return mDefaultLocale[1];
601            return getStringParam(Engine.KEY_PARAM_COUNTRY, "");
602        }
603
604        private String getVariant() {
605            if (!hasLanguage()) return mDefaultLocale[2];
606            return getStringParam(Engine.KEY_PARAM_VARIANT, "");
607        }
608
609        private int getSpeechRate() {
610            return getIntParam(Engine.KEY_PARAM_RATE, getDefaultSpeechRate());
611        }
612
613        private int getPitch() {
614            return getIntParam(Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH);
615        }
616    }
617
618    private class SynthesisToFileSpeechItem extends SynthesisSpeechItem {
619        private final File mFile;
620
621        public SynthesisToFileSpeechItem(String callingApp, Bundle params, String text,
622                File file) {
623            super(callingApp, params, text);
624            mFile = file;
625        }
626
627        @Override
628        public boolean isValid() {
629            if (!super.isValid()) {
630                return false;
631            }
632            return checkFile(mFile);
633        }
634
635        @Override
636        protected AbstractSynthesisCallback createSynthesisCallback() {
637            return new FileSynthesisCallback(mFile);
638        }
639
640        @Override
641        protected int playImpl() {
642            dispatchOnStart();
643            int status = super.playImpl();
644            if (status == TextToSpeech.SUCCESS) {
645                dispatchOnDone();
646            } else {
647                dispatchOnError();
648            }
649            return status;
650        }
651
652        /**
653         * Checks that the given file can be used for synthesis output.
654         */
655        private boolean checkFile(File file) {
656            try {
657                if (file.exists()) {
658                    Log.v(TAG, "File " + file + " exists, deleting.");
659                    if (!file.delete()) {
660                        Log.e(TAG, "Failed to delete " + file);
661                        return false;
662                    }
663                }
664                if (!file.createNewFile()) {
665                    Log.e(TAG, "Can't create file " + file);
666                    return false;
667                }
668                if (!file.delete()) {
669                    Log.e(TAG, "Failed to delete " + file);
670                    return false;
671                }
672                return true;
673            } catch (IOException e) {
674                Log.e(TAG, "Can't use " + file + " due to exception " + e);
675                return false;
676            }
677        }
678    }
679
680    private class AudioSpeechItem extends SpeechItem {
681
682        private final BlockingMediaPlayer mPlayer;
683        private AudioMessageParams mToken;
684
685        public AudioSpeechItem(String callingApp, Bundle params, Uri uri) {
686            super(callingApp, params);
687            mPlayer = new BlockingMediaPlayer(TextToSpeechService.this, uri, getStreamType());
688        }
689
690        @Override
691        public boolean isValid() {
692            return true;
693        }
694
695        @Override
696        protected int playImpl() {
697            mToken = new AudioMessageParams(this, getCallingApp(), mPlayer);
698            mAudioPlaybackHandler.enqueueAudio(mToken);
699            return TextToSpeech.SUCCESS;
700        }
701
702        @Override
703        protected void stopImpl() {
704            // Do nothing.
705        }
706    }
707
708    private class SilenceSpeechItem extends SpeechItem {
709        private final long mDuration;
710        private SilenceMessageParams mToken;
711
712        public SilenceSpeechItem(String callingApp, Bundle params, long duration) {
713            super(callingApp, params);
714            mDuration = duration;
715        }
716
717        @Override
718        public boolean isValid() {
719            return true;
720        }
721
722        @Override
723        protected int playImpl() {
724            mToken = new SilenceMessageParams(this, getCallingApp(), mDuration);
725            mAudioPlaybackHandler.enqueueSilence(mToken);
726            return TextToSpeech.SUCCESS;
727        }
728
729        @Override
730        protected void stopImpl() {
731            // Do nothing.
732        }
733    }
734
735    @Override
736    public IBinder onBind(Intent intent) {
737        if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
738            return mBinder;
739        }
740        return null;
741    }
742
743    /**
744     * Binder returned from {@code #onBind(Intent)}. The methods in this class can be
745     * called called from several different threads.
746     */
747    // NOTE: All calls that are passed in a calling app are interned so that
748    // they can be used as message objects (which are tested for equality using ==).
749    private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() {
750
751        public int speak(String callingApp, String text, int queueMode, Bundle params) {
752            if (!checkNonNull(callingApp, text, params)) {
753                return TextToSpeech.ERROR;
754            }
755
756            SpeechItem item = new SynthesisSpeechItem(intern(callingApp), params, text);
757            return mSynthHandler.enqueueSpeechItem(queueMode, item);
758        }
759
760        public int synthesizeToFile(String callingApp, String text, String filename,
761                Bundle params) {
762            if (!checkNonNull(callingApp, text, filename, params)) {
763                return TextToSpeech.ERROR;
764            }
765
766            File file = new File(filename);
767            SpeechItem item = new SynthesisToFileSpeechItem(intern(callingApp),
768                    params, text, file);
769            return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
770        }
771
772        public int playAudio(String callingApp, Uri audioUri, int queueMode, Bundle params) {
773            if (!checkNonNull(callingApp, audioUri, params)) {
774                return TextToSpeech.ERROR;
775            }
776
777            SpeechItem item = new AudioSpeechItem(intern(callingApp), params, audioUri);
778            return mSynthHandler.enqueueSpeechItem(queueMode, item);
779        }
780
781        public int playSilence(String callingApp, long duration, int queueMode, Bundle params) {
782            if (!checkNonNull(callingApp, params)) {
783                return TextToSpeech.ERROR;
784            }
785
786            SpeechItem item = new SilenceSpeechItem(intern(callingApp), params, duration);
787            return mSynthHandler.enqueueSpeechItem(queueMode, item);
788        }
789
790        public boolean isSpeaking() {
791            return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking();
792        }
793
794        public int stop(String callingApp) {
795            if (!checkNonNull(callingApp)) {
796                return TextToSpeech.ERROR;
797            }
798
799            return mSynthHandler.stopForApp(intern(callingApp));
800        }
801
802        public String[] getLanguage() {
803            return onGetLanguage();
804        }
805
806        /*
807         * If defaults are enforced, then no language is "available" except
808         * perhaps the default language selected by the user.
809         */
810        public int isLanguageAvailable(String lang, String country, String variant) {
811            if (!checkNonNull(lang)) {
812                return TextToSpeech.ERROR;
813            }
814
815            return onIsLanguageAvailable(lang, country, variant);
816        }
817
818        public String[] getFeaturesForLanguage(String lang, String country, String variant) {
819            Set<String> features = onGetFeaturesForLanguage(lang, country, variant);
820            String[] featuresArray = null;
821            if (features != null) {
822                featuresArray = new String[features.size()];
823                features.toArray(featuresArray);
824            } else {
825                featuresArray = new String[0];
826            }
827            return featuresArray;
828        }
829
830        /*
831         * There is no point loading a non default language if defaults
832         * are enforced.
833         */
834        public int loadLanguage(String lang, String country, String variant) {
835            if (!checkNonNull(lang)) {
836                return TextToSpeech.ERROR;
837            }
838
839            return onLoadLanguage(lang, country, variant);
840        }
841
842        public void setCallback(String packageName, ITextToSpeechCallback cb) {
843            // Note that passing in a null callback is a valid use case.
844            if (!checkNonNull(packageName)) {
845                return;
846            }
847
848            mCallbacks.setCallback(packageName, cb);
849        }
850
851        private String intern(String in) {
852            // The input parameter will be non null.
853            return in.intern();
854        }
855
856        private boolean checkNonNull(Object... args) {
857            for (Object o : args) {
858                if (o == null) return false;
859            }
860            return true;
861        }
862    };
863
864    private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
865
866        private final HashMap<String, ITextToSpeechCallback> mAppToCallback
867                = new HashMap<String, ITextToSpeechCallback>();
868
869        public void setCallback(String packageName, ITextToSpeechCallback cb) {
870            synchronized (mAppToCallback) {
871                ITextToSpeechCallback old;
872                if (cb != null) {
873                    register(cb, packageName);
874                    old = mAppToCallback.put(packageName, cb);
875                } else {
876                    old = mAppToCallback.remove(packageName);
877                }
878                if (old != null && old != cb) {
879                    unregister(old);
880                }
881            }
882        }
883
884        public void dispatchOnDone(String packageName, String utteranceId) {
885            ITextToSpeechCallback cb = getCallbackFor(packageName);
886            if (cb == null) return;
887            try {
888                cb.onDone(utteranceId);
889            } catch (RemoteException e) {
890                Log.e(TAG, "Callback onDone failed: " + e);
891            }
892        }
893
894        public void dispatchOnStart(String packageName, String utteranceId) {
895            ITextToSpeechCallback cb = getCallbackFor(packageName);
896            if (cb == null) return;
897            try {
898                cb.onStart(utteranceId);
899            } catch (RemoteException e) {
900                Log.e(TAG, "Callback onStart failed: " + e);
901            }
902
903        }
904
905        public void dispatchOnError(String packageName, String utteranceId) {
906            ITextToSpeechCallback cb = getCallbackFor(packageName);
907            if (cb == null) return;
908            try {
909                cb.onError(utteranceId);
910            } catch (RemoteException e) {
911                Log.e(TAG, "Callback onError failed: " + e);
912            }
913        }
914
915        @Override
916        public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
917            String packageName = (String) cookie;
918            synchronized (mAppToCallback) {
919                mAppToCallback.remove(packageName);
920            }
921            mSynthHandler.stopForApp(packageName);
922        }
923
924        @Override
925        public void kill() {
926            synchronized (mAppToCallback) {
927                mAppToCallback.clear();
928                super.kill();
929            }
930        }
931
932        private ITextToSpeechCallback getCallbackFor(String packageName) {
933            ITextToSpeechCallback cb;
934            synchronized (mAppToCallback) {
935                cb = mAppToCallback.get(packageName);
936            }
937
938            return cb;
939        }
940
941    }
942
943}
944