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