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