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 SpeechItem setCurrentSpeechItem(SpeechItem speechItem) { 506 SpeechItem old = mCurrentSpeechItem; 507 mCurrentSpeechItem = speechItem; 508 return old; 509 } 510 511 private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) { 512 if (mCurrentSpeechItem != null && 513 (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) { 514 SpeechItem current = mCurrentSpeechItem; 515 mCurrentSpeechItem = null; 516 return current; 517 } 518 519 return null; 520 } 521 522 public boolean isSpeaking() { 523 return getCurrentSpeechItem() != null; 524 } 525 526 public void quit() { 527 // Don't process any more speech items 528 getLooper().quit(); 529 // Stop the current speech item 530 SpeechItem current = setCurrentSpeechItem(null); 531 if (current != null) { 532 current.stop(); 533 } 534 // The AudioPlaybackHandler will be destroyed by the caller. 535 } 536 537 /** 538 * Adds a speech item to the queue. 539 * 540 * Called on a service binder thread. 541 */ 542 public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) { 543 UtteranceProgressDispatcher utterenceProgress = null; 544 if (speechItem instanceof UtteranceProgressDispatcher) { 545 utterenceProgress = (UtteranceProgressDispatcher) speechItem; 546 } 547 548 if (!speechItem.isValid()) { 549 if (utterenceProgress != null) { 550 utterenceProgress.dispatchOnError( 551 TextToSpeech.ERROR_INVALID_REQUEST); 552 } 553 return TextToSpeech.ERROR; 554 } 555 556 if (queueMode == TextToSpeech.QUEUE_FLUSH) { 557 stopForApp(speechItem.getCallerIdentity()); 558 } else if (queueMode == TextToSpeech.QUEUE_DESTROY) { 559 stopAll(); 560 } 561 Runnable runnable = new Runnable() { 562 @Override 563 public void run() { 564 if (isFlushed(speechItem)) { 565 speechItem.stop(); 566 } else { 567 setCurrentSpeechItem(speechItem); 568 speechItem.play(); 569 setCurrentSpeechItem(null); 570 } 571 } 572 }; 573 Message msg = Message.obtain(this, runnable); 574 575 // The obj is used to remove all callbacks from the given app in 576 // stopForApp(String). 577 // 578 // Note that this string is interned, so the == comparison works. 579 msg.obj = speechItem.getCallerIdentity(); 580 581 if (sendMessage(msg)) { 582 return TextToSpeech.SUCCESS; 583 } else { 584 Log.w(TAG, "SynthThread has quit"); 585 if (utterenceProgress != null) { 586 utterenceProgress.dispatchOnError(TextToSpeech.ERROR_SERVICE); 587 } 588 return TextToSpeech.ERROR; 589 } 590 } 591 592 /** 593 * Stops all speech output and removes any utterances still in the queue for 594 * the calling app. 595 * 596 * Called on a service binder thread. 597 */ 598 public int stopForApp(final Object callerIdentity) { 599 if (callerIdentity == null) { 600 return TextToSpeech.ERROR; 601 } 602 603 // Flush pending messages from callerIdentity 604 startFlushingSpeechItems(callerIdentity); 605 606 // This stops writing data to the file / or publishing 607 // items to the audio playback handler. 608 // 609 // Note that the current speech item must be removed only if it 610 // belongs to the callingApp, else the item will be "orphaned" and 611 // not stopped correctly if a stop request comes along for the item 612 // from the app it belongs to. 613 SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity); 614 if (current != null) { 615 current.stop(); 616 } 617 618 // Remove any enqueued audio too. 619 mAudioPlaybackHandler.stopForApp(callerIdentity); 620 621 // Stop flushing pending messages 622 Runnable runnable = new Runnable() { 623 @Override 624 public void run() { 625 endFlushingSpeechItems(callerIdentity); 626 } 627 }; 628 sendMessage(Message.obtain(this, runnable)); 629 return TextToSpeech.SUCCESS; 630 } 631 632 public int stopAll() { 633 // Order to flush pending messages 634 startFlushingSpeechItems(null); 635 636 // Stop the current speech item unconditionally . 637 SpeechItem current = setCurrentSpeechItem(null); 638 if (current != null) { 639 current.stop(); 640 } 641 // Remove all pending playback as well. 642 mAudioPlaybackHandler.stop(); 643 644 // Message to stop flushing pending messages 645 Runnable runnable = new Runnable() { 646 @Override 647 public void run() { 648 endFlushingSpeechItems(null); 649 } 650 }; 651 sendMessage(Message.obtain(this, runnable)); 652 653 654 return TextToSpeech.SUCCESS; 655 } 656 } 657 658 interface UtteranceProgressDispatcher { 659 void dispatchOnStop(); 660 661 void dispatchOnSuccess(); 662 663 void dispatchOnStart(); 664 665 void dispatchOnError(int errorCode); 666 667 void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount); 668 669 void dispatchOnAudioAvailable(byte[] audio); 670 671 public void dispatchOnRangeStart(int start, int end, int frame); 672 } 673 674 /** Set of parameters affecting audio output. */ 675 static class AudioOutputParams { 676 /** 677 * Audio session identifier. May be used to associate audio playback with one of the 678 * {@link android.media.audiofx.AudioEffect} objects. If not specified by client, 679 * it should be equal to {@link AudioManager#AUDIO_SESSION_ID_GENERATE}. 680 */ 681 public final int mSessionId; 682 683 /** 684 * Volume, in the range [0.0f, 1.0f]. The default value is 685 * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f). 686 */ 687 public final float mVolume; 688 689 /** 690 * Left/right position of the audio, in the range [-1.0f, 1.0f]. 691 * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f). 692 */ 693 public final float mPan; 694 695 696 /** 697 * Audio attributes, set by {@link TextToSpeech#setAudioAttributes} 698 * or created from the value of {@link TextToSpeech.Engine#KEY_PARAM_STREAM}. 699 */ 700 public final AudioAttributes mAudioAttributes; 701 702 /** Create AudioOutputParams with default values */ 703 AudioOutputParams() { 704 mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE; 705 mVolume = Engine.DEFAULT_VOLUME; 706 mPan = Engine.DEFAULT_PAN; 707 mAudioAttributes = null; 708 } 709 710 AudioOutputParams(int sessionId, float volume, float pan, 711 AudioAttributes audioAttributes) { 712 mSessionId = sessionId; 713 mVolume = volume; 714 mPan = pan; 715 mAudioAttributes = audioAttributes; 716 } 717 718 /** Create AudioOutputParams from A {@link SynthesisRequest#getParams()} bundle */ 719 static AudioOutputParams createFromParamsBundle(Bundle paramsBundle, boolean isSpeech) { 720 if (paramsBundle == null) { 721 return new AudioOutputParams(); 722 } 723 724 AudioAttributes audioAttributes = 725 (AudioAttributes) paramsBundle.getParcelable( 726 Engine.KEY_PARAM_AUDIO_ATTRIBUTES); 727 if (audioAttributes == null) { 728 int streamType = paramsBundle.getInt( 729 Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); 730 audioAttributes = (new AudioAttributes.Builder()) 731 .setLegacyStreamType(streamType) 732 .setContentType((isSpeech ? 733 AudioAttributes.CONTENT_TYPE_SPEECH : 734 AudioAttributes.CONTENT_TYPE_SONIFICATION)) 735 .build(); 736 } 737 738 return new AudioOutputParams( 739 paramsBundle.getInt( 740 Engine.KEY_PARAM_SESSION_ID, 741 AudioManager.AUDIO_SESSION_ID_GENERATE), 742 paramsBundle.getFloat( 743 Engine.KEY_PARAM_VOLUME, 744 Engine.DEFAULT_VOLUME), 745 paramsBundle.getFloat( 746 Engine.KEY_PARAM_PAN, 747 Engine.DEFAULT_PAN), 748 audioAttributes); 749 } 750 } 751 752 753 /** 754 * An item in the synth thread queue. 755 */ 756 private abstract class SpeechItem { 757 private final Object mCallerIdentity; 758 private final int mCallerUid; 759 private final int mCallerPid; 760 private boolean mStarted = false; 761 private boolean mStopped = false; 762 763 public SpeechItem(Object caller, int callerUid, int callerPid) { 764 mCallerIdentity = caller; 765 mCallerUid = callerUid; 766 mCallerPid = callerPid; 767 } 768 769 public Object getCallerIdentity() { 770 return mCallerIdentity; 771 } 772 773 public int getCallerUid() { 774 return mCallerUid; 775 } 776 777 public int getCallerPid() { 778 return mCallerPid; 779 } 780 781 /** 782 * Checker whether the item is valid. If this method returns false, the item should not 783 * be played. 784 */ 785 public abstract boolean isValid(); 786 787 /** 788 * Plays the speech item. Blocks until playback is finished. 789 * Must not be called more than once. 790 * 791 * Only called on the synthesis thread. 792 */ 793 public void play() { 794 synchronized (this) { 795 if (mStarted) { 796 throw new IllegalStateException("play() called twice"); 797 } 798 mStarted = true; 799 } 800 playImpl(); 801 } 802 803 protected abstract void playImpl(); 804 805 /** 806 * Stops the speech item. 807 * Must not be called more than once. 808 * 809 * Can be called on multiple threads, but not on the synthesis thread. 810 */ 811 public void stop() { 812 synchronized (this) { 813 if (mStopped) { 814 throw new IllegalStateException("stop() called twice"); 815 } 816 mStopped = true; 817 } 818 stopImpl(); 819 } 820 821 protected abstract void stopImpl(); 822 823 protected synchronized boolean isStopped() { 824 return mStopped; 825 } 826 827 protected synchronized boolean isStarted() { 828 return mStarted; 829 } 830 } 831 832 /** 833 * An item in the synth thread queue that process utterance (and call back to client about 834 * progress). 835 */ 836 private abstract class UtteranceSpeechItem extends SpeechItem 837 implements UtteranceProgressDispatcher { 838 839 public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) { 840 super(caller, callerUid, callerPid); 841 } 842 843 @Override 844 public void dispatchOnSuccess() { 845 final String utteranceId = getUtteranceId(); 846 if (utteranceId != null) { 847 mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId); 848 } 849 } 850 851 @Override 852 public void dispatchOnStop() { 853 final String utteranceId = getUtteranceId(); 854 if (utteranceId != null) { 855 mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted()); 856 } 857 } 858 859 @Override 860 public void dispatchOnStart() { 861 final String utteranceId = getUtteranceId(); 862 if (utteranceId != null) { 863 mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId); 864 } 865 } 866 867 @Override 868 public void dispatchOnError(int errorCode) { 869 final String utteranceId = getUtteranceId(); 870 if (utteranceId != null) { 871 mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode); 872 } 873 } 874 875 @Override 876 public void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount) { 877 final String utteranceId = getUtteranceId(); 878 if (utteranceId != null) { 879 mCallbacks.dispatchOnBeginSynthesis(getCallerIdentity(), utteranceId, sampleRateInHz, audioFormat, channelCount); 880 } 881 } 882 883 @Override 884 public void dispatchOnAudioAvailable(byte[] audio) { 885 final String utteranceId = getUtteranceId(); 886 if (utteranceId != null) { 887 mCallbacks.dispatchOnAudioAvailable(getCallerIdentity(), utteranceId, audio); 888 } 889 } 890 891 @Override 892 public void dispatchOnRangeStart(int start, int end, int frame) { 893 final String utteranceId = getUtteranceId(); 894 if (utteranceId != null) { 895 mCallbacks.dispatchOnRangeStart( 896 getCallerIdentity(), utteranceId, start, end, frame); 897 } 898 } 899 900 abstract public String getUtteranceId(); 901 902 String getStringParam(Bundle params, String key, String defaultValue) { 903 return params == null ? defaultValue : params.getString(key, defaultValue); 904 } 905 906 int getIntParam(Bundle params, String key, int defaultValue) { 907 return params == null ? defaultValue : params.getInt(key, defaultValue); 908 } 909 910 float getFloatParam(Bundle params, String key, float defaultValue) { 911 return params == null ? defaultValue : params.getFloat(key, defaultValue); 912 } 913 } 914 915 /** 916 * Synthesis parameters are kept in a single Bundle passed as parameter. This class allow 917 * subclasses to access them conveniently. 918 */ 919 private abstract class UtteranceSpeechItemWithParams extends UtteranceSpeechItem { 920 protected final Bundle mParams; 921 protected final String mUtteranceId; 922 923 UtteranceSpeechItemWithParams( 924 Object callerIdentity, 925 int callerUid, 926 int callerPid, 927 Bundle params, 928 String utteranceId) { 929 super(callerIdentity, callerUid, callerPid); 930 mParams = params; 931 mUtteranceId = utteranceId; 932 } 933 934 boolean hasLanguage() { 935 return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null)); 936 } 937 938 int getSpeechRate() { 939 return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); 940 } 941 942 int getPitch() { 943 return getIntParam(mParams, Engine.KEY_PARAM_PITCH, getDefaultPitch()); 944 } 945 946 @Override 947 public String getUtteranceId() { 948 return mUtteranceId; 949 } 950 951 AudioOutputParams getAudioParams() { 952 return AudioOutputParams.createFromParamsBundle(mParams, true); 953 } 954 } 955 956 class SynthesisSpeechItem extends UtteranceSpeechItemWithParams { 957 // Never null. 958 private final CharSequence mText; 959 private final SynthesisRequest mSynthesisRequest; 960 private final String[] mDefaultLocale; 961 // Non null after synthesis has started, and all accesses 962 // guarded by 'this'. 963 private AbstractSynthesisCallback mSynthesisCallback; 964 private final EventLogger mEventLogger; 965 private final int mCallerUid; 966 967 public SynthesisSpeechItem( 968 Object callerIdentity, 969 int callerUid, 970 int callerPid, 971 Bundle params, 972 String utteranceId, 973 CharSequence text) { 974 super(callerIdentity, callerUid, callerPid, params, utteranceId); 975 mText = text; 976 mCallerUid = callerUid; 977 mSynthesisRequest = new SynthesisRequest(mText, mParams); 978 mDefaultLocale = getSettingsLocale(); 979 setRequestParams(mSynthesisRequest); 980 mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid, mPackageName); 981 } 982 983 public CharSequence getText() { 984 return mText; 985 } 986 987 @Override 988 public boolean isValid() { 989 if (mText == null) { 990 Log.e(TAG, "null synthesis text"); 991 return false; 992 } 993 if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) { 994 Log.w(TAG, "Text too long: " + mText.length() + " chars"); 995 return false; 996 } 997 return true; 998 } 999 1000 @Override 1001 protected void playImpl() { 1002 AbstractSynthesisCallback synthesisCallback; 1003 mEventLogger.onRequestProcessingStart(); 1004 synchronized (this) { 1005 // stop() might have been called before we enter this 1006 // synchronized block. 1007 if (isStopped()) { 1008 return; 1009 } 1010 mSynthesisCallback = createSynthesisCallback(); 1011 synthesisCallback = mSynthesisCallback; 1012 } 1013 1014 TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); 1015 1016 // Fix for case where client called .start() & .error(), but did not called .done() 1017 if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) { 1018 synthesisCallback.done(); 1019 } 1020 } 1021 1022 protected AbstractSynthesisCallback createSynthesisCallback() { 1023 return new PlaybackSynthesisCallback(getAudioParams(), 1024 mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false); 1025 } 1026 1027 private void setRequestParams(SynthesisRequest request) { 1028 String voiceName = getVoiceName(); 1029 request.setLanguage(getLanguage(), getCountry(), getVariant()); 1030 if (!TextUtils.isEmpty(voiceName)) { 1031 request.setVoiceName(getVoiceName()); 1032 } 1033 request.setSpeechRate(getSpeechRate()); 1034 request.setCallerUid(mCallerUid); 1035 request.setPitch(getPitch()); 1036 } 1037 1038 @Override 1039 protected void stopImpl() { 1040 AbstractSynthesisCallback synthesisCallback; 1041 synchronized (this) { 1042 synthesisCallback = mSynthesisCallback; 1043 } 1044 if (synthesisCallback != null) { 1045 // If the synthesis callback is null, it implies that we haven't 1046 // entered the synchronized(this) block in playImpl which in 1047 // turn implies that synthesis would not have started. 1048 synthesisCallback.stop(); 1049 TextToSpeechService.this.onStop(); 1050 } else { 1051 dispatchOnStop(); 1052 } 1053 } 1054 1055 private String getCountry() { 1056 if (!hasLanguage()) return mDefaultLocale[1]; 1057 return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, ""); 1058 } 1059 1060 private String getVariant() { 1061 if (!hasLanguage()) return mDefaultLocale[2]; 1062 return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, ""); 1063 } 1064 1065 public String getLanguage() { 1066 return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); 1067 } 1068 1069 public String getVoiceName() { 1070 return getStringParam(mParams, Engine.KEY_PARAM_VOICE_NAME, ""); 1071 } 1072 } 1073 1074 private class SynthesisToFileOutputStreamSpeechItem extends SynthesisSpeechItem { 1075 private final FileOutputStream mFileOutputStream; 1076 1077 public SynthesisToFileOutputStreamSpeechItem( 1078 Object callerIdentity, 1079 int callerUid, 1080 int callerPid, 1081 Bundle params, 1082 String utteranceId, 1083 CharSequence text, 1084 FileOutputStream fileOutputStream) { 1085 super(callerIdentity, callerUid, callerPid, params, utteranceId, text); 1086 mFileOutputStream = fileOutputStream; 1087 } 1088 1089 @Override 1090 protected AbstractSynthesisCallback createSynthesisCallback() { 1091 return new FileSynthesisCallback(mFileOutputStream.getChannel(), this, false); 1092 } 1093 1094 @Override 1095 protected void playImpl() { 1096 dispatchOnStart(); 1097 super.playImpl(); 1098 try { 1099 mFileOutputStream.close(); 1100 } catch(IOException e) { 1101 Log.w(TAG, "Failed to close output file", e); 1102 } 1103 } 1104 } 1105 1106 private class AudioSpeechItem extends UtteranceSpeechItemWithParams { 1107 private final AudioPlaybackQueueItem mItem; 1108 1109 public AudioSpeechItem( 1110 Object callerIdentity, 1111 int callerUid, 1112 int callerPid, 1113 Bundle params, 1114 String utteranceId, 1115 Uri uri) { 1116 super(callerIdentity, callerUid, callerPid, params, utteranceId); 1117 mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), 1118 TextToSpeechService.this, uri, getAudioParams()); 1119 } 1120 1121 @Override 1122 public boolean isValid() { 1123 return true; 1124 } 1125 1126 @Override 1127 protected void playImpl() { 1128 mAudioPlaybackHandler.enqueue(mItem); 1129 } 1130 1131 @Override 1132 protected void stopImpl() { 1133 // Do nothing. 1134 } 1135 1136 @Override 1137 public String getUtteranceId() { 1138 return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null); 1139 } 1140 1141 @Override 1142 AudioOutputParams getAudioParams() { 1143 return AudioOutputParams.createFromParamsBundle(mParams, false); 1144 } 1145 } 1146 1147 private class SilenceSpeechItem extends UtteranceSpeechItem { 1148 private final long mDuration; 1149 private final String mUtteranceId; 1150 1151 public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, 1152 String utteranceId, long duration) { 1153 super(callerIdentity, callerUid, callerPid); 1154 mUtteranceId = utteranceId; 1155 mDuration = duration; 1156 } 1157 1158 @Override 1159 public boolean isValid() { 1160 return true; 1161 } 1162 1163 @Override 1164 protected void playImpl() { 1165 mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( 1166 this, getCallerIdentity(), mDuration)); 1167 } 1168 1169 @Override 1170 protected void stopImpl() { 1171 1172 } 1173 1174 @Override 1175 public String getUtteranceId() { 1176 return mUtteranceId; 1177 } 1178 } 1179 1180 /** 1181 * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. 1182 */ 1183 private class LoadLanguageItem extends SpeechItem { 1184 private final String mLanguage; 1185 private final String mCountry; 1186 private final String mVariant; 1187 1188 public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, 1189 String language, String country, String variant) { 1190 super(callerIdentity, callerUid, callerPid); 1191 mLanguage = language; 1192 mCountry = country; 1193 mVariant = variant; 1194 } 1195 1196 @Override 1197 public boolean isValid() { 1198 return true; 1199 } 1200 1201 @Override 1202 protected void playImpl() { 1203 TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); 1204 } 1205 1206 @Override 1207 protected void stopImpl() { 1208 // No-op 1209 } 1210 } 1211 1212 /** 1213 * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. 1214 */ 1215 private class LoadVoiceItem extends SpeechItem { 1216 private final String mVoiceName; 1217 1218 public LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid, 1219 String voiceName) { 1220 super(callerIdentity, callerUid, callerPid); 1221 mVoiceName = voiceName; 1222 } 1223 1224 @Override 1225 public boolean isValid() { 1226 return true; 1227 } 1228 1229 @Override 1230 protected void playImpl() { 1231 TextToSpeechService.this.onLoadVoice(mVoiceName); 1232 } 1233 1234 @Override 1235 protected void stopImpl() { 1236 // No-op 1237 } 1238 } 1239 1240 1241 @Override 1242 public IBinder onBind(Intent intent) { 1243 if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { 1244 return mBinder; 1245 } 1246 return null; 1247 } 1248 1249 /** 1250 * Binder returned from {@code #onBind(Intent)}. The methods in this class can be called called 1251 * from several different threads. 1252 */ 1253 // NOTE: All calls that are passed in a calling app are interned so that 1254 // they can be used as message objects (which are tested for equality using ==). 1255 private final ITextToSpeechService.Stub mBinder = 1256 new ITextToSpeechService.Stub() { 1257 @Override 1258 public int speak( 1259 IBinder caller, 1260 CharSequence text, 1261 int queueMode, 1262 Bundle params, 1263 String utteranceId) { 1264 if (!checkNonNull(caller, text, params)) { 1265 return TextToSpeech.ERROR; 1266 } 1267 1268 SpeechItem item = 1269 new SynthesisSpeechItem( 1270 caller, 1271 Binder.getCallingUid(), 1272 Binder.getCallingPid(), 1273 params, 1274 utteranceId, 1275 text); 1276 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1277 } 1278 1279 @Override 1280 public int synthesizeToFileDescriptor( 1281 IBinder caller, 1282 CharSequence text, 1283 ParcelFileDescriptor fileDescriptor, 1284 Bundle params, 1285 String utteranceId) { 1286 if (!checkNonNull(caller, text, fileDescriptor, params)) { 1287 return TextToSpeech.ERROR; 1288 } 1289 1290 // In test env, ParcelFileDescriptor instance may be EXACTLY the same 1291 // one that is used by client. And it will be closed by a client, thus 1292 // preventing us from writing anything to it. 1293 final ParcelFileDescriptor sameFileDescriptor = 1294 ParcelFileDescriptor.adoptFd(fileDescriptor.detachFd()); 1295 1296 SpeechItem item = 1297 new SynthesisToFileOutputStreamSpeechItem( 1298 caller, 1299 Binder.getCallingUid(), 1300 Binder.getCallingPid(), 1301 params, 1302 utteranceId, 1303 text, 1304 new ParcelFileDescriptor.AutoCloseOutputStream( 1305 sameFileDescriptor)); 1306 return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); 1307 } 1308 1309 @Override 1310 public int playAudio( 1311 IBinder caller, 1312 Uri audioUri, 1313 int queueMode, 1314 Bundle params, 1315 String utteranceId) { 1316 if (!checkNonNull(caller, audioUri, params)) { 1317 return TextToSpeech.ERROR; 1318 } 1319 1320 SpeechItem item = 1321 new AudioSpeechItem( 1322 caller, 1323 Binder.getCallingUid(), 1324 Binder.getCallingPid(), 1325 params, 1326 utteranceId, 1327 audioUri); 1328 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1329 } 1330 1331 @Override 1332 public int playSilence( 1333 IBinder caller, long duration, int queueMode, String utteranceId) { 1334 if (!checkNonNull(caller)) { 1335 return TextToSpeech.ERROR; 1336 } 1337 1338 SpeechItem item = 1339 new SilenceSpeechItem( 1340 caller, 1341 Binder.getCallingUid(), 1342 Binder.getCallingPid(), 1343 utteranceId, 1344 duration); 1345 return mSynthHandler.enqueueSpeechItem(queueMode, item); 1346 } 1347 1348 @Override 1349 public boolean isSpeaking() { 1350 return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); 1351 } 1352 1353 @Override 1354 public int stop(IBinder caller) { 1355 if (!checkNonNull(caller)) { 1356 return TextToSpeech.ERROR; 1357 } 1358 1359 return mSynthHandler.stopForApp(caller); 1360 } 1361 1362 @Override 1363 public String[] getLanguage() { 1364 return onGetLanguage(); 1365 } 1366 1367 @Override 1368 public String[] getClientDefaultLanguage() { 1369 return getSettingsLocale(); 1370 } 1371 1372 /* 1373 * If defaults are enforced, then no language is "available" except 1374 * perhaps the default language selected by the user. 1375 */ 1376 @Override 1377 public int isLanguageAvailable(String lang, String country, String variant) { 1378 if (!checkNonNull(lang)) { 1379 return TextToSpeech.ERROR; 1380 } 1381 1382 return onIsLanguageAvailable(lang, country, variant); 1383 } 1384 1385 @Override 1386 public String[] getFeaturesForLanguage( 1387 String lang, String country, String variant) { 1388 Set<String> features = onGetFeaturesForLanguage(lang, country, variant); 1389 String[] featuresArray = null; 1390 if (features != null) { 1391 featuresArray = new String[features.size()]; 1392 features.toArray(featuresArray); 1393 } else { 1394 featuresArray = new String[0]; 1395 } 1396 return featuresArray; 1397 } 1398 1399 /* 1400 * There is no point loading a non default language if defaults 1401 * are enforced. 1402 */ 1403 @Override 1404 public int loadLanguage( 1405 IBinder caller, String lang, String country, String variant) { 1406 if (!checkNonNull(lang)) { 1407 return TextToSpeech.ERROR; 1408 } 1409 int retVal = onIsLanguageAvailable(lang, country, variant); 1410 1411 if (retVal == TextToSpeech.LANG_AVAILABLE 1412 || retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE 1413 || retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 1414 1415 SpeechItem item = 1416 new LoadLanguageItem( 1417 caller, 1418 Binder.getCallingUid(), 1419 Binder.getCallingPid(), 1420 lang, 1421 country, 1422 variant); 1423 1424 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) 1425 != TextToSpeech.SUCCESS) { 1426 return TextToSpeech.ERROR; 1427 } 1428 } 1429 return retVal; 1430 } 1431 1432 @Override 1433 public List<Voice> getVoices() { 1434 return onGetVoices(); 1435 } 1436 1437 @Override 1438 public int loadVoice(IBinder caller, String voiceName) { 1439 if (!checkNonNull(voiceName)) { 1440 return TextToSpeech.ERROR; 1441 } 1442 int retVal = onIsValidVoiceName(voiceName); 1443 1444 if (retVal == TextToSpeech.SUCCESS) { 1445 SpeechItem item = 1446 new LoadVoiceItem( 1447 caller, 1448 Binder.getCallingUid(), 1449 Binder.getCallingPid(), 1450 voiceName); 1451 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) 1452 != TextToSpeech.SUCCESS) { 1453 return TextToSpeech.ERROR; 1454 } 1455 } 1456 return retVal; 1457 } 1458 1459 public String getDefaultVoiceNameFor(String lang, String country, String variant) { 1460 if (!checkNonNull(lang)) { 1461 return null; 1462 } 1463 int retVal = onIsLanguageAvailable(lang, country, variant); 1464 1465 if (retVal == TextToSpeech.LANG_AVAILABLE 1466 || retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE 1467 || retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 1468 return onGetDefaultVoiceNameFor(lang, country, variant); 1469 } else { 1470 return null; 1471 } 1472 } 1473 1474 @Override 1475 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 1476 // Note that passing in a null callback is a valid use case. 1477 if (!checkNonNull(caller)) { 1478 return; 1479 } 1480 1481 mCallbacks.setCallback(caller, cb); 1482 } 1483 1484 private String intern(String in) { 1485 // The input parameter will be non null. 1486 return in.intern(); 1487 } 1488 1489 private boolean checkNonNull(Object... args) { 1490 for (Object o : args) { 1491 if (o == null) return false; 1492 } 1493 return true; 1494 } 1495 }; 1496 1497 private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { 1498 private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback 1499 = new HashMap<IBinder, ITextToSpeechCallback>(); 1500 1501 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 1502 synchronized (mCallerToCallback) { 1503 ITextToSpeechCallback old; 1504 if (cb != null) { 1505 register(cb, caller); 1506 old = mCallerToCallback.put(caller, cb); 1507 } else { 1508 old = mCallerToCallback.remove(caller); 1509 } 1510 if (old != null && old != cb) { 1511 unregister(old); 1512 } 1513 } 1514 } 1515 1516 public void dispatchOnStop(Object callerIdentity, String utteranceId, boolean started) { 1517 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1518 if (cb == null) return; 1519 try { 1520 cb.onStop(utteranceId, started); 1521 } catch (RemoteException e) { 1522 Log.e(TAG, "Callback onStop failed: " + e); 1523 } 1524 } 1525 1526 public void dispatchOnSuccess(Object callerIdentity, String utteranceId) { 1527 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1528 if (cb == null) return; 1529 try { 1530 cb.onSuccess(utteranceId); 1531 } catch (RemoteException e) { 1532 Log.e(TAG, "Callback onDone failed: " + e); 1533 } 1534 } 1535 1536 public void dispatchOnStart(Object callerIdentity, String utteranceId) { 1537 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1538 if (cb == null) return; 1539 try { 1540 cb.onStart(utteranceId); 1541 } catch (RemoteException e) { 1542 Log.e(TAG, "Callback onStart failed: " + e); 1543 } 1544 } 1545 1546 public void dispatchOnError(Object callerIdentity, String utteranceId, 1547 int errorCode) { 1548 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1549 if (cb == null) return; 1550 try { 1551 cb.onError(utteranceId, errorCode); 1552 } catch (RemoteException e) { 1553 Log.e(TAG, "Callback onError failed: " + e); 1554 } 1555 } 1556 1557 public void dispatchOnBeginSynthesis(Object callerIdentity, String utteranceId, int sampleRateInHz, int audioFormat, int channelCount) { 1558 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1559 if (cb == null) return; 1560 try { 1561 cb.onBeginSynthesis(utteranceId, sampleRateInHz, audioFormat, channelCount); 1562 } catch (RemoteException e) { 1563 Log.e(TAG, "Callback dispatchOnBeginSynthesis(String, int, int, int) failed: " + e); 1564 } 1565 } 1566 1567 public void dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer) { 1568 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1569 if (cb == null) return; 1570 try { 1571 cb.onAudioAvailable(utteranceId, buffer); 1572 } catch (RemoteException e) { 1573 Log.e(TAG, "Callback dispatchOnAudioAvailable(String, byte[]) failed: " + e); 1574 } 1575 } 1576 1577 public void dispatchOnRangeStart( 1578 Object callerIdentity, String utteranceId, int start, int end, int frame) { 1579 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 1580 if (cb == null) return; 1581 try { 1582 cb.onRangeStart(utteranceId, start, end, frame); 1583 } catch (RemoteException e) { 1584 Log.e(TAG, "Callback dispatchOnRangeStart(String, int, int, int) failed: " + e); 1585 } 1586 } 1587 1588 @Override 1589 public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { 1590 IBinder caller = (IBinder) cookie; 1591 synchronized (mCallerToCallback) { 1592 mCallerToCallback.remove(caller); 1593 } 1594 //mSynthHandler.stopForApp(caller); 1595 } 1596 1597 @Override 1598 public void kill() { 1599 synchronized (mCallerToCallback) { 1600 mCallerToCallback.clear(); 1601 super.kill(); 1602 } 1603 } 1604 1605 private ITextToSpeechCallback getCallbackFor(Object caller) { 1606 ITextToSpeechCallback cb; 1607 IBinder asBinder = (IBinder) caller; 1608 synchronized (mCallerToCallback) { 1609 cb = mCallerToCallback.get(asBinder); 1610 } 1611 1612 return cb; 1613 } 1614 } 1615 } 1616