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