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