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