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