TextToSpeechService.java revision 4b73867a12a9339c7788e8949aac4a32d2eee22b
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.media.AudioAttributes; 21import android.media.AudioSystem; 22import android.net.Uri; 23import android.os.Binder; 24import android.os.Bundle; 25import android.os.Handler; 26import android.os.HandlerThread; 27import android.os.IBinder; 28import android.os.Looper; 29import android.os.Message; 30import android.os.MessageQueue; 31import android.os.ParcelFileDescriptor; 32import android.os.RemoteCallbackList; 33import android.os.RemoteException; 34import android.provider.Settings; 35import android.speech.tts.TextToSpeech.Engine; 36import android.text.TextUtils; 37import android.util.Log; 38 39import java.io.FileOutputStream; 40import java.io.IOException; 41import java.util.ArrayList; 42import java.util.Collections; 43import java.util.HashMap; 44import java.util.List; 45import java.util.Locale; 46import java.util.Map; 47import java.util.MissingResourceException; 48import java.util.Set; 49 50 51/** 52 * Abstract base class for TTS engine implementations. The following methods 53 * need to be implemented: 54 * <ul> 55 * <li>{@link #onIsLanguageAvailable}</li> 56 * <li>{@link #onLoadLanguage}</li> 57 * <li>{@link #onGetLanguage}</li> 58 * <li>{@link #onSynthesizeText}</li> 59 * <li>{@link #onStop}</li> 60 * </ul> 61 * The first three deal primarily with language management, and are used to 62 * query the engine for it's support for a given language and indicate to it 63 * that requests in a given language are imminent. 64 * 65 * {@link #onSynthesizeText} is central to the engine implementation. The 66 * implementation should synthesize text as per the request parameters and 67 * return synthesized data via the supplied callback. This class and its helpers 68 * will then consume that data, which might mean queuing it for playback or writing 69 * it to a file or similar. All calls to this method will be on a single thread, 70 * which will be different from the main thread of the service. Synthesis must be 71 * synchronous which means the engine must NOT hold on to the callback or call any 72 * methods on it after the method returns. 73 * 74 * {@link #onStop} tells the engine that it should stop 75 * all ongoing synthesis, if any. Any pending data from the current synthesis 76 * will be discarded. 77 * 78 * {@link #onGetLanguage} is not required as of JELLYBEAN_MR2 (API 18) and later, it is only 79 * called on earlier versions of Android. 80 * 81 * API Level 20 adds support for Voice objects. Voices are an abstraction that allow the TTS 82 * service to expose multiple backends for a single locale. Each one of them can have a different 83 * features set. In order to fully take advantage of voices, an engine should implement 84 * the following methods: 85 * <ul> 86 * <li>{@link #onGetVoices()}</li> 87 * <li>{@link #onIsValidVoiceName(String)}</li> 88 * <li>{@link #onLoadVoice(String)}</li> 89 * <li>{@link #onGetDefaultVoiceNameFor(String, String, String)}</li> 90 * </ul> 91 * The first three methods are siblings of the {@link #onGetLanguage}, 92 * {@link #onIsLanguageAvailable} and {@link #onLoadLanguage} methods. The last one, 93 * {@link #onGetDefaultVoiceNameFor(String, String, String)} is a link between locale and voice 94 * based methods. Since API level 21 {@link TextToSpeech#setLanguage} is implemented by 95 * calling {@link TextToSpeech#setVoice} with the voice returned by 96 * {@link #onGetDefaultVoiceNameFor(String, String, String)}. 97 * 98 * If the client uses a voice instead of a locale, {@link SynthesisRequest} will contain the 99 * requested voice name. 100 * 101 * The default implementations of Voice-related methods implement them using the 102 * pre-existing locale-based implementation. 103 */ 104public abstract class TextToSpeechService extends Service { 105 106 private static final boolean DBG = false; 107 private static final String TAG = "TextToSpeechService"; 108 109 private static final String SYNTH_THREAD_NAME = "SynthThread"; 110 111 private SynthHandler mSynthHandler; 112 // A thread and it's associated handler for playing back any audio 113 // associated with this TTS engine. Will handle all requests except synthesis 114 // to file requests, which occur on the synthesis thread. 115 private AudioPlaybackHandler mAudioPlaybackHandler; 116 private TtsEngines mEngineHelper; 117 118 private CallbackMap mCallbacks; 119 private String mPackageName; 120 121 private final Object mVoicesInfoLock = new Object(); 122 123 @Override 124 public void onCreate() { 125 if (DBG) Log.d(TAG, "onCreate()"); 126 super.onCreate(); 127 128 SynthThread synthThread = new SynthThread(); 129 synthThread.start(); 130 mSynthHandler = new SynthHandler(synthThread.getLooper()); 131 132 mAudioPlaybackHandler = new AudioPlaybackHandler(); 133 mAudioPlaybackHandler.start(); 134 135 mEngineHelper = new TtsEngines(this); 136 137 mCallbacks = new CallbackMap(); 138 139 mPackageName = getApplicationInfo().packageName; 140 141 String[] defaultLocale = getSettingsLocale(); 142 143 // Load default language 144 onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]); 145 } 146 147 @Override 148 public void onDestroy() { 149 if (DBG) Log.d(TAG, "onDestroy()"); 150 151 // Tell the synthesizer to stop 152 mSynthHandler.quit(); 153 // Tell the audio playback thread to stop. 154 mAudioPlaybackHandler.quit(); 155 // Unregister all callbacks. 156 mCallbacks.kill(); 157 158 super.onDestroy(); 159 } 160 161 /** 162 * Checks whether the engine supports a given language. 163 * 164 * Can be called on multiple threads. 165 * 166 * Its return values HAVE to be consistent with onLoadLanguage. 167 * 168 * @param lang ISO-3 language code. 169 * @param country ISO-3 country code. May be empty or null. 170 * @param variant Language variant. May be empty or null. 171 * @return Code indicating the support status for the locale. 172 * One of {@link TextToSpeech#LANG_AVAILABLE}, 173 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 174 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 175 * {@link TextToSpeech#LANG_MISSING_DATA} 176 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 177 */ 178 protected abstract int onIsLanguageAvailable(String lang, String country, String variant); 179 180 /** 181 * Returns the language, country and variant currently being used by the TTS engine. 182 * 183 * This method will be called only on Android 4.2 and before (API <= 17). In later versions 184 * this method is not called by the Android TTS framework. 185 * 186 * Can be called on multiple threads. 187 * 188 * @return A 3-element array, containing language (ISO 3-letter code), 189 * country (ISO 3-letter code) and variant used by the engine. 190 * The country and variant may be {@code ""}. If country is empty, then variant must 191 * be empty too. 192 * @see Locale#getISO3Language() 193 * @see Locale#getISO3Country() 194 * @see Locale#getVariant() 195 */ 196 protected abstract String[] onGetLanguage(); 197 198 /** 199 * Notifies the engine that it should load a speech synthesis language. There is no guarantee 200 * that this method is always called before the language is used for synthesis. It is merely 201 * a hint to the engine that it will probably get some synthesis requests for this language 202 * at some point in the future. 203 * 204 * Can be called on multiple threads. 205 * In <= Android 4.2 (<= API 17) can be called on main and service binder threads. 206 * In > Android 4.2 (> API 17) can be called on main and synthesis threads. 207 * 208 * @param lang ISO-3 language code. 209 * @param country ISO-3 country code. May be empty or null. 210 * @param variant Language variant. May be empty or null. 211 * @return Code indicating the support status for the locale. 212 * One of {@link TextToSpeech#LANG_AVAILABLE}, 213 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 214 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 215 * {@link TextToSpeech#LANG_MISSING_DATA} 216 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 217 */ 218 protected abstract int onLoadLanguage(String lang, String country, String variant); 219 220 /** 221 * Notifies the service that it should stop any in-progress speech synthesis. 222 * This method can be called even if no speech synthesis is currently in progress. 223 * 224 * Can be called on multiple threads, but not on the synthesis thread. 225 */ 226 protected abstract void onStop(); 227 228 /** 229 * Tells the service to synthesize speech from the given text. This method 230 * should block until the synthesis is finished. Used for requests from V1 231 * clients ({@link android.speech.tts.TextToSpeech}). Called on the synthesis 232 * thread. 233 * 234 * @param request The synthesis request. 235 * @param callback The callback that the engine must use to make data 236 * available for playback or for writing to a file. 237 */ 238 protected abstract void onSynthesizeText(SynthesisRequest request, 239 SynthesisCallback callback); 240 241 /** 242 * Queries the service for a set of features supported for a given language. 243 * 244 * Can be called on multiple threads. 245 * 246 * @param lang ISO-3 language code. 247 * @param country ISO-3 country code. May be empty or null. 248 * @param variant Language variant. May be empty or null. 249 * @return A list of features supported for the given language. 250 */ 251 protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) { 252 return null; 253 } 254 255 private int getExpectedLanguageAvailableStatus(Locale locale) { 256 int expectedStatus = TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE; 257 if (locale.getVariant().isEmpty()) { 258 if (locale.getCountry().isEmpty()) { 259 expectedStatus = TextToSpeech.LANG_AVAILABLE; 260 } else { 261 expectedStatus = TextToSpeech.LANG_COUNTRY_AVAILABLE; 262 } 263 } 264 return expectedStatus; 265 } 266 267 /** 268 * Queries the service for a set of supported voices. 269 * 270 * Can be called on multiple threads. 271 * 272 * The default implementation tries to enumerate all available locales, pass them to 273 * {@link #onIsLanguageAvailable(String, String, String)} and create Voice instances (using 274 * the locale's BCP-47 language tag as the voice name) for the ones that are supported. 275 * Note, that this implementation is suitable only for engines that don't have multiple voices 276 * for a single locale. Also, this implementation won't work with Locales not listed in the 277 * set returned by the {@link Locale#getAvailableLocales()} method. 278 * 279 * @return A list of voices supported. 280 */ 281 public List<Voice> onGetVoices() { 282 // Enumerate all locales and check if they are available 283 ArrayList<Voice> voices = new ArrayList<Voice>(); 284 for (Locale locale : Locale.getAvailableLocales()) { 285 int expectedStatus = getExpectedLanguageAvailableStatus(locale); 286 try { 287 int localeStatus = onIsLanguageAvailable(locale.getISO3Language(), 288 locale.getISO3Country(), locale.getVariant()); 289 if (localeStatus != expectedStatus) { 290 continue; 291 } 292 } catch (MissingResourceException e) { 293 // Ignore locale without iso 3 codes 294 continue; 295 } 296 Set<String> features = onGetFeaturesForLanguage(locale.getISO3Language(), 297 locale.getISO3Country(), locale.getVariant()); 298 voices.add(new Voice(locale.toLanguageTag(), locale, Voice.QUALITY_NORMAL, 299 Voice.LATENCY_NORMAL, false, features)); 300 } 301 return voices; 302 } 303 304 /** 305 * Return a name of the default voice for a given locale. 306 * 307 * This method provides a mapping between locales and available voices. This method is 308 * used in {@link TextToSpeech#setLanguage}, which calls this method and then calls 309 * {@link TextToSpeech#setVoice} with the voice returned by this method. 310 * 311 * Also, it's used by {@link TextToSpeech#getDefaultVoice()} to find a default voice for 312 * the default locale. 313 * 314 * @param lang ISO-3 language code. 315 * @param country ISO-3 country code. May be empty or null. 316 * @param variant Language variant. May be empty or null. 317 318 * @return A name of the default voice for a given locale. 319 */ 320 public String onGetDefaultVoiceNameFor(String lang, String country, String variant) { 321 int localeStatus = onIsLanguageAvailable(lang, country, variant); 322 Locale iso3Locale = null; 323 switch (localeStatus) { 324 case TextToSpeech.LANG_AVAILABLE: 325 iso3Locale = new Locale(lang); 326 break; 327 case TextToSpeech.LANG_COUNTRY_AVAILABLE: 328 iso3Locale = new Locale(lang, country); 329 break; 330 case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE: 331 iso3Locale = new Locale(lang, country, variant); 332 break; 333 default: 334 return null; 335 } 336 Locale properLocale = TtsEngines.normalizeTTSLocale(iso3Locale); 337 String voiceName = properLocale.toLanguageTag(); 338 if (onIsValidVoiceName(voiceName) == TextToSpeech.SUCCESS) { 339 return voiceName; 340 } else { 341 return null; 342 } 343 } 344 345 /** 346 * Notifies the engine that it should load a speech synthesis voice. There is no guarantee 347 * that this method is always called before the voice is used for synthesis. It is merely 348 * a hint to the engine that it will probably get some synthesis requests for this voice 349 * at some point in the future. 350 * 351 * Will be called only on synthesis thread. 352 * 353 * The default implementation creates a Locale from the voice name (by interpreting the name as 354 * a BCP-47 tag for the locale), and passes it to 355 * {@link #onLoadLanguage(String, String, String)}. 356 * 357 * @param voiceName Name of the voice. 358 * @return {@link TextToSpeech#ERROR} or {@link TextToSpeech#SUCCESS}. 359 */ 360 public int onLoadVoice(String voiceName) { 361 Locale locale = Locale.forLanguageTag(voiceName); 362 if (locale == null) { 363 return TextToSpeech.ERROR; 364 } 365 int expectedStatus = getExpectedLanguageAvailableStatus(locale); 366 try { 367 int localeStatus = onIsLanguageAvailable(locale.getISO3Language(), 368 locale.getISO3Country(), locale.getVariant()); 369 if (localeStatus != expectedStatus) { 370 return TextToSpeech.ERROR; 371 } 372 onLoadLanguage(locale.getISO3Language(), 373 locale.getISO3Country(), locale.getVariant()); 374 return TextToSpeech.SUCCESS; 375 } catch (MissingResourceException e) { 376 return TextToSpeech.ERROR; 377 } 378 } 379 380 /** 381 * Checks whether the engine supports a voice with a given name. 382 * 383 * Can be called on multiple threads. 384 * 385 * The default implementation treats the voice name as a language tag, creating a Locale from 386 * the voice name, and passes it to {@link #onIsLanguageAvailable(String, String, String)}. 387 * 388 * @param voiceName Name of the voice. 389 * @return {@link TextToSpeech#ERROR} or {@link TextToSpeech#SUCCESS}. 390 */ 391 public int onIsValidVoiceName(String voiceName) { 392 Locale locale = Locale.forLanguageTag(voiceName); 393 if (locale == null) { 394 return TextToSpeech.ERROR; 395 } 396 int expectedStatus = getExpectedLanguageAvailableStatus(locale); 397 try { 398 int localeStatus = onIsLanguageAvailable(locale.getISO3Language(), 399 locale.getISO3Country(), locale.getVariant()); 400 if (localeStatus != expectedStatus) { 401 return TextToSpeech.ERROR; 402 } 403 return TextToSpeech.SUCCESS; 404 } catch (MissingResourceException e) { 405 return TextToSpeech.ERROR; 406 } 407 } 408 409 private int getDefaultSpeechRate() { 410 return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE); 411 } 412 413 private String[] getSettingsLocale() { 414 final Locale locale = mEngineHelper.getLocalePrefForEngine(mPackageName); 415 return TtsEngines.toOldLocaleStringFormat(locale); 416 } 417 418 private int getSecureSettingInt(String name, int defaultValue) { 419 return Settings.Secure.getInt(getContentResolver(), name, defaultValue); 420 } 421 422 /** 423 * Synthesizer thread. This thread is used to run {@link SynthHandler}. 424 */ 425 private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler { 426 427 private boolean mFirstIdle = true; 428 429 public SynthThread() { 430 super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_DEFAULT); 431 } 432 433 @Override 434 protected void onLooperPrepared() { 435 getLooper().getQueue().addIdleHandler(this); 436 } 437 438 @Override 439 public boolean queueIdle() { 440 if (mFirstIdle) { 441 mFirstIdle = false; 442 } else { 443 broadcastTtsQueueProcessingCompleted(); 444 } 445 return true; 446 } 447 448 private void broadcastTtsQueueProcessingCompleted() { 449 Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED); 450 if (DBG) Log.d(TAG, "Broadcasting: " + i); 451 sendBroadcast(i); 452 } 453 } 454 455 private class SynthHandler extends Handler { 456 private SpeechItem mCurrentSpeechItem = null; 457 458 private ArrayList<Object> mFlushedObjects = new ArrayList<Object>(); 459 private boolean mFlushAll; 460 461 public SynthHandler(Looper looper) { 462 super(looper); 463 } 464 465 private void startFlushingSpeechItems(Object callerIdentity) { 466 synchronized (mFlushedObjects) { 467 if (callerIdentity == null) { 468 mFlushAll = true; 469 } else { 470 mFlushedObjects.add(callerIdentity); 471 } 472 } 473 } 474 private void endFlushingSpeechItems(Object callerIdentity) { 475 synchronized (mFlushedObjects) { 476 if (callerIdentity == null) { 477 mFlushAll = false; 478 } else { 479 mFlushedObjects.remove(callerIdentity); 480 } 481 } 482 } 483 private boolean isFlushed(SpeechItem speechItem) { 484 synchronized (mFlushedObjects) { 485 return mFlushAll || mFlushedObjects.contains(speechItem.getCallerIdentity()); 486 } 487 } 488 489 private synchronized SpeechItem getCurrentSpeechItem() { 490 return mCurrentSpeechItem; 491 } 492 493 private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) { 494 SpeechItem old = mCurrentSpeechItem; 495 mCurrentSpeechItem = speechItem; 496 return old; 497 } 498 499 private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) { 500 if (mCurrentSpeechItem != null && 501 (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) { 502 SpeechItem current = mCurrentSpeechItem; 503 mCurrentSpeechItem = null; 504 return current; 505 } 506 507 return null; 508 } 509 510 public boolean isSpeaking() { 511 return getCurrentSpeechItem() != null; 512 } 513 514 public void quit() { 515 // Don't process any more speech items 516 getLooper().quit(); 517 // Stop the current speech item 518 SpeechItem current = setCurrentSpeechItem(null); 519 if (current != null) { 520 current.stop(); 521 } 522 // The AudioPlaybackHandler will be destroyed by the caller. 523 } 524 525 /** 526 * Adds a speech item to the queue. 527 * 528 * Called on a service binder thread. 529 */ 530 public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) { 531 UtteranceProgressDispatcher utterenceProgress = null; 532 if (speechItem instanceof UtteranceProgressDispatcher) { 533 utterenceProgress = (UtteranceProgressDispatcher) speechItem; 534 } 535 536 if (!speechItem.isValid()) { 537 if (utterenceProgress != null) { 538 utterenceProgress.dispatchOnError( 539 TextToSpeech.ERROR_INVALID_REQUEST); 540 } 541 return TextToSpeech.ERROR; 542 } 543 544 if (queueMode == TextToSpeech.QUEUE_FLUSH) { 545 stopForApp(speechItem.getCallerIdentity()); 546 } else if (queueMode == TextToSpeech.QUEUE_DESTROY) { 547 stopAll(); 548 } 549 Runnable runnable = new Runnable() { 550 @Override 551 public void run() { 552 if (isFlushed(speechItem)) { 553 speechItem.stop(); 554 } else { 555 setCurrentSpeechItem(speechItem); 556 speechItem.play(); 557 setCurrentSpeechItem(null); 558 } 559 } 560 }; 561 Message msg = Message.obtain(this, runnable); 562 563 // The obj is used to remove all callbacks from the given app in 564 // stopForApp(String). 565 // 566 // Note that this string is interned, so the == comparison works. 567 msg.obj = speechItem.getCallerIdentity(); 568 569 if (sendMessage(msg)) { 570 return TextToSpeech.SUCCESS; 571 } else { 572 Log.w(TAG, "SynthThread has quit"); 573 if (utterenceProgress != null) { 574 utterenceProgress.dispatchOnError(TextToSpeech.ERROR_SERVICE); 575 } 576 return TextToSpeech.ERROR; 577 } 578 } 579 580 /** 581 * Stops all speech output and removes any utterances still in the queue for 582 * the calling app. 583 * 584 * Called on a service binder thread. 585 */ 586 public int stopForApp(final Object callerIdentity) { 587 if (callerIdentity == null) { 588 return TextToSpeech.ERROR; 589 } 590 591 // Flush pending messages from callerIdentity 592 startFlushingSpeechItems(callerIdentity); 593 594 // This stops writing data to the file / or publishing 595 // items to the audio playback handler. 596 // 597 // Note that the current speech item must be removed only if it 598 // belongs to the callingApp, else the item will be "orphaned" and 599 // not stopped correctly if a stop request comes along for the item 600 // from the app it belongs to. 601 SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity); 602 if (current != null) { 603 current.stop(); 604 } 605 606 // Remove any enqueued audio too. 607 mAudioPlaybackHandler.stopForApp(callerIdentity); 608 609 // Stop flushing pending messages 610 Runnable runnable = new Runnable() { 611 @Override 612 public void run() { 613 endFlushingSpeechItems(callerIdentity); 614 } 615 }; 616 sendMessage(Message.obtain(this, runnable)); 617 return TextToSpeech.SUCCESS; 618 } 619 620 public int stopAll() { 621 // Order to flush pending messages 622 startFlushingSpeechItems(null); 623 624 // Stop the current speech item unconditionally . 625 SpeechItem current = setCurrentSpeechItem(null); 626 if (current != null) { 627 current.stop(); 628 } 629 // Remove all pending playback as well. 630 mAudioPlaybackHandler.stop(); 631 632 // Message to stop flushing pending messages 633 Runnable runnable = new Runnable() { 634 @Override 635 public void run() { 636 endFlushingSpeechItems(null); 637 } 638 }; 639 sendMessage(Message.obtain(this, runnable)); 640 641 642 return TextToSpeech.SUCCESS; 643 } 644 } 645 646 interface UtteranceProgressDispatcher { 647 public void dispatchOnStop(); 648 public void dispatchOnSuccess(); 649 public void dispatchOnStart(); 650 public void dispatchOnError(int errorCode); 651 } 652 653 /** Set of parameters affecting audio output. */ 654 static class AudioOutputParams { 655 /** 656 * Audio session identifier. May be used to associate audio playback with one of the 657 * {@link android.media.audiofx.AudioEffect} objects. If not specified by client, 658 * it should be equal to {@link AudioSystem#AUDIO_SESSION_ALLOCATE}. 659 */ 660 public final int mSessionId; 661 662 /** 663 * Volume, in the range [0.0f, 1.0f]. The default value is 664 * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f). 665 */ 666 public final float mVolume; 667 668 /** 669 * Left/right position of the audio, in the range [-1.0f, 1.0f]. 670 * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f). 671 */ 672 public final float mPan; 673 674 675 /** 676 * Audio attributes, set by {@link TextToSpeech#setAudioAttributes} 677 * or created from the value of {@link TextToSpeech.Engine#KEY_PARAM_STREAM}. 678 */ 679 public final AudioAttributes mAudioAttributes; 680 681 /** Create AudioOutputParams with default values */ 682 AudioOutputParams() { 683 mSessionId = AudioSystem.AUDIO_SESSION_ALLOCATE; 684 mVolume = Engine.DEFAULT_VOLUME; 685 mPan = Engine.DEFAULT_PAN; 686 mAudioAttributes = null; 687 } 688 689 AudioOutputParams(int sessionId, float volume, float pan, 690 AudioAttributes audioAttributes) { 691 mSessionId = sessionId; 692 mVolume = volume; 693 mPan = pan; 694 mAudioAttributes = audioAttributes; 695 } 696 697 /** Create AudioOutputParams from A {@link SynthesisRequest#getParams()} bundle */ 698 static AudioOutputParams createFromV1ParamsBundle(Bundle paramsBundle, 699 boolean isSpeech) { 700 if (paramsBundle == null) { 701 return new AudioOutputParams(); 702 } 703 704 AudioAttributes audioAttributes = 705 (AudioAttributes) paramsBundle.getParcelable( 706 Engine.KEY_PARAM_AUDIO_ATTRIBUTES); 707 if (audioAttributes == null) { 708 int streamType = paramsBundle.getInt( 709 Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); 710 audioAttributes = (new AudioAttributes.Builder()) 711 .setLegacyStreamType(streamType) 712 .setContentType((isSpeech ? 713 AudioAttributes.CONTENT_TYPE_SPEECH : 714 AudioAttributes.CONTENT_TYPE_SONIFICATION)) 715 .build(); 716 } 717 718 return new AudioOutputParams( 719 paramsBundle.getInt( 720 Engine.KEY_PARAM_SESSION_ID, 721 AudioSystem.AUDIO_SESSION_ALLOCATE), 722 paramsBundle.getFloat( 723 Engine.KEY_PARAM_VOLUME, 724 Engine.DEFAULT_VOLUME), 725 paramsBundle.getFloat( 726 Engine.KEY_PARAM_PAN, 727 Engine.DEFAULT_PAN), 728 audioAttributes); 729 } 730 } 731 732 733 /** 734 * An item in the synth thread queue. 735 */ 736 private abstract class SpeechItem { 737 private final Object mCallerIdentity; 738 private final int mCallerUid; 739 private final int mCallerPid; 740 private boolean mStarted = false; 741 private boolean mStopped = false; 742 743 public SpeechItem(Object caller, int callerUid, int callerPid) { 744 mCallerIdentity = caller; 745 mCallerUid = callerUid; 746 mCallerPid = callerPid; 747 } 748 749 public Object getCallerIdentity() { 750 return mCallerIdentity; 751 } 752 753 public int getCallerUid() { 754 return mCallerUid; 755 } 756 757 public int getCallerPid() { 758 return mCallerPid; 759 } 760 761 /** 762 * Checker whether the item is valid. If this method returns false, the item should not 763 * be played. 764 */ 765 public abstract boolean isValid(); 766 767 /** 768 * Plays the speech item. Blocks until playback is finished. 769 * Must not be called more than once. 770 * 771 * Only called on the synthesis thread. 772 */ 773 public void play() { 774 synchronized (this) { 775 if (mStarted) { 776 throw new IllegalStateException("play() called twice"); 777 } 778 mStarted = true; 779 } 780 playImpl(); 781 } 782 783 protected abstract void playImpl(); 784 785 /** 786 * Stops the speech item. 787 * Must not be called more than once. 788 * 789 * Can be called on multiple threads, but not on the synthesis thread. 790 */ 791 public void stop() { 792 synchronized (this) { 793 if (mStopped) { 794 throw new IllegalStateException("stop() called twice"); 795 } 796 mStopped = true; 797 } 798 stopImpl(); 799 } 800 801 protected abstract void stopImpl(); 802 803 protected synchronized boolean isStopped() { 804 return mStopped; 805 } 806 807 protected synchronized boolean isStarted() { 808 return mStarted; 809 } 810 } 811 812 /** 813 * An item in the synth thread queue that process utterance (and call back to client about 814 * progress). 815 */ 816 private abstract class UtteranceSpeechItem extends SpeechItem 817 implements UtteranceProgressDispatcher { 818 819 public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) { 820 super(caller, callerUid, callerPid); 821 } 822 823 @Override 824 public void dispatchOnSuccess() { 825 final String utteranceId = getUtteranceId(); 826 if (utteranceId != null) { 827 mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId); 828 } 829 } 830 831 @Override 832 public void dispatchOnStop() { 833 final String utteranceId = getUtteranceId(); 834 if (utteranceId != null) { 835 mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted()); 836 } 837 } 838 839 @Override 840 public void dispatchOnStart() { 841 final String utteranceId = getUtteranceId(); 842 if (utteranceId != null) { 843 mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId); 844 } 845 } 846 847 @Override 848 public void dispatchOnError(int errorCode) { 849 final String utteranceId = getUtteranceId(); 850 if (utteranceId != null) { 851 mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode); 852 } 853 } 854 855 abstract public String getUtteranceId(); 856 857 String getStringParam(Bundle params, String key, String defaultValue) { 858 return params == null ? defaultValue : params.getString(key, defaultValue); 859 } 860 861 int getIntParam(Bundle params, String key, int defaultValue) { 862 return params == null ? defaultValue : params.getInt(key, defaultValue); 863 } 864 865 float getFloatParam(Bundle params, String key, float defaultValue) { 866 return params == null ? defaultValue : params.getFloat(key, defaultValue); 867 } 868 } 869 870 /** 871 * UtteranceSpeechItem for V1 API speech items. V1 API speech items keep 872 * synthesis parameters in a single Bundle passed as parameter. This class 873 * allow subclasses to access them conveniently. 874 */ 875 private abstract class SpeechItemV1 extends UtteranceSpeechItem { 876 protected final Bundle mParams; 877 protected final String mUtteranceId; 878 879 SpeechItemV1(Object callerIdentity, int callerUid, int callerPid, 880 Bundle params, String utteranceId) { 881 super(callerIdentity, callerUid, callerPid); 882 mParams = params; 883 mUtteranceId = utteranceId; 884 } 885 886 boolean hasLanguage() { 887 return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null)); 888 } 889 890 int getSpeechRate() { 891 return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); 892 } 893 894 int getPitch() { 895 return getIntParam(mParams, Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); 896 } 897 898 @Override 899 public String getUtteranceId() { 900 return mUtteranceId; 901 } 902 903 AudioOutputParams getAudioParams() { 904 return AudioOutputParams.createFromV1ParamsBundle(mParams, true); 905 } 906 } 907 908 class SynthesisSpeechItemV1 extends SpeechItemV1 { 909 // Never null. 910 private final CharSequence mText; 911 private final SynthesisRequest mSynthesisRequest; 912 private final String[] mDefaultLocale; 913 // Non null after synthesis has started, and all accesses 914 // guarded by 'this'. 915 private AbstractSynthesisCallback mSynthesisCallback; 916 private final EventLoggerV1 mEventLogger; 917 private final int mCallerUid; 918 919 public SynthesisSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, 920 Bundle params, String utteranceId, CharSequence text) { 921 super(callerIdentity, callerUid, callerPid, params, utteranceId); 922 mText = text; 923 mCallerUid = callerUid; 924 mSynthesisRequest = new SynthesisRequest(mText, mParams); 925 mDefaultLocale = getSettingsLocale(); 926 setRequestParams(mSynthesisRequest); 927 mEventLogger = new EventLoggerV1(mSynthesisRequest, callerUid, callerPid, 928 mPackageName); 929 } 930 931 public CharSequence getText() { 932 return mText; 933 } 934 935 @Override 936 public boolean isValid() { 937 if (mText == null) { 938 Log.e(TAG, "null synthesis text"); 939 return false; 940 } 941 if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) { 942 Log.w(TAG, "Text too long: " + mText.length() + " chars"); 943 return false; 944 } 945 return true; 946 } 947 948 @Override 949 protected void playImpl() { 950 AbstractSynthesisCallback synthesisCallback; 951 mEventLogger.onRequestProcessingStart(); 952 synchronized (this) { 953 // stop() might have been called before we enter this 954 // synchronized block. 955 if (isStopped()) { 956 return; 957 } 958 mSynthesisCallback = createSynthesisCallback(); 959 synthesisCallback = mSynthesisCallback; 960 } 961 962 TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); 963 964 // Fix for case where client called .start() & .error(), but did not called .done() 965 if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) { 966 synthesisCallback.done(); 967 } 968 } 969 970 protected AbstractSynthesisCallback createSynthesisCallback() { 971 return new PlaybackSynthesisCallback(getAudioParams(), 972 mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false); 973 } 974 975 private void setRequestParams(SynthesisRequest request) { 976 String voiceName = getVoiceName(); 977 request.setLanguage(getLanguage(), getCountry(), getVariant()); 978 if (!TextUtils.isEmpty(voiceName)) { 979 request.setVoiceName(getVoiceName()); 980 } 981 request.setSpeechRate(getSpeechRate()); 982 request.setCallerUid(mCallerUid); 983 request.setPitch(getPitch()); 984 } 985 986 @Override 987 protected void stopImpl() { 988 AbstractSynthesisCallback synthesisCallback; 989 synchronized (this) { 990 synthesisCallback = mSynthesisCallback; 991 } 992 if (synthesisCallback != null) { 993 // If the synthesis callback is null, it implies that we haven't 994 // entered the synchronized(this) block in playImpl which in 995 // turn implies that synthesis would not have started. 996 synthesisCallback.stop(); 997 TextToSpeechService.this.onStop(); 998 } else { 999 dispatchOnStop(); 1000 } 1001 } 1002 1003 private String getCountry() { 1004 if (!hasLanguage()) return mDefaultLocale[1]; 1005 return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, ""); 1006 } 1007 1008 private String getVariant() { 1009 if (!hasLanguage()) return mDefaultLocale[2]; 1010 return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, ""); 1011 } 1012 1013 public String getLanguage() { 1014 return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); 1015 } 1016 1017 public String getVoiceName() { 1018 return getStringParam(mParams, Engine.KEY_PARAM_VOICE_NAME, ""); 1019 } 1020 } 1021 1022 private class SynthesisToFileOutputStreamSpeechItemV1 extends SynthesisSpeechItemV1 { 1023 private final FileOutputStream mFileOutputStream; 1024 1025 public SynthesisToFileOutputStreamSpeechItemV1(Object callerIdentity, int callerUid, 1026 int callerPid, Bundle params, String utteranceId, CharSequence text, 1027 FileOutputStream fileOutputStream) { 1028 super(callerIdentity, callerUid, callerPid, params, utteranceId, text); 1029 mFileOutputStream = fileOutputStream; 1030 } 1031 1032 @Override 1033 protected AbstractSynthesisCallback createSynthesisCallback() { 1034 return new FileSynthesisCallback(mFileOutputStream.getChannel(), 1035 this, getCallerIdentity(), false); 1036 } 1037 1038 @Override 1039 protected void playImpl() { 1040 dispatchOnStart(); 1041 super.playImpl(); 1042 try { 1043 mFileOutputStream.close(); 1044 } catch(IOException e) { 1045 Log.w(TAG, "Failed to close output file", e); 1046 } 1047 } 1048 } 1049 1050 private class AudioSpeechItemV1 extends SpeechItemV1 { 1051 private final AudioPlaybackQueueItem mItem; 1052 1053 public AudioSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, 1054 Bundle params, String utteranceId, Uri uri) { 1055 super(callerIdentity, callerUid, callerPid, params, utteranceId); 1056 mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), 1057 TextToSpeechService.this, uri, getAudioParams()); 1058 } 1059 1060 @Override 1061 public boolean isValid() { 1062 return true; 1063 } 1064 1065 @Override 1066 protected void playImpl() { 1067 mAudioPlaybackHandler.enqueue(mItem); 1068 } 1069 1070 @Override 1071 protected void stopImpl() { 1072 // Do nothing. 1073 } 1074 1075 @Override 1076 public String getUtteranceId() { 1077 return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null); 1078 } 1079 1080 @Override 1081 AudioOutputParams getAudioParams() { 1082 return AudioOutputParams.createFromV1ParamsBundle(mParams, false); 1083 } 1084 } 1085 1086 private class SilenceSpeechItem extends UtteranceSpeechItem { 1087 private final long mDuration; 1088 private final String mUtteranceId; 1089 1090 public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, 1091 String utteranceId, long duration) { 1092 super(callerIdentity, callerUid, callerPid); 1093 mUtteranceId = utteranceId; 1094 mDuration = duration; 1095 } 1096 1097 @Override 1098 public boolean isValid() { 1099 return true; 1100 } 1101 1102 @Override 1103 protected void playImpl() { 1104 mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( 1105 this, getCallerIdentity(), mDuration)); 1106 } 1107 1108 @Override 1109 protected void stopImpl() { 1110 1111 } 1112 1113 @Override 1114 public String getUtteranceId() { 1115 return mUtteranceId; 1116 } 1117 } 1118 1119 /** 1120 * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. 1121 */ 1122 private class LoadLanguageItem extends SpeechItem { 1123 private final String mLanguage; 1124 private final String mCountry; 1125 private final String mVariant; 1126 1127 public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, 1128 String language, String country, String variant) { 1129 super(callerIdentity, callerUid, callerPid); 1130 mLanguage = language; 1131 mCountry = country; 1132 mVariant = variant; 1133 } 1134 1135 @Override 1136 public boolean isValid() { 1137 return true; 1138 } 1139 1140 @Override 1141 protected void playImpl() { 1142 TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); 1143 } 1144 1145 @Override 1146 protected void stopImpl() { 1147 // No-op 1148 } 1149 } 1150 1151 /** 1152 * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. 1153 */ 1154 private class LoadVoiceItem extends SpeechItem { 1155 private final String mVoiceName; 1156 1157 public LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid, 1158 String voiceName) { 1159 super(callerIdentity, callerUid, callerPid); 1160 mVoiceName = voiceName; 1161 } 1162 1163 @Override 1164 public boolean isValid() { 1165 return true; 1166 } 1167 1168 @Override 1169 protected void playImpl() { 1170 TextToSpeechService.this.onLoadVoice(mVoiceName); 1171 } 1172 1173 @Override 1174 protected void stopImpl() { 1175 // No-op 1176 } 1177 } 1178 1179 1180 @Override 1181 public IBinder onBind(Intent intent) { 1182 if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { 1183 return mBinder; 1184 } 1185 return null; 1186 } 1187 1188 /** 1189 * Binder returned from {@code #onBind(Intent)}. The methods in this class can be 1190 * called called from several different threads. 1191 */ 1192 // NOTE: All calls that are passed in a calling app are interned so that 1193 // they can be used as message objects (which are tested for equality using ==). 1194 private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { 1195 @Override 1196 public int speak(IBinder caller, CharSequence text, int queueMode, Bundle params, 1197 String utteranceId) { 1198 if (!checkNonNull(caller, text, params)) { 1199 return TextToSpeech.ERROR; 1200 } 1201 1202 SpeechItem item = new SynthesisSpeechItemV1(caller, 1203 Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text); 1204 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1205 } 1206 1207 @Override 1208 public int synthesizeToFileDescriptor(IBinder caller, CharSequence text, ParcelFileDescriptor 1209 fileDescriptor, Bundle params, String utteranceId) { 1210 if (!checkNonNull(caller, text, fileDescriptor, params)) { 1211 return TextToSpeech.ERROR; 1212 } 1213 1214 // In test env, ParcelFileDescriptor instance may be EXACTLY the same 1215 // one that is used by client. And it will be closed by a client, thus 1216 // preventing us from writing anything to it. 1217 final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd( 1218 fileDescriptor.detachFd()); 1219 1220 SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV1(caller, 1221 Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text, 1222 new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor)); 1223 return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); 1224 } 1225 1226 @Override 1227 public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params, 1228 String utteranceId) { 1229 if (!checkNonNull(caller, audioUri, params)) { 1230 return TextToSpeech.ERROR; 1231 } 1232 1233 SpeechItem item = new AudioSpeechItemV1(caller, 1234 Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, audioUri); 1235 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1236 } 1237 1238 @Override 1239 public int playSilence(IBinder caller, long duration, int queueMode, String utteranceId) { 1240 if (!checkNonNull(caller)) { 1241 return TextToSpeech.ERROR; 1242 } 1243 1244 SpeechItem item = new SilenceSpeechItem(caller, 1245 Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, duration); 1246 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1247 } 1248 1249 @Override 1250 public boolean isSpeaking() { 1251 return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); 1252 } 1253 1254 @Override 1255 public int stop(IBinder caller) { 1256 if (!checkNonNull(caller)) { 1257 return TextToSpeech.ERROR; 1258 } 1259 1260 return mSynthHandler.stopForApp(caller); 1261 } 1262 1263 @Override 1264 public String[] getLanguage() { 1265 return onGetLanguage(); 1266 } 1267 1268 @Override 1269 public String[] getClientDefaultLanguage() { 1270 return getSettingsLocale(); 1271 } 1272 1273 /* 1274 * If defaults are enforced, then no language is "available" except 1275 * perhaps the default language selected by the user. 1276 */ 1277 @Override 1278 public int isLanguageAvailable(String lang, String country, String variant) { 1279 if (!checkNonNull(lang)) { 1280 return TextToSpeech.ERROR; 1281 } 1282 1283 return onIsLanguageAvailable(lang, country, variant); 1284 } 1285 1286 @Override 1287 public String[] getFeaturesForLanguage(String lang, String country, String variant) { 1288 Set<String> features = onGetFeaturesForLanguage(lang, country, variant); 1289 String[] featuresArray = null; 1290 if (features != null) { 1291 featuresArray = new String[features.size()]; 1292 features.toArray(featuresArray); 1293 } else { 1294 featuresArray = new String[0]; 1295 } 1296 return featuresArray; 1297 } 1298 1299 /* 1300 * There is no point loading a non default language if defaults 1301 * are enforced. 1302 */ 1303 @Override 1304 public int loadLanguage(IBinder caller, String lang, String country, String variant) { 1305 if (!checkNonNull(lang)) { 1306 return TextToSpeech.ERROR; 1307 } 1308 int retVal = onIsLanguageAvailable(lang, country, variant); 1309 1310 if (retVal == TextToSpeech.LANG_AVAILABLE || 1311 retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE || 1312 retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 1313 1314 SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(), 1315 Binder.getCallingPid(), lang, country, variant); 1316 1317 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) != 1318 TextToSpeech.SUCCESS) { 1319 return TextToSpeech.ERROR; 1320 } 1321 } 1322 return retVal; 1323 } 1324 1325 @Override 1326 public List<Voice> getVoices() { 1327 return onGetVoices(); 1328 } 1329 1330 @Override 1331 public int loadVoice(IBinder caller, String voiceName) { 1332 if (!checkNonNull(voiceName)) { 1333 return TextToSpeech.ERROR; 1334 } 1335 int retVal = onIsValidVoiceName(voiceName); 1336 1337 if (retVal == TextToSpeech.SUCCESS) { 1338 SpeechItem item = new LoadVoiceItem(caller, Binder.getCallingUid(), 1339 Binder.getCallingPid(), voiceName); 1340 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) != 1341 TextToSpeech.SUCCESS) { 1342 return TextToSpeech.ERROR; 1343 } 1344 } 1345 return retVal; 1346 } 1347 1348 public String getDefaultVoiceNameFor(String lang, String country, String variant) { 1349 if (!checkNonNull(lang)) { 1350 return null; 1351 } 1352 int retVal = onIsLanguageAvailable(lang, country, variant); 1353 1354 if (retVal == TextToSpeech.LANG_AVAILABLE || 1355 retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE || 1356 retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 1357 return onGetDefaultVoiceNameFor(lang, country, variant); 1358 } else { 1359 return null; 1360 } 1361 } 1362 1363 @Override 1364 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 1365 // Note that passing in a null callback is a valid use case. 1366 if (!checkNonNull(caller)) { 1367 return; 1368 } 1369 1370 mCallbacks.setCallback(caller, cb); 1371 } 1372 1373 private String intern(String in) { 1374 // The input parameter will be non null. 1375 return in.intern(); 1376 } 1377 1378 private boolean checkNonNull(Object... args) { 1379 for (Object o : args) { 1380 if (o == null) return false; 1381 } 1382 return true; 1383 } 1384 }; 1385 1386 private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { 1387 private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback 1388 = new HashMap<IBinder, ITextToSpeechCallback>(); 1389 1390 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 1391 synchronized (mCallerToCallback) { 1392 ITextToSpeechCallback old; 1393 if (cb != null) { 1394 register(cb, caller); 1395 old = mCallerToCallback.put(caller, cb); 1396 } else { 1397 old = mCallerToCallback.remove(caller); 1398 } 1399 if (old != null && old != cb) { 1400 unregister(old); 1401 } 1402 } 1403 } 1404 1405 public void dispatchOnStop(Object callerIdentity, String utteranceId, boolean started) { 1406 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1407 if (cb == null) return; 1408 try { 1409 cb.onStop(utteranceId, started); 1410 } catch (RemoteException e) { 1411 Log.e(TAG, "Callback onStop failed: " + e); 1412 } 1413 } 1414 1415 public void dispatchOnSuccess(Object callerIdentity, String utteranceId) { 1416 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1417 if (cb == null) return; 1418 try { 1419 cb.onSuccess(utteranceId); 1420 } catch (RemoteException e) { 1421 Log.e(TAG, "Callback onDone failed: " + e); 1422 } 1423 } 1424 1425 public void dispatchOnStart(Object callerIdentity, String utteranceId) { 1426 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1427 if (cb == null) return; 1428 try { 1429 cb.onStart(utteranceId); 1430 } catch (RemoteException e) { 1431 Log.e(TAG, "Callback onStart failed: " + e); 1432 } 1433 1434 } 1435 1436 public void dispatchOnError(Object callerIdentity, String utteranceId, 1437 int errorCode) { 1438 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1439 if (cb == null) return; 1440 try { 1441 cb.onError(utteranceId, errorCode); 1442 } catch (RemoteException e) { 1443 Log.e(TAG, "Callback onError failed: " + e); 1444 } 1445 } 1446 1447 @Override 1448 public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { 1449 IBinder caller = (IBinder) cookie; 1450 synchronized (mCallerToCallback) { 1451 mCallerToCallback.remove(caller); 1452 } 1453 //mSynthHandler.stopForApp(caller); 1454 } 1455 1456 @Override 1457 public void kill() { 1458 synchronized (mCallerToCallback) { 1459 mCallerToCallback.clear(); 1460 super.kill(); 1461 } 1462 } 1463 1464 private ITextToSpeechCallback getCallbackFor(Object caller) { 1465 ITextToSpeechCallback cb; 1466 IBinder asBinder = (IBinder) caller; 1467 synchronized (mCallerToCallback) { 1468 cb = mCallerToCallback.get(asBinder); 1469 } 1470 1471 return cb; 1472 } 1473 } 1474} 1475