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 124 public void onCreate() { 125 if (DBG) Log.d(TAG, "onCreate()"); 126 super.onCreate(); 127 128 SynthThread synthThread = new SynthThread(); 129 synthThread.start(); 130 mSynthHandler = new SynthHandler(synthThread.getLooper()); 131 132 mAudioPlaybackHandler = new AudioPlaybackHandler(); 133 mAudioPlaybackHandler.start(); 134 135 mEngineHelper = new TtsEngines(this); 136 137 mCallbacks = new CallbackMap(); 138 139 mPackageName = getApplicationInfo().packageName; 140 141 String[] defaultLocale = getSettingsLocale(); 142 143 // Load default language 144 onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]); 145 } 146 147 @Override 148 public void onDestroy() { 149 if (DBG) Log.d(TAG, "onDestroy()"); 150 151 // Tell the synthesizer to stop 152 mSynthHandler.quit(); 153 // Tell the audio playback thread to stop. 154 mAudioPlaybackHandler.quit(); 155 // Unregister all callbacks. 156 mCallbacks.kill(); 157 158 super.onDestroy(); 159 } 160 161 /** 162 * Checks whether the engine supports a given language. 163 * 164 * Can be called on multiple threads. 165 * 166 * Its return values HAVE to be consistent with onLoadLanguage. 167 * 168 * @param lang ISO-3 language code. 169 * @param country ISO-3 country code. May be empty or null. 170 * @param variant Language variant. May be empty or null. 171 * @return Code indicating the support status for the locale. 172 * One of {@link TextToSpeech#LANG_AVAILABLE}, 173 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 174 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 175 * {@link TextToSpeech#LANG_MISSING_DATA} 176 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 177 */ 178 protected abstract int onIsLanguageAvailable(String lang, String country, String variant); 179 180 /** 181 * Returns the language, country and variant currently being used by the TTS engine. 182 * 183 * This method will be called only on Android 4.2 and before (API <= 17). In later versions 184 * this method is not called by the Android TTS framework. 185 * 186 * Can be called on multiple threads. 187 * 188 * @return A 3-element array, containing language (ISO 3-letter code), 189 * country (ISO 3-letter code) and variant used by the engine. 190 * The country and variant may be {@code ""}. If country is empty, then variant must 191 * be empty too. 192 * @see Locale#getISO3Language() 193 * @see Locale#getISO3Country() 194 * @see Locale#getVariant() 195 */ 196 protected abstract String[] onGetLanguage(); 197 198 /** 199 * Notifies the engine that it should load a speech synthesis language. There is no guarantee 200 * that this method is always called before the language is used for synthesis. It is merely 201 * a hint to the engine that it will probably get some synthesis requests for this language 202 * at some point in the future. 203 * 204 * Can be called on multiple threads. 205 * In <= Android 4.2 (<= API 17) can be called on main and service binder threads. 206 * In > Android 4.2 (> API 17) can be called on main and synthesis threads. 207 * 208 * @param lang ISO-3 language code. 209 * @param country ISO-3 country code. May be empty or null. 210 * @param variant Language variant. May be empty or null. 211 * @return Code indicating the support status for the locale. 212 * One of {@link TextToSpeech#LANG_AVAILABLE}, 213 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 214 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 215 * {@link TextToSpeech#LANG_MISSING_DATA} 216 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 217 */ 218 protected abstract int onLoadLanguage(String lang, String country, String variant); 219 220 /** 221 * Notifies the service that it should stop any in-progress speech synthesis. 222 * This method can be called even if no speech synthesis is currently in progress. 223 * 224 * Can be called on multiple threads, but not on the synthesis thread. 225 */ 226 protected abstract void onStop(); 227 228 /** 229 * Tells the service to synthesize speech from the given text. This method 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 */ 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 */ 248 protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) { 249 return new HashSet<String>(); 250 } 251 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 */ 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 */ 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 */ 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 */ 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 408 private int getDefaultSpeechRate() { 409 return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE); 410 } 411 412 private int getDefaultPitch() { 413 return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_PITCH, Engine.DEFAULT_PITCH); 414 } 415 416 private String[] getSettingsLocale() { 417 final Locale locale = mEngineHelper.getLocalePrefForEngine(mPackageName); 418 return TtsEngines.toOldLocaleStringFormat(locale); 419 } 420 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 432 public SynthThread() { 433 super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_DEFAULT); 434 } 435 436 @Override 437 protected void onLooperPrepared() { 438 getLooper().getQueue().addIdleHandler(this); 439 } 440 441 @Override 442 public boolean queueIdle() { 443 if (mFirstIdle) { 444 mFirstIdle = false; 445 } else { 446 broadcastTtsQueueProcessingCompleted(); 447 } 448 return true; 449 } 450 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 473 public SynthHandler(Looper looper) { 474 super(looper); 475 } 476 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 } 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 } 495 private boolean isFlushed(SpeechItem speechItem) { 496 synchronized (mFlushedObjects) { 497 return mFlushAll > 0 || mFlushedObjects.contains(speechItem.getCallerIdentity()); 498 } 499 } 500 501 private synchronized SpeechItem getCurrentSpeechItem() { 502 return mCurrentSpeechItem; 503 } 504 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 523 private synchronized SpeechItem removeCurrentSpeechItem() { 524 SpeechItem current = mCurrentSpeechItem; 525 mCurrentSpeechItem = null; 526 return current; 527 } 528 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 540 public boolean isSpeaking() { 541 return getCurrentSpeechItem() != null; 542 } 543 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 */ 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 */ 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 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 { 678 void dispatchOnStop(); 679 680 void dispatchOnSuccess(); 681 682 void dispatchOnStart(); 683 684 void dispatchOnError(int errorCode); 685 686 void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount); 687 688 void dispatchOnAudioAvailable(byte[] audio); 689 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 */ 722 AudioOutputParams() { 723 mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE; 724 mVolume = Engine.DEFAULT_VOLUME; 725 mPan = Engine.DEFAULT_PAN; 726 mAudioAttributes = null; 727 } 728 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 */ 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 782 public SpeechItem(Object caller, int callerUid, int callerPid) { 783 mCallerIdentity = caller; 784 mCallerUid = callerUid; 785 mCallerPid = callerPid; 786 } 787 788 public Object getCallerIdentity() { 789 return mCallerIdentity; 790 } 791 792 public int getCallerUid() { 793 return mCallerUid; 794 } 795 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 */ 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 */ 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 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 */ 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 840 protected abstract void stopImpl(); 841 842 protected synchronized boolean isStopped() { 843 return mStopped; 844 } 845 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 858 public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) { 859 super(caller, callerUid, callerPid); 860 } 861 862 @Override 863 public void dispatchOnSuccess() { 864 final String utteranceId = getUtteranceId(); 865 if (utteranceId != null) { 866 mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId); 867 } 868 } 869 870 @Override 871 public void dispatchOnStop() { 872 final String utteranceId = getUtteranceId(); 873 if (utteranceId != null) { 874 mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted()); 875 } 876 } 877 878 @Override 879 public void dispatchOnStart() { 880 final String utteranceId = getUtteranceId(); 881 if (utteranceId != null) { 882 mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId); 883 } 884 } 885 886 @Override 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 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 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 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 919 abstract public String getUtteranceId(); 920 921 String getStringParam(Bundle params, String key, String defaultValue) { 922 return params == null ? defaultValue : params.getString(key, defaultValue); 923 } 924 925 int getIntParam(Bundle params, String key, int defaultValue) { 926 return params == null ? defaultValue : params.getInt(key, defaultValue); 927 } 928 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 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 953 boolean hasLanguage() { 954 return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null)); 955 } 956 957 int getSpeechRate() { 958 return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); 959 } 960 961 int getPitch() { 962 return getIntParam(mParams, Engine.KEY_PARAM_PITCH, getDefaultPitch()); 963 } 964 965 @Override 966 public String getUtteranceId() { 967 return mUtteranceId; 968 } 969 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 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 1002 public CharSequence getText() { 1003 return mText; 1004 } 1005 1006 @Override 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 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 1041 protected AbstractSynthesisCallback createSynthesisCallback() { 1042 return new PlaybackSynthesisCallback(getAudioParams(), 1043 mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false); 1044 } 1045 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 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 1074 private String getCountry() { 1075 if (!hasLanguage()) return mDefaultLocale[1]; 1076 return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, ""); 1077 } 1078 1079 private String getVariant() { 1080 if (!hasLanguage()) return mDefaultLocale[2]; 1081 return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, ""); 1082 } 1083 1084 public String getLanguage() { 1085 return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); 1086 } 1087 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 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 1109 protected AbstractSynthesisCallback createSynthesisCallback() { 1110 return new FileSynthesisCallback(mFileOutputStream.getChannel(), this, false); 1111 } 1112 1113 @Override 1114 protected void playImpl() { 1115 dispatchOnStart(); 1116 super.playImpl(); 1117 try { 1118 mFileOutputStream.close(); 1119 } catch(IOException e) { 1120 Log.w(TAG, "Failed to close output file", e); 1121 } 1122 } 1123 } 1124 1125 private class AudioSpeechItem extends UtteranceSpeechItemWithParams { 1126 private final AudioPlaybackQueueItem mItem; 1127 1128 public AudioSpeechItem( 1129 Object callerIdentity, 1130 int callerUid, 1131 int callerPid, 1132 Bundle params, 1133 String utteranceId, 1134 Uri uri) { 1135 super(callerIdentity, callerUid, callerPid, params, utteranceId); 1136 mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), 1137 TextToSpeechService.this, uri, getAudioParams()); 1138 } 1139 1140 @Override 1141 public boolean isValid() { 1142 return true; 1143 } 1144 1145 @Override 1146 protected void playImpl() { 1147 mAudioPlaybackHandler.enqueue(mItem); 1148 } 1149 1150 @Override 1151 protected void stopImpl() { 1152 // Do nothing. 1153 } 1154 1155 @Override 1156 public String getUtteranceId() { 1157 return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null); 1158 } 1159 1160 @Override 1161 AudioOutputParams getAudioParams() { 1162 return AudioOutputParams.createFromParamsBundle(mParams, false); 1163 } 1164 } 1165 1166 private class SilenceSpeechItem extends UtteranceSpeechItem { 1167 private final long mDuration; 1168 private final String mUtteranceId; 1169 1170 public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, 1171 String utteranceId, long duration) { 1172 super(callerIdentity, callerUid, callerPid); 1173 mUtteranceId = utteranceId; 1174 mDuration = duration; 1175 } 1176 1177 @Override 1178 public boolean isValid() { 1179 return true; 1180 } 1181 1182 @Override 1183 protected void playImpl() { 1184 mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( 1185 this, getCallerIdentity(), mDuration)); 1186 } 1187 1188 @Override 1189 protected void stopImpl() { 1190 1191 } 1192 1193 @Override 1194 public String getUtteranceId() { 1195 return mUtteranceId; 1196 } 1197 } 1198 1199 /** 1200 * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. 1201 */ 1202 private class LoadLanguageItem extends SpeechItem { 1203 private final String mLanguage; 1204 private final String mCountry; 1205 private final String mVariant; 1206 1207 public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, 1208 String language, String country, String variant) { 1209 super(callerIdentity, callerUid, callerPid); 1210 mLanguage = language; 1211 mCountry = country; 1212 mVariant = variant; 1213 } 1214 1215 @Override 1216 public boolean isValid() { 1217 return true; 1218 } 1219 1220 @Override 1221 protected void playImpl() { 1222 TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); 1223 } 1224 1225 @Override 1226 protected void stopImpl() { 1227 // No-op 1228 } 1229 } 1230 1231 /** 1232 * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. 1233 */ 1234 private class LoadVoiceItem extends SpeechItem { 1235 private final String mVoiceName; 1236 1237 public LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid, 1238 String voiceName) { 1239 super(callerIdentity, callerUid, callerPid); 1240 mVoiceName = voiceName; 1241 } 1242 1243 @Override 1244 public boolean isValid() { 1245 return true; 1246 } 1247 1248 @Override 1249 protected void playImpl() { 1250 TextToSpeechService.this.onLoadVoice(mVoiceName); 1251 } 1252 1253 @Override 1254 protected void stopImpl() { 1255 // No-op 1256 } 1257 } 1258 1259 1260 @Override 1261 public IBinder onBind(Intent intent) { 1262 if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { 1263 return mBinder; 1264 } 1265 return null; 1266 } 1267 1268 /** 1269 * Binder returned from {@code #onBind(Intent)}. The methods in this class can be called called 1270 * from several different threads. 1271 */ 1272 // NOTE: All calls that are passed in a calling app are interned so that 1273 // they can be used as message objects (which are tested for equality using ==). 1274 private final ITextToSpeechService.Stub mBinder = 1275 new ITextToSpeechService.Stub() { 1276 @Override 1277 public int speak( 1278 IBinder caller, 1279 CharSequence text, 1280 int queueMode, 1281 Bundle params, 1282 String utteranceId) { 1283 if (!checkNonNull(caller, text, params)) { 1284 return TextToSpeech.ERROR; 1285 } 1286 1287 SpeechItem item = 1288 new SynthesisSpeechItem( 1289 caller, 1290 Binder.getCallingUid(), 1291 Binder.getCallingPid(), 1292 params, 1293 utteranceId, 1294 text); 1295 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1296 } 1297 1298 @Override 1299 public int synthesizeToFileDescriptor( 1300 IBinder caller, 1301 CharSequence text, 1302 ParcelFileDescriptor fileDescriptor, 1303 Bundle params, 1304 String utteranceId) { 1305 if (!checkNonNull(caller, text, fileDescriptor, params)) { 1306 return TextToSpeech.ERROR; 1307 } 1308 1309 // In test env, ParcelFileDescriptor instance may be EXACTLY the same 1310 // one that is used by client. And it will be closed by a client, thus 1311 // preventing us from writing anything to it. 1312 final ParcelFileDescriptor sameFileDescriptor = 1313 ParcelFileDescriptor.adoptFd(fileDescriptor.detachFd()); 1314 1315 SpeechItem item = 1316 new SynthesisToFileOutputStreamSpeechItem( 1317 caller, 1318 Binder.getCallingUid(), 1319 Binder.getCallingPid(), 1320 params, 1321 utteranceId, 1322 text, 1323 new ParcelFileDescriptor.AutoCloseOutputStream( 1324 sameFileDescriptor)); 1325 return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); 1326 } 1327 1328 @Override 1329 public int playAudio( 1330 IBinder caller, 1331 Uri audioUri, 1332 int queueMode, 1333 Bundle params, 1334 String utteranceId) { 1335 if (!checkNonNull(caller, audioUri, params)) { 1336 return TextToSpeech.ERROR; 1337 } 1338 1339 SpeechItem item = 1340 new AudioSpeechItem( 1341 caller, 1342 Binder.getCallingUid(), 1343 Binder.getCallingPid(), 1344 params, 1345 utteranceId, 1346 audioUri); 1347 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1348 } 1349 1350 @Override 1351 public int playSilence( 1352 IBinder caller, long duration, int queueMode, String utteranceId) { 1353 if (!checkNonNull(caller)) { 1354 return TextToSpeech.ERROR; 1355 } 1356 1357 SpeechItem item = 1358 new SilenceSpeechItem( 1359 caller, 1360 Binder.getCallingUid(), 1361 Binder.getCallingPid(), 1362 utteranceId, 1363 duration); 1364 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1365 } 1366 1367 @Override 1368 public boolean isSpeaking() { 1369 return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); 1370 } 1371 1372 @Override 1373 public int stop(IBinder caller) { 1374 if (!checkNonNull(caller)) { 1375 return TextToSpeech.ERROR; 1376 } 1377 1378 return mSynthHandler.stopForApp(caller); 1379 } 1380 1381 @Override 1382 public String[] getLanguage() { 1383 return onGetLanguage(); 1384 } 1385 1386 @Override 1387 public String[] getClientDefaultLanguage() { 1388 return getSettingsLocale(); 1389 } 1390 1391 /* 1392 * If defaults are enforced, then no language is "available" except 1393 * perhaps the default language selected by the user. 1394 */ 1395 @Override 1396 public int isLanguageAvailable(String lang, String country, String variant) { 1397 if (!checkNonNull(lang)) { 1398 return TextToSpeech.ERROR; 1399 } 1400 1401 return onIsLanguageAvailable(lang, country, variant); 1402 } 1403 1404 @Override 1405 public String[] getFeaturesForLanguage( 1406 String lang, String country, String variant) { 1407 Set<String> features = onGetFeaturesForLanguage(lang, country, variant); 1408 String[] featuresArray = null; 1409 if (features != null) { 1410 featuresArray = new String[features.size()]; 1411 features.toArray(featuresArray); 1412 } else { 1413 featuresArray = new String[0]; 1414 } 1415 return featuresArray; 1416 } 1417 1418 /* 1419 * There is no point loading a non default language if defaults 1420 * are enforced. 1421 */ 1422 @Override 1423 public int loadLanguage( 1424 IBinder caller, String lang, String country, String variant) { 1425 if (!checkNonNull(lang)) { 1426 return TextToSpeech.ERROR; 1427 } 1428 int retVal = onIsLanguageAvailable(lang, country, variant); 1429 1430 if (retVal == TextToSpeech.LANG_AVAILABLE 1431 || retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE 1432 || retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 1433 1434 SpeechItem item = 1435 new LoadLanguageItem( 1436 caller, 1437 Binder.getCallingUid(), 1438 Binder.getCallingPid(), 1439 lang, 1440 country, 1441 variant); 1442 1443 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) 1444 != TextToSpeech.SUCCESS) { 1445 return TextToSpeech.ERROR; 1446 } 1447 } 1448 return retVal; 1449 } 1450 1451 @Override 1452 public List<Voice> getVoices() { 1453 return onGetVoices(); 1454 } 1455 1456 @Override 1457 public int loadVoice(IBinder caller, String voiceName) { 1458 if (!checkNonNull(voiceName)) { 1459 return TextToSpeech.ERROR; 1460 } 1461 int retVal = onIsValidVoiceName(voiceName); 1462 1463 if (retVal == TextToSpeech.SUCCESS) { 1464 SpeechItem item = 1465 new LoadVoiceItem( 1466 caller, 1467 Binder.getCallingUid(), 1468 Binder.getCallingPid(), 1469 voiceName); 1470 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) 1471 != TextToSpeech.SUCCESS) { 1472 return TextToSpeech.ERROR; 1473 } 1474 } 1475 return retVal; 1476 } 1477 1478 public String getDefaultVoiceNameFor(String lang, String country, String variant) { 1479 if (!checkNonNull(lang)) { 1480 return null; 1481 } 1482 int retVal = onIsLanguageAvailable(lang, country, variant); 1483 1484 if (retVal == TextToSpeech.LANG_AVAILABLE 1485 || retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE 1486 || retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 1487 return onGetDefaultVoiceNameFor(lang, country, variant); 1488 } else { 1489 return null; 1490 } 1491 } 1492 1493 @Override 1494 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 1495 // Note that passing in a null callback is a valid use case. 1496 if (!checkNonNull(caller)) { 1497 return; 1498 } 1499 1500 mCallbacks.setCallback(caller, cb); 1501 } 1502 1503 private String intern(String in) { 1504 // The input parameter will be non null. 1505 return in.intern(); 1506 } 1507 1508 private boolean checkNonNull(Object... args) { 1509 for (Object o : args) { 1510 if (o == null) return false; 1511 } 1512 return true; 1513 } 1514 }; 1515 1516 private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { 1517 private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback 1518 = new HashMap<IBinder, ITextToSpeechCallback>(); 1519 1520 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 1521 synchronized (mCallerToCallback) { 1522 ITextToSpeechCallback old; 1523 if (cb != null) { 1524 register(cb, caller); 1525 old = mCallerToCallback.put(caller, cb); 1526 } else { 1527 old = mCallerToCallback.remove(caller); 1528 } 1529 if (old != null && old != cb) { 1530 unregister(old); 1531 } 1532 } 1533 } 1534 1535 public void dispatchOnStop(Object callerIdentity, String utteranceId, boolean started) { 1536 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1537 if (cb == null) return; 1538 try { 1539 cb.onStop(utteranceId, started); 1540 } catch (RemoteException e) { 1541 Log.e(TAG, "Callback onStop failed: " + e); 1542 } 1543 } 1544 1545 public void dispatchOnSuccess(Object callerIdentity, String utteranceId) { 1546 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1547 if (cb == null) return; 1548 try { 1549 cb.onSuccess(utteranceId); 1550 } catch (RemoteException e) { 1551 Log.e(TAG, "Callback onDone failed: " + e); 1552 } 1553 } 1554 1555 public void dispatchOnStart(Object callerIdentity, String utteranceId) { 1556 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1557 if (cb == null) return; 1558 try { 1559 cb.onStart(utteranceId); 1560 } catch (RemoteException e) { 1561 Log.e(TAG, "Callback onStart failed: " + e); 1562 } 1563 } 1564 1565 public void dispatchOnError(Object callerIdentity, String utteranceId, 1566 int errorCode) { 1567 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1568 if (cb == null) return; 1569 try { 1570 cb.onError(utteranceId, errorCode); 1571 } catch (RemoteException e) { 1572 Log.e(TAG, "Callback onError failed: " + e); 1573 } 1574 } 1575 1576 public void dispatchOnBeginSynthesis(Object callerIdentity, String utteranceId, int sampleRateInHz, int audioFormat, int channelCount) { 1577 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1578 if (cb == null) return; 1579 try { 1580 cb.onBeginSynthesis(utteranceId, sampleRateInHz, audioFormat, channelCount); 1581 } catch (RemoteException e) { 1582 Log.e(TAG, "Callback dispatchOnBeginSynthesis(String, int, int, int) failed: " + e); 1583 } 1584 } 1585 1586 public void dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer) { 1587 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1588 if (cb == null) return; 1589 try { 1590 cb.onAudioAvailable(utteranceId, buffer); 1591 } catch (RemoteException e) { 1592 Log.e(TAG, "Callback dispatchOnAudioAvailable(String, byte[]) failed: " + e); 1593 } 1594 } 1595 1596 public void dispatchOnRangeStart( 1597 Object callerIdentity, String utteranceId, int start, int end, int frame) { 1598 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1599 if (cb == null) return; 1600 try { 1601 cb.onRangeStart(utteranceId, start, end, frame); 1602 } catch (RemoteException e) { 1603 Log.e(TAG, "Callback dispatchOnRangeStart(String, int, int, int) failed: " + e); 1604 } 1605 } 1606 1607 @Override 1608 public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { 1609 IBinder caller = (IBinder) cookie; 1610 synchronized (mCallerToCallback) { 1611 mCallerToCallback.remove(caller); 1612 } 1613 //mSynthHandler.stopForApp(caller); 1614 } 1615 1616 @Override 1617 public void kill() { 1618 synchronized (mCallerToCallback) { 1619 mCallerToCallback.clear(); 1620 super.kill(); 1621 } 1622 } 1623 1624 private ITextToSpeechCallback getCallbackFor(Object caller) { 1625 ITextToSpeechCallback cb; 1626 IBinder asBinder = (IBinder) caller; 1627 synchronized (mCallerToCallback) { 1628 cb = mCallerToCallback.get(asBinder); 1629 } 1630 1631 return cb; 1632 } 1633 } 1634 } 1635