TextToSpeechService.java revision ad6df74ada7c478257425b746588f22eeec199a6
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.AudioManager; 21import android.media.AudioSystem; 22import android.net.Uri; 23import android.os.Binder; 24import android.os.Bundle; 25import android.os.Handler; 26import android.os.HandlerThread; 27import android.os.IBinder; 28import android.os.Looper; 29import android.os.Message; 30import android.os.MessageQueue; 31import android.os.ParcelFileDescriptor; 32import android.os.RemoteCallbackList; 33import android.os.RemoteException; 34import android.provider.Settings; 35import android.speech.tts.TextToSpeech.Engine; 36import android.text.TextUtils; 37import android.util.Log; 38 39import java.io.FileOutputStream; 40import java.io.IOException; 41import java.util.ArrayList; 42import java.util.Collections; 43import java.util.HashMap; 44import java.util.List; 45import java.util.Locale; 46import java.util.Map; 47import java.util.MissingResourceException; 48import java.util.Set; 49 50 51/** 52 * Abstract base class for TTS engine implementations. The following methods 53 * need to be implemented: 54 * <ul> 55 * <li>{@link #onIsLanguageAvailable}</li> 56 * <li>{@link #onLoadLanguage}</li> 57 * <li>{@link #onGetLanguage}</li> 58 * <li>{@link #onSynthesizeText}</li> 59 * <li>{@link #onStop}</li> 60 * </ul> 61 * The first three deal primarily with language management, and are used to 62 * query the engine for it's support for a given language and indicate to it 63 * that requests in a given language are imminent. 64 * 65 * {@link #onSynthesizeText} is central to the engine implementation. The 66 * implementation should synthesize text as per the request parameters and 67 * return synthesized data via the supplied callback. This class and its helpers 68 * will then consume that data, which might mean queuing it for playback or writing 69 * it to a file or similar. All calls to this method will be on a single thread, 70 * which will be different from the main thread of the service. Synthesis must be 71 * synchronous which means the engine must NOT hold on to the callback or call any 72 * methods on it after the method returns. 73 * 74 * {@link #onStop} tells the engine that it should stop 75 * all ongoing synthesis, if any. Any pending data from the current synthesis 76 * will be discarded. 77 * 78 * {@link #onGetLanguage} is not required as of JELLYBEAN_MR2 (API 18) and later, it is only 79 * called on earlier versions of Android. 80 * 81 * API Level 20 adds support for Voice objects. Voices are an abstraction that allow the TTS 82 * service to expose multiple backends for a single locale. Each one of them can have a different 83 * features set. In order to fully take advantage of voices, an engine should implement 84 * the following methods: 85 * <ul> 86 * <li>{@link #onGetVoices()}</li> 87 * <li>{@link #isValidVoiceName(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 20 {@link TextToSpeech#setLanguage} is implemented by 95 * calling {@link TextToSpeech#setVoice} with the voice returned by 96 * {@link #onGetDefaultVoiceNameFor(String, String, String)}. 97 * 98 * If the client uses a voice instead of a locale, {@link SynthesisRequest} will contain the 99 * requested voice name. 100 * 101 * The default implementations of Voice-related methods implement them using the 102 * pre-existing locale-based implementation. 103 */ 104public abstract class TextToSpeechService extends Service { 105 106 private static final boolean DBG = false; 107 private static final String TAG = "TextToSpeechService"; 108 109 private static final String SYNTH_THREAD_NAME = "SynthThread"; 110 111 private SynthHandler mSynthHandler; 112 // A thread and it's associated handler for playing back any audio 113 // associated with this TTS engine. Will handle all requests except synthesis 114 // to file requests, which occur on the synthesis thread. 115 private AudioPlaybackHandler mAudioPlaybackHandler; 116 private TtsEngines mEngineHelper; 117 118 private CallbackMap mCallbacks; 119 private String mPackageName; 120 121 private final Object mVoicesInfoLock = new Object(); 122 123 @Override 124 public void onCreate() { 125 if (DBG) Log.d(TAG, "onCreate()"); 126 super.onCreate(); 127 128 SynthThread synthThread = new SynthThread(); 129 synthThread.start(); 130 mSynthHandler = new SynthHandler(synthThread.getLooper()); 131 132 mAudioPlaybackHandler = new AudioPlaybackHandler(); 133 mAudioPlaybackHandler.start(); 134 135 mEngineHelper = new TtsEngines(this); 136 137 mCallbacks = new CallbackMap(); 138 139 mPackageName = getApplicationInfo().packageName; 140 141 String[] defaultLocale = getSettingsLocale(); 142 143 // Load default language 144 onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]); 145 } 146 147 @Override 148 public void onDestroy() { 149 if (DBG) Log.d(TAG, "onDestroy()"); 150 151 // Tell the synthesizer to stop 152 mSynthHandler.quit(); 153 // Tell the audio playback thread to stop. 154 mAudioPlaybackHandler.quit(); 155 // Unregister all callbacks. 156 mCallbacks.kill(); 157 158 super.onDestroy(); 159 } 160 161 /** 162 * Checks whether the engine supports a given language. 163 * 164 * Can be called on multiple threads. 165 * 166 * Its return values HAVE to be consistent with onLoadLanguage. 167 * 168 * @param lang ISO-3 language code. 169 * @param country ISO-3 country code. May be empty or null. 170 * @param variant Language variant. May be empty or null. 171 * @return Code indicating the support status for the locale. 172 * One of {@link TextToSpeech#LANG_AVAILABLE}, 173 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 174 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 175 * {@link TextToSpeech#LANG_MISSING_DATA} 176 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 177 */ 178 protected abstract int onIsLanguageAvailable(String lang, String country, String variant); 179 180 /** 181 * Returns the language, country and variant currently being used by the TTS engine. 182 * 183 * This method will be called only on Android 4.2 and before (API <= 17). In later versions 184 * this method is not called by the Android TTS framework. 185 * 186 * Can be called on multiple threads. 187 * 188 * @return A 3-element array, containing language (ISO 3-letter code), 189 * country (ISO 3-letter code) and variant used by the engine. 190 * The country and variant may be {@code ""}. If country is empty, then variant must 191 * be empty too. 192 * @see Locale#getISO3Language() 193 * @see Locale#getISO3Country() 194 * @see Locale#getVariant() 195 */ 196 protected abstract String[] onGetLanguage(); 197 198 /** 199 * Notifies the engine that it should load a speech synthesis language. There is no guarantee 200 * that this method is always called before the language is used for synthesis. It is merely 201 * a hint to the engine that it will probably get some synthesis requests for this language 202 * at some point in the future. 203 * 204 * Can be called on multiple threads. 205 * In <= Android 4.2 (<= API 17) can be called on main and service binder threads. 206 * In > Android 4.2 (> API 17) can be called on main and synthesis threads. 207 * 208 * @param lang ISO-3 language code. 209 * @param country ISO-3 country code. May be empty or null. 210 * @param variant Language variant. May be empty or null. 211 * @return Code indicating the support status for the locale. 212 * One of {@link TextToSpeech#LANG_AVAILABLE}, 213 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 214 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 215 * {@link TextToSpeech#LANG_MISSING_DATA} 216 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 217 */ 218 protected abstract int onLoadLanguage(String lang, String country, String variant); 219 220 /** 221 * Notifies the service that it should stop any in-progress speech synthesis. 222 * This method can be called even if no speech synthesis is currently in progress. 223 * 224 * Can be called on multiple threads, but not on the synthesis thread. 225 */ 226 protected abstract void onStop(); 227 228 /** 229 * Tells the service to synthesize speech from the given text. This method 230 * should block until the synthesis is finished. Used for requests from V1 231 * clients ({@link android.speech.tts.TextToSpeech}). Called on the synthesis 232 * thread. 233 * 234 * @param request The synthesis request. 235 * @param callback The callback that the engine must use to make data 236 * available for playback or for writing to a file. 237 */ 238 protected abstract void onSynthesizeText(SynthesisRequest request, 239 SynthesisCallback callback); 240 241 /** 242 * Queries the service for a set of features supported for a given language. 243 * 244 * Can be called on multiple threads. 245 * 246 * @param lang ISO-3 language code. 247 * @param country ISO-3 country code. May be empty or null. 248 * @param variant Language variant. May be empty or null. 249 * @return A list of features supported for the given language. 250 */ 251 protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) { 252 return null; 253 } 254 255 private int getExpectedLanguageAvailableStatus(Locale locale) { 256 int expectedStatus = TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE; 257 if (locale.getVariant().isEmpty()) { 258 if (locale.getCountry().isEmpty()) { 259 expectedStatus = TextToSpeech.LANG_AVAILABLE; 260 } else { 261 expectedStatus = TextToSpeech.LANG_COUNTRY_AVAILABLE; 262 } 263 } 264 return expectedStatus; 265 } 266 267 /** 268 * Queries the service for a set of supported voices. 269 * 270 * Can be called on multiple threads. 271 * 272 * The default implementation tries to enumerate all available locales, pass them to 273 * {@link #onIsLanguageAvailable(String, String, String)} and create Voice instances (using 274 * the locale's BCP-47 language tag as the voice name) for the ones that are supported. 275 * Note, that this implementation is suitable only for engines that don't have multiple voices 276 * for a single locale. Also, this implementation won't work with Locales not listed in the 277 * set returned by the {@link Locale#getAvailableLocales()} method. 278 * 279 * @return A list of voices supported. 280 */ 281 protected 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 protected 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 (isValidVoiceName(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 protected 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 protected int isValidVoiceName(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 dispatchOnFallback(); 596 public void dispatchOnStop(); 597 public void dispatchOnSuccess(); 598 public void dispatchOnStart(); 599 public void dispatchOnError(int errorCode); 600 } 601 602 603 /** Set of parameters affecting audio output. */ 604 static class AudioOutputParams { 605 /** 606 * Audio session identifier. May be used to associate audio playback with one of the 607 * {@link android.media.audiofx.AudioEffect} objects. If not specified by client, 608 * it should be equal to {@link AudioSystem#AUDIO_SESSION_ALLOCATE}. 609 */ 610 public final int mSessionId; 611 612 /** 613 * Audio stream type. Must be one of the STREAM_ contants defined in 614 * {@link android.media.AudioManager}. 615 */ 616 public final int mStreamType; 617 618 /** 619 * Volume, in the range [0.0f, 1.0f]. The default value is 620 * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f). 621 */ 622 public final float mVolume; 623 624 /** 625 * Left/right position of the audio, in the range [-1.0f, 1.0f]. 626 * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f). 627 */ 628 public final float mPan; 629 630 /** Create AudioOutputParams with default values */ 631 AudioOutputParams() { 632 mSessionId = AudioSystem.AUDIO_SESSION_ALLOCATE; 633 mStreamType = Engine.DEFAULT_STREAM; 634 mVolume = Engine.DEFAULT_VOLUME; 635 mPan = Engine.DEFAULT_PAN; 636 } 637 638 AudioOutputParams(int sessionId, int streamType, float volume, float pan) { 639 mSessionId = sessionId; 640 mStreamType = streamType; 641 mVolume = volume; 642 mPan = pan; 643 } 644 645 /** Create AudioOutputParams from A {@link SynthesisRequest#getParams()} bundle */ 646 static AudioOutputParams createFromV1ParamsBundle(Bundle paramsBundle) { 647 if (paramsBundle == null) { 648 return new AudioOutputParams(); 649 } 650 651 return new AudioOutputParams( 652 paramsBundle.getInt( 653 Engine.KEY_PARAM_SESSION_ID, 654 AudioSystem.AUDIO_SESSION_ALLOCATE), 655 paramsBundle.getInt( 656 Engine.KEY_PARAM_STREAM, 657 Engine.DEFAULT_STREAM), 658 paramsBundle.getFloat( 659 Engine.KEY_PARAM_VOLUME, 660 Engine.DEFAULT_VOLUME), 661 paramsBundle.getFloat( 662 Engine.KEY_PARAM_PAN, 663 Engine.DEFAULT_PAN)); 664 } 665 666 } 667 668 669 /** 670 * An item in the synth thread queue. 671 */ 672 private abstract class SpeechItem { 673 private final Object mCallerIdentity; 674 private final int mCallerUid; 675 private final int mCallerPid; 676 private boolean mStarted = false; 677 private boolean mStopped = false; 678 679 public SpeechItem(Object caller, int callerUid, int callerPid) { 680 mCallerIdentity = caller; 681 mCallerUid = callerUid; 682 mCallerPid = callerPid; 683 } 684 685 public Object getCallerIdentity() { 686 return mCallerIdentity; 687 } 688 689 690 public int getCallerUid() { 691 return mCallerUid; 692 } 693 694 public int getCallerPid() { 695 return mCallerPid; 696 } 697 698 /** 699 * Checker whether the item is valid. If this method returns false, the item should not 700 * be played. 701 */ 702 public abstract boolean isValid(); 703 704 /** 705 * Plays the speech item. Blocks until playback is finished. 706 * Must not be called more than once. 707 * 708 * Only called on the synthesis thread. 709 */ 710 public void play() { 711 synchronized (this) { 712 if (mStarted) { 713 throw new IllegalStateException("play() called twice"); 714 } 715 mStarted = true; 716 } 717 playImpl(); 718 } 719 720 protected abstract void playImpl(); 721 722 /** 723 * Stops the speech item. 724 * Must not be called more than once. 725 * 726 * Can be called on multiple threads, but not on the synthesis thread. 727 */ 728 public void stop() { 729 synchronized (this) { 730 if (mStopped) { 731 throw new IllegalStateException("stop() called twice"); 732 } 733 mStopped = true; 734 } 735 stopImpl(); 736 } 737 738 protected abstract void stopImpl(); 739 740 protected synchronized boolean isStopped() { 741 return mStopped; 742 } 743 } 744 745 /** 746 * An item in the synth thread queue that process utterance (and call back to client about 747 * progress). 748 */ 749 private abstract class UtteranceSpeechItem extends SpeechItem 750 implements UtteranceProgressDispatcher { 751 752 public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) { 753 super(caller, callerUid, callerPid); 754 } 755 756 @Override 757 public void dispatchOnSuccess() { 758 final String utteranceId = getUtteranceId(); 759 if (utteranceId != null) { 760 mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId); 761 } 762 } 763 764 @Override 765 public void dispatchOnStop() { 766 final String utteranceId = getUtteranceId(); 767 if (utteranceId != null) { 768 mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId); 769 } 770 } 771 772 @Override 773 public void dispatchOnFallback() { 774 final String utteranceId = getUtteranceId(); 775 if (utteranceId != null) { 776 mCallbacks.dispatchOnFallback(getCallerIdentity(), utteranceId); 777 } 778 } 779 780 @Override 781 public void dispatchOnStart() { 782 final String utteranceId = getUtteranceId(); 783 if (utteranceId != null) { 784 mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId); 785 } 786 } 787 788 @Override 789 public void dispatchOnError(int errorCode) { 790 final String utteranceId = getUtteranceId(); 791 if (utteranceId != null) { 792 mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode); 793 } 794 } 795 796 abstract public String getUtteranceId(); 797 798 String getStringParam(Bundle params, String key, String defaultValue) { 799 return params == null ? defaultValue : params.getString(key, defaultValue); 800 } 801 802 int getIntParam(Bundle params, String key, int defaultValue) { 803 return params == null ? defaultValue : params.getInt(key, defaultValue); 804 } 805 806 float getFloatParam(Bundle params, String key, float defaultValue) { 807 return params == null ? defaultValue : params.getFloat(key, defaultValue); 808 } 809 } 810 811 /** 812 * UtteranceSpeechItem for V1 API speech items. V1 API speech items keep 813 * synthesis parameters in a single Bundle passed as parameter. This class 814 * allow subclasses to access them conveniently. 815 */ 816 private abstract class SpeechItemV1 extends UtteranceSpeechItem { 817 protected final Bundle mParams; 818 protected final String mUtteranceId; 819 820 SpeechItemV1(Object callerIdentity, int callerUid, int callerPid, 821 Bundle params, String utteranceId) { 822 super(callerIdentity, callerUid, callerPid); 823 mParams = params; 824 mUtteranceId = utteranceId; 825 } 826 827 boolean hasLanguage() { 828 return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null)); 829 } 830 831 int getSpeechRate() { 832 return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); 833 } 834 835 int getPitch() { 836 return getIntParam(mParams, Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); 837 } 838 839 @Override 840 public String getUtteranceId() { 841 return mUtteranceId; 842 } 843 844 AudioOutputParams getAudioParams() { 845 return AudioOutputParams.createFromV1ParamsBundle(mParams); 846 } 847 } 848 849 class SynthesisSpeechItemV1 extends SpeechItemV1 { 850 // Never null. 851 private final CharSequence mText; 852 private final SynthesisRequest mSynthesisRequest; 853 private final String[] mDefaultLocale; 854 // Non null after synthesis has started, and all accesses 855 // guarded by 'this'. 856 private AbstractSynthesisCallback mSynthesisCallback; 857 private final EventLoggerV1 mEventLogger; 858 private final int mCallerUid; 859 860 public SynthesisSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, 861 Bundle params, String utteranceId, CharSequence text) { 862 super(callerIdentity, callerUid, callerPid, params, utteranceId); 863 mText = text; 864 mCallerUid = callerUid; 865 mSynthesisRequest = new SynthesisRequest(mText, mParams); 866 mDefaultLocale = getSettingsLocale(); 867 setRequestParams(mSynthesisRequest); 868 mEventLogger = new EventLoggerV1(mSynthesisRequest, callerUid, callerPid, 869 mPackageName); 870 } 871 872 public CharSequence getText() { 873 return mText; 874 } 875 876 @Override 877 public boolean isValid() { 878 if (mText == null) { 879 Log.e(TAG, "null synthesis text"); 880 return false; 881 } 882 if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) { 883 Log.w(TAG, "Text too long: " + mText.length() + " chars"); 884 return false; 885 } 886 return true; 887 } 888 889 @Override 890 protected void playImpl() { 891 AbstractSynthesisCallback synthesisCallback; 892 mEventLogger.onRequestProcessingStart(); 893 synchronized (this) { 894 // stop() might have been called before we enter this 895 // synchronized block. 896 if (isStopped()) { 897 return; 898 } 899 mSynthesisCallback = createSynthesisCallback(); 900 synthesisCallback = mSynthesisCallback; 901 } 902 903 TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); 904 905 // Fix for case where client called .start() & .error(), but did not called .done() 906 if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) { 907 synthesisCallback.done(); 908 } 909 } 910 911 protected AbstractSynthesisCallback createSynthesisCallback() { 912 return new PlaybackSynthesisCallback(getAudioParams(), 913 mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false); 914 } 915 916 private void setRequestParams(SynthesisRequest request) { 917 String voiceName = getVoiceName(); 918 request.setLanguage(getLanguage(), getCountry(), getVariant()); 919 if (!TextUtils.isEmpty(voiceName)) { 920 request.setVoiceName(getVoiceName()); 921 } 922 request.setSpeechRate(getSpeechRate()); 923 request.setCallerUid(mCallerUid); 924 request.setPitch(getPitch()); 925 } 926 927 @Override 928 protected void stopImpl() { 929 AbstractSynthesisCallback synthesisCallback; 930 synchronized (this) { 931 synthesisCallback = mSynthesisCallback; 932 } 933 if (synthesisCallback != null) { 934 // If the synthesis callback is null, it implies that we haven't 935 // entered the synchronized(this) block in playImpl which in 936 // turn implies that synthesis would not have started. 937 synthesisCallback.stop(); 938 TextToSpeechService.this.onStop(); 939 } 940 } 941 942 private String getCountry() { 943 if (!hasLanguage()) return mDefaultLocale[1]; 944 return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, ""); 945 } 946 947 private String getVariant() { 948 if (!hasLanguage()) return mDefaultLocale[2]; 949 return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, ""); 950 } 951 952 public String getLanguage() { 953 return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); 954 } 955 956 public String getVoiceName() { 957 return getStringParam(mParams, Engine.KEY_PARAM_VOICE_NAME, ""); 958 } 959 } 960 961 private class SynthesisToFileOutputStreamSpeechItemV1 extends SynthesisSpeechItemV1 { 962 private final FileOutputStream mFileOutputStream; 963 964 public SynthesisToFileOutputStreamSpeechItemV1(Object callerIdentity, int callerUid, 965 int callerPid, Bundle params, String utteranceId, CharSequence text, 966 FileOutputStream fileOutputStream) { 967 super(callerIdentity, callerUid, callerPid, params, utteranceId, text); 968 mFileOutputStream = fileOutputStream; 969 } 970 971 @Override 972 protected AbstractSynthesisCallback createSynthesisCallback() { 973 return new FileSynthesisCallback(mFileOutputStream.getChannel(), 974 this, getCallerIdentity(), false); 975 } 976 977 @Override 978 protected void playImpl() { 979 dispatchOnStart(); 980 super.playImpl(); 981 try { 982 mFileOutputStream.close(); 983 } catch(IOException e) { 984 Log.w(TAG, "Failed to close output file", e); 985 } 986 } 987 } 988 989 private class AudioSpeechItemV1 extends SpeechItemV1 { 990 private final AudioPlaybackQueueItem mItem; 991 992 public AudioSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, 993 Bundle params, String utteranceId, Uri uri) { 994 super(callerIdentity, callerUid, callerPid, params, utteranceId); 995 mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), 996 TextToSpeechService.this, uri, getAudioParams()); 997 } 998 999 @Override 1000 public boolean isValid() { 1001 return true; 1002 } 1003 1004 @Override 1005 protected void playImpl() { 1006 mAudioPlaybackHandler.enqueue(mItem); 1007 } 1008 1009 @Override 1010 protected void stopImpl() { 1011 // Do nothing. 1012 } 1013 1014 @Override 1015 public String getUtteranceId() { 1016 return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null); 1017 } 1018 } 1019 1020 private class SilenceSpeechItem extends UtteranceSpeechItem { 1021 private final long mDuration; 1022 private final String mUtteranceId; 1023 1024 public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, 1025 String utteranceId, long duration) { 1026 super(callerIdentity, callerUid, callerPid); 1027 mUtteranceId = utteranceId; 1028 mDuration = duration; 1029 } 1030 1031 @Override 1032 public boolean isValid() { 1033 return true; 1034 } 1035 1036 @Override 1037 protected void playImpl() { 1038 mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( 1039 this, getCallerIdentity(), mDuration)); 1040 } 1041 1042 @Override 1043 protected void stopImpl() { 1044 1045 } 1046 1047 @Override 1048 public String getUtteranceId() { 1049 return mUtteranceId; 1050 } 1051 } 1052 1053 /** 1054 * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. 1055 */ 1056 private class LoadLanguageItem extends SpeechItem { 1057 private final String mLanguage; 1058 private final String mCountry; 1059 private final String mVariant; 1060 1061 public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, 1062 String language, String country, String variant) { 1063 super(callerIdentity, callerUid, callerPid); 1064 mLanguage = language; 1065 mCountry = country; 1066 mVariant = variant; 1067 } 1068 1069 @Override 1070 public boolean isValid() { 1071 return true; 1072 } 1073 1074 @Override 1075 protected void playImpl() { 1076 TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); 1077 } 1078 1079 @Override 1080 protected void stopImpl() { 1081 // No-op 1082 } 1083 } 1084 1085 /** 1086 * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. 1087 */ 1088 private class LoadVoiceItem extends SpeechItem { 1089 private final String mVoiceName; 1090 1091 public LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid, 1092 String voiceName) { 1093 super(callerIdentity, callerUid, callerPid); 1094 mVoiceName = voiceName; 1095 } 1096 1097 @Override 1098 public boolean isValid() { 1099 return true; 1100 } 1101 1102 @Override 1103 protected void playImpl() { 1104 TextToSpeechService.this.onLoadVoice(mVoiceName); 1105 } 1106 1107 @Override 1108 protected void stopImpl() { 1109 // No-op 1110 } 1111 } 1112 1113 1114 @Override 1115 public IBinder onBind(Intent intent) { 1116 if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { 1117 return mBinder; 1118 } 1119 return null; 1120 } 1121 1122 /** 1123 * Binder returned from {@code #onBind(Intent)}. The methods in this class can be 1124 * called called from several different threads. 1125 */ 1126 // NOTE: All calls that are passed in a calling app are interned so that 1127 // they can be used as message objects (which are tested for equality using ==). 1128 private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { 1129 @Override 1130 public int speak(IBinder caller, CharSequence text, int queueMode, Bundle params, 1131 String utteranceId) { 1132 if (!checkNonNull(caller, text, params)) { 1133 return TextToSpeech.ERROR; 1134 } 1135 1136 SpeechItem item = new SynthesisSpeechItemV1(caller, 1137 Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text); 1138 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1139 } 1140 1141 @Override 1142 public int synthesizeToFileDescriptor(IBinder caller, CharSequence text, ParcelFileDescriptor 1143 fileDescriptor, Bundle params, String utteranceId) { 1144 if (!checkNonNull(caller, text, fileDescriptor, params)) { 1145 return TextToSpeech.ERROR; 1146 } 1147 1148 // In test env, ParcelFileDescriptor instance may be EXACTLY the same 1149 // one that is used by client. And it will be closed by a client, thus 1150 // preventing us from writing anything to it. 1151 final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd( 1152 fileDescriptor.detachFd()); 1153 1154 SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV1(caller, 1155 Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text, 1156 new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor)); 1157 return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); 1158 } 1159 1160 @Override 1161 public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params, 1162 String utteranceId) { 1163 if (!checkNonNull(caller, audioUri, params)) { 1164 return TextToSpeech.ERROR; 1165 } 1166 1167 SpeechItem item = new AudioSpeechItemV1(caller, 1168 Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, audioUri); 1169 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1170 } 1171 1172 @Override 1173 public int playSilence(IBinder caller, long duration, int queueMode, String utteranceId) { 1174 if (!checkNonNull(caller)) { 1175 return TextToSpeech.ERROR; 1176 } 1177 1178 SpeechItem item = new SilenceSpeechItem(caller, 1179 Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, duration); 1180 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1181 } 1182 1183 @Override 1184 public boolean isSpeaking() { 1185 return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); 1186 } 1187 1188 @Override 1189 public int stop(IBinder caller) { 1190 if (!checkNonNull(caller)) { 1191 return TextToSpeech.ERROR; 1192 } 1193 1194 return mSynthHandler.stopForApp(caller); 1195 } 1196 1197 @Override 1198 public String[] getLanguage() { 1199 return onGetLanguage(); 1200 } 1201 1202 @Override 1203 public String[] getClientDefaultLanguage() { 1204 return getSettingsLocale(); 1205 } 1206 1207 /* 1208 * If defaults are enforced, then no language is "available" except 1209 * perhaps the default language selected by the user. 1210 */ 1211 @Override 1212 public int isLanguageAvailable(String lang, String country, String variant) { 1213 if (!checkNonNull(lang)) { 1214 return TextToSpeech.ERROR; 1215 } 1216 1217 return onIsLanguageAvailable(lang, country, variant); 1218 } 1219 1220 @Override 1221 public String[] getFeaturesForLanguage(String lang, String country, String variant) { 1222 Set<String> features = onGetFeaturesForLanguage(lang, country, variant); 1223 String[] featuresArray = null; 1224 if (features != null) { 1225 featuresArray = new String[features.size()]; 1226 features.toArray(featuresArray); 1227 } else { 1228 featuresArray = new String[0]; 1229 } 1230 return featuresArray; 1231 } 1232 1233 /* 1234 * There is no point loading a non default language if defaults 1235 * are enforced. 1236 */ 1237 @Override 1238 public int loadLanguage(IBinder caller, String lang, String country, String variant) { 1239 if (!checkNonNull(lang)) { 1240 return TextToSpeech.ERROR; 1241 } 1242 int retVal = onIsLanguageAvailable(lang, country, variant); 1243 1244 if (retVal == TextToSpeech.LANG_AVAILABLE || 1245 retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE || 1246 retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 1247 1248 SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(), 1249 Binder.getCallingPid(), lang, country, variant); 1250 1251 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) != 1252 TextToSpeech.SUCCESS) { 1253 return TextToSpeech.ERROR; 1254 } 1255 } 1256 return retVal; 1257 } 1258 1259 @Override 1260 public List<Voice> getVoices() { 1261 return onGetVoices(); 1262 } 1263 1264 @Override 1265 public int loadVoice(IBinder caller, String voiceName) { 1266 if (!checkNonNull(voiceName)) { 1267 return TextToSpeech.ERROR; 1268 } 1269 int retVal = isValidVoiceName(voiceName); 1270 1271 if (retVal == TextToSpeech.SUCCESS) { 1272 SpeechItem item = new LoadVoiceItem(caller, Binder.getCallingUid(), 1273 Binder.getCallingPid(), voiceName); 1274 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) != 1275 TextToSpeech.SUCCESS) { 1276 return TextToSpeech.ERROR; 1277 } 1278 } 1279 return retVal; 1280 } 1281 1282 public String getDefaultVoiceNameFor(String lang, String country, String variant) { 1283 if (!checkNonNull(lang)) { 1284 return null; 1285 } 1286 int retVal = onIsLanguageAvailable(lang, country, variant); 1287 1288 if (retVal == TextToSpeech.LANG_AVAILABLE || 1289 retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE || 1290 retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 1291 return onGetDefaultVoiceNameFor(lang, country, variant); 1292 } else { 1293 return null; 1294 } 1295 } 1296 1297 @Override 1298 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 1299 // Note that passing in a null callback is a valid use case. 1300 if (!checkNonNull(caller)) { 1301 return; 1302 } 1303 1304 mCallbacks.setCallback(caller, cb); 1305 } 1306 1307 private String intern(String in) { 1308 // The input parameter will be non null. 1309 return in.intern(); 1310 } 1311 1312 private boolean checkNonNull(Object... args) { 1313 for (Object o : args) { 1314 if (o == null) return false; 1315 } 1316 return true; 1317 } 1318 }; 1319 1320 private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { 1321 private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback 1322 = new HashMap<IBinder, ITextToSpeechCallback>(); 1323 1324 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 1325 synchronized (mCallerToCallback) { 1326 ITextToSpeechCallback old; 1327 if (cb != null) { 1328 register(cb, caller); 1329 old = mCallerToCallback.put(caller, cb); 1330 } else { 1331 old = mCallerToCallback.remove(caller); 1332 } 1333 if (old != null && old != cb) { 1334 unregister(old); 1335 } 1336 } 1337 } 1338 1339 public void dispatchOnFallback(Object callerIdentity, String utteranceId) { 1340 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1341 if (cb == null) return; 1342 try { 1343 cb.onFallback(utteranceId); 1344 } catch (RemoteException e) { 1345 Log.e(TAG, "Callback onFallback failed: " + e); 1346 } 1347 } 1348 1349 public void dispatchOnStop(Object callerIdentity, String utteranceId) { 1350 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1351 if (cb == null) return; 1352 try { 1353 cb.onStop(utteranceId); 1354 } catch (RemoteException e) { 1355 Log.e(TAG, "Callback onStop failed: " + e); 1356 } 1357 } 1358 1359 public void dispatchOnSuccess(Object callerIdentity, String utteranceId) { 1360 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1361 if (cb == null) return; 1362 try { 1363 cb.onSuccess(utteranceId); 1364 } catch (RemoteException e) { 1365 Log.e(TAG, "Callback onDone failed: " + e); 1366 } 1367 } 1368 1369 public void dispatchOnStart(Object callerIdentity, String utteranceId) { 1370 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1371 if (cb == null) return; 1372 try { 1373 cb.onStart(utteranceId); 1374 } catch (RemoteException e) { 1375 Log.e(TAG, "Callback onStart failed: " + e); 1376 } 1377 1378 } 1379 1380 public void dispatchOnError(Object callerIdentity, String utteranceId, 1381 int errorCode) { 1382 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1383 if (cb == null) return; 1384 try { 1385 cb.onError(utteranceId, errorCode); 1386 } catch (RemoteException e) { 1387 Log.e(TAG, "Callback onError failed: " + e); 1388 } 1389 } 1390 1391 @Override 1392 public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { 1393 IBinder caller = (IBinder) cookie; 1394 synchronized (mCallerToCallback) { 1395 mCallerToCallback.remove(caller); 1396 } 1397 //mSynthHandler.stopForApp(caller); 1398 } 1399 1400 @Override 1401 public void kill() { 1402 synchronized (mCallerToCallback) { 1403 mCallerToCallback.clear(); 1404 super.kill(); 1405 } 1406 } 1407 1408 private ITextToSpeechCallback getCallbackFor(Object caller) { 1409 ITextToSpeechCallback cb; 1410 IBinder asBinder = (IBinder) caller; 1411 synchronized (mCallerToCallback) { 1412 cb = mCallerToCallback.get(asBinder); 1413 } 1414 1415 return cb; 1416 } 1417 } 1418} 1419