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