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