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