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 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 */ onSynthesizeText(SynthesisRequest request, SynthesisCallback callback)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 */ onGetFeaturesForLanguage(String lang, String country, String variant)251 protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) { 252 return new HashSet<String>(); 253 } 254 getExpectedLanguageAvailableStatus(Locale locale)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 */ onGetVoices()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 */ onGetDefaultVoiceNameFor(String lang, String country, String variant)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 */ onLoadVoice(String voiceName)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 */ onIsValidVoiceName(String voiceName)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 getDefaultSpeechRate()411 private int getDefaultSpeechRate() { 412 return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE); 413 } 414 getSettingsLocale()415 private String[] getSettingsLocale() { 416 final Locale locale = mEngineHelper.getLocalePrefForEngine(mPackageName); 417 return TtsEngines.toOldLocaleStringFormat(locale); 418 } 419 getSecureSettingInt(String name, int defaultValue)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 SynthThread()431 public SynthThread() { 432 super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_DEFAULT); 433 } 434 435 @Override onLooperPrepared()436 protected void onLooperPrepared() { 437 getLooper().getQueue().addIdleHandler(this); 438 } 439 440 @Override queueIdle()441 public boolean queueIdle() { 442 if (mFirstIdle) { 443 mFirstIdle = false; 444 } else { 445 broadcastTtsQueueProcessingCompleted(); 446 } 447 return true; 448 } 449 broadcastTtsQueueProcessingCompleted()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 SynthHandler(Looper looper)472 public SynthHandler(Looper looper) { 473 super(looper); 474 } 475 startFlushingSpeechItems(Object callerIdentity)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 } endFlushingSpeechItems(Object callerIdentity)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 } isFlushed(SpeechItem speechItem)494 private boolean isFlushed(SpeechItem speechItem) { 495 synchronized (mFlushedObjects) { 496 return mFlushAll > 0 || mFlushedObjects.contains(speechItem.getCallerIdentity()); 497 } 498 } 499 getCurrentSpeechItem()500 private synchronized SpeechItem getCurrentSpeechItem() { 501 return mCurrentSpeechItem; 502 } 503 setCurrentSpeechItem(SpeechItem speechItem)504 private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) { 505 SpeechItem old = mCurrentSpeechItem; 506 mCurrentSpeechItem = speechItem; 507 return old; 508 } 509 maybeRemoveCurrentSpeechItem(Object callerIdentity)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 isSpeaking()521 public boolean isSpeaking() { 522 return getCurrentSpeechItem() != null; 523 } 524 quit()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 */ enqueueSpeechItem(int queueMode, final SpeechItem speechItem)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 */ stopForApp(final Object callerIdentity)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 stopAll()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 { dispatchOnStop()658 public void dispatchOnStop(); dispatchOnSuccess()659 public void dispatchOnSuccess(); dispatchOnStart()660 public void dispatchOnStart(); dispatchOnError(int errorCode)661 public void dispatchOnError(int errorCode); dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount)662 public void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount); dispatchOnAudioAvailable(byte[] audio)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 */ AudioOutputParams()695 AudioOutputParams() { 696 mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE; 697 mVolume = Engine.DEFAULT_VOLUME; 698 mPan = Engine.DEFAULT_PAN; 699 mAudioAttributes = null; 700 } 701 AudioOutputParams(int sessionId, float volume, float pan, AudioAttributes audioAttributes)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 */ createFromV1ParamsBundle(Bundle paramsBundle, boolean isSpeech)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 SpeechItem(Object caller, int callerUid, int callerPid)756 public SpeechItem(Object caller, int callerUid, int callerPid) { 757 mCallerIdentity = caller; 758 mCallerUid = callerUid; 759 mCallerPid = callerPid; 760 } 761 getCallerIdentity()762 public Object getCallerIdentity() { 763 return mCallerIdentity; 764 } 765 getCallerUid()766 public int getCallerUid() { 767 return mCallerUid; 768 } 769 getCallerPid()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 */ isValid()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 */ play()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 playImpl()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 */ stop()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 stopImpl()814 protected abstract void stopImpl(); 815 isStopped()816 protected synchronized boolean isStopped() { 817 return mStopped; 818 } 819 isStarted()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 UtteranceSpeechItem(Object caller, int callerUid, int callerPid)832 public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) { 833 super(caller, callerUid, callerPid); 834 } 835 836 @Override dispatchOnSuccess()837 public void dispatchOnSuccess() { 838 final String utteranceId = getUtteranceId(); 839 if (utteranceId != null) { 840 mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId); 841 } 842 } 843 844 @Override dispatchOnStop()845 public void dispatchOnStop() { 846 final String utteranceId = getUtteranceId(); 847 if (utteranceId != null) { 848 mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted()); 849 } 850 } 851 852 @Override dispatchOnStart()853 public void dispatchOnStart() { 854 final String utteranceId = getUtteranceId(); 855 if (utteranceId != null) { 856 mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId); 857 } 858 } 859 860 @Override dispatchOnError(int errorCode)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 dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount)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 dispatchOnAudioAvailable(byte[] audio)877 public void dispatchOnAudioAvailable(byte[] audio) { 878 final String utteranceId = getUtteranceId(); 879 if (utteranceId != null) { 880 mCallbacks.dispatchOnAudioAvailable(getCallerIdentity(), utteranceId, audio); 881 } 882 } 883 getUtteranceId()884 abstract public String getUtteranceId(); 885 getStringParam(Bundle params, String key, String defaultValue)886 String getStringParam(Bundle params, String key, String defaultValue) { 887 return params == null ? defaultValue : params.getString(key, defaultValue); 888 } 889 getIntParam(Bundle params, String key, int defaultValue)890 int getIntParam(Bundle params, String key, int defaultValue) { 891 return params == null ? defaultValue : params.getInt(key, defaultValue); 892 } 893 getFloatParam(Bundle params, String key, float defaultValue)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 SpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId)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 hasLanguage()915 boolean hasLanguage() { 916 return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null)); 917 } 918 getSpeechRate()919 int getSpeechRate() { 920 return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); 921 } 922 getPitch()923 int getPitch() { 924 return getIntParam(mParams, Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); 925 } 926 927 @Override getUtteranceId()928 public String getUtteranceId() { 929 return mUtteranceId; 930 } 931 getAudioParams()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 SynthesisSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId, CharSequence text)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 getText()960 public CharSequence getText() { 961 return mText; 962 } 963 964 @Override isValid()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 playImpl()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 createSynthesisCallback()999 protected AbstractSynthesisCallback createSynthesisCallback() { 1000 return new PlaybackSynthesisCallback(getAudioParams(), 1001 mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false); 1002 } 1003 setRequestParams(SynthesisRequest request)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 stopImpl()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 getCountry()1032 private String getCountry() { 1033 if (!hasLanguage()) return mDefaultLocale[1]; 1034 return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, ""); 1035 } 1036 getVariant()1037 private String getVariant() { 1038 if (!hasLanguage()) return mDefaultLocale[2]; 1039 return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, ""); 1040 } 1041 getLanguage()1042 public String getLanguage() { 1043 return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); 1044 } 1045 getVoiceName()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 SynthesisToFileOutputStreamSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId, CharSequence text, FileOutputStream fileOutputStream)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 createSynthesisCallback()1062 protected AbstractSynthesisCallback createSynthesisCallback() { 1063 return new FileSynthesisCallback(mFileOutputStream.getChannel(), this, false); 1064 } 1065 1066 @Override playImpl()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 AudioSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId, Uri uri)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 isValid()1089 public boolean isValid() { 1090 return true; 1091 } 1092 1093 @Override playImpl()1094 protected void playImpl() { 1095 mAudioPlaybackHandler.enqueue(mItem); 1096 } 1097 1098 @Override stopImpl()1099 protected void stopImpl() { 1100 // Do nothing. 1101 } 1102 1103 @Override getUtteranceId()1104 public String getUtteranceId() { 1105 return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null); 1106 } 1107 1108 @Override getAudioParams()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 SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, String utteranceId, long duration)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 isValid()1126 public boolean isValid() { 1127 return true; 1128 } 1129 1130 @Override playImpl()1131 protected void playImpl() { 1132 mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( 1133 this, getCallerIdentity(), mDuration)); 1134 } 1135 1136 @Override stopImpl()1137 protected void stopImpl() { 1138 1139 } 1140 1141 @Override getUtteranceId()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 LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, String language, String country, String variant)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 isValid()1164 public boolean isValid() { 1165 return true; 1166 } 1167 1168 @Override playImpl()1169 protected void playImpl() { 1170 TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); 1171 } 1172 1173 @Override stopImpl()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 LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid, String voiceName)1185 public LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid, 1186 String voiceName) { 1187 super(callerIdentity, callerUid, callerPid); 1188 mVoiceName = voiceName; 1189 } 1190 1191 @Override isValid()1192 public boolean isValid() { 1193 return true; 1194 } 1195 1196 @Override playImpl()1197 protected void playImpl() { 1198 TextToSpeechService.this.onLoadVoice(mVoiceName); 1199 } 1200 1201 @Override stopImpl()1202 protected void stopImpl() { 1203 // No-op 1204 } 1205 } 1206 1207 1208 @Override onBind(Intent intent)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 setCallback(IBinder caller, ITextToSpeechCallback cb)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 dispatchOnStop(Object callerIdentity, String utteranceId, boolean started)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 dispatchOnSuccess(Object callerIdentity, String utteranceId)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 dispatchOnStart(Object callerIdentity, String utteranceId)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 dispatchOnError(Object callerIdentity, String utteranceId, int errorCode)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 dispatchOnBeginSynthesis(Object callerIdentity, String utteranceId, int sampleRateInHz, int audioFormat, int channelCount)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 dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer)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 onCallbackDied(ITextToSpeechCallback callback, Object cookie)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 kill()1504 public void kill() { 1505 synchronized (mCallerToCallback) { 1506 mCallerToCallback.clear(); 1507 super.kill(); 1508 } 1509 } 1510 getCallbackFor(Object caller)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