TextToSpeechService.java revision 453c13f77121641a52d8b422a527feb7b3d3b8ab
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.HashMap; 43import java.util.HashSet; 44import java.util.List; 45import java.util.Locale; 46import java.util.MissingResourceException; 47import 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 */ 103public 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(), this, 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