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