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