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