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