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.app.Service; 19 import android.content.Intent; 20 import android.net.Uri; 21 import android.os.Binder; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.os.HandlerThread; 25 import android.os.IBinder; 26 import android.os.Looper; 27 import android.os.Message; 28 import android.os.MessageQueue; 29 import android.os.ParcelFileDescriptor; 30 import android.os.RemoteCallbackList; 31 import android.os.RemoteException; 32 import android.provider.Settings; 33 import android.speech.tts.TextToSpeech.Engine; 34 import android.text.TextUtils; 35 import android.util.Log; 36 37 import java.io.FileDescriptor; 38 import java.io.FileOutputStream; 39 import java.io.IOException; 40 import java.util.HashMap; 41 import java.util.Locale; 42 import java.util.Set; 43 44 45 /** 46 * Abstract base class for TTS engine implementations. The following methods 47 * need to be implemented. 48 * 49 * <ul> 50 * <li>{@link #onIsLanguageAvailable}</li> 51 * <li>{@link #onLoadLanguage}</li> 52 * <li>{@link #onGetLanguage}</li> 53 * <li>{@link #onSynthesizeText}</li> 54 * <li>{@link #onStop}</li> 55 * </ul> 56 * 57 * The first three deal primarily with language management, and are used to 58 * query the engine for it's support for a given language and indicate to it 59 * that requests in a given language are imminent. 60 * 61 * {@link #onSynthesizeText} is central to the engine implementation. The 62 * implementation should synthesize text as per the request parameters and 63 * return synthesized data via the supplied callback. This class and its helpers 64 * will then consume that data, which might mean queueing it for playback or writing 65 * it to a file or similar. All calls to this method will be on a single 66 * thread, which will be different from the main thread of the service. Synthesis 67 * must be synchronous which means the engine must NOT hold on the callback or call 68 * any methods on it after the method returns 69 * 70 * {@link #onStop} tells the engine that it should stop all ongoing synthesis, if 71 * any. Any pending data from the current synthesis will be discarded. 72 * 73 */ 74 public abstract class TextToSpeechService extends Service { 75 76 private static final boolean DBG = false; 77 private static final String TAG = "TextToSpeechService"; 78 79 80 private static final String SYNTH_THREAD_NAME = "SynthThread"; 81 82 private SynthHandler mSynthHandler; 83 // A thread and it's associated handler for playing back any audio 84 // associated with this TTS engine. Will handle all requests except synthesis 85 // to file requests, which occur on the synthesis thread. 86 private AudioPlaybackHandler mAudioPlaybackHandler; 87 private TtsEngines mEngineHelper; 88 89 private CallbackMap mCallbacks; 90 private String mPackageName; 91 92 @Override 93 public void onCreate() { 94 if (DBG) Log.d(TAG, "onCreate()"); 95 super.onCreate(); 96 97 SynthThread synthThread = new SynthThread(); 98 synthThread.start(); 99 mSynthHandler = new SynthHandler(synthThread.getLooper()); 100 101 mAudioPlaybackHandler = new AudioPlaybackHandler(); 102 mAudioPlaybackHandler.start(); 103 104 mEngineHelper = new TtsEngines(this); 105 106 mCallbacks = new CallbackMap(); 107 108 mPackageName = getApplicationInfo().packageName; 109 110 String[] defaultLocale = getSettingsLocale(); 111 // Load default language 112 onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]); 113 } 114 115 @Override 116 public void onDestroy() { 117 if (DBG) Log.d(TAG, "onDestroy()"); 118 119 // Tell the synthesizer to stop 120 mSynthHandler.quit(); 121 // Tell the audio playback thread to stop. 122 mAudioPlaybackHandler.quit(); 123 // Unregister all callbacks. 124 mCallbacks.kill(); 125 126 super.onDestroy(); 127 } 128 129 /** 130 * Checks whether the engine supports a given language. 131 * 132 * Can be called on multiple threads. 133 * 134 * Its return values HAVE to be consistent with onLoadLanguage. 135 * 136 * @param lang ISO-3 language code. 137 * @param country ISO-3 country code. May be empty or null. 138 * @param variant Language variant. May be empty or null. 139 * @return Code indicating the support status for the locale. 140 * One of {@link TextToSpeech#LANG_AVAILABLE}, 141 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 142 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 143 * {@link TextToSpeech#LANG_MISSING_DATA} 144 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 145 */ 146 protected abstract int onIsLanguageAvailable(String lang, String country, String variant); 147 148 /** 149 * Returns the language, country and variant currently being used by the TTS engine. 150 * 151 * Can be called on multiple threads. 152 * 153 * @return A 3-element array, containing language (ISO 3-letter code), 154 * country (ISO 3-letter code) and variant used by the engine. 155 * The country and variant may be {@code ""}. If country is empty, then variant must 156 * be empty too. 157 * @see Locale#getISO3Language() 158 * @see Locale#getISO3Country() 159 * @see Locale#getVariant() 160 */ 161 protected abstract String[] onGetLanguage(); 162 163 /** 164 * Notifies the engine that it should load a speech synthesis language. There is no guarantee 165 * that this method is always called before the language is used for synthesis. It is merely 166 * a hint to the engine that it will probably get some synthesis requests for this language 167 * at some point in the future. 168 * 169 * Can be called on multiple threads. 170 * In <= Android 4.2 (<= API 17) can be called on main and service binder threads. 171 * In > Android 4.2 (> API 17) can be called on main and synthesis threads. 172 * 173 * @param lang ISO-3 language code. 174 * @param country ISO-3 country code. May be empty or null. 175 * @param variant Language variant. May be empty or null. 176 * @return Code indicating the support status for the locale. 177 * One of {@link TextToSpeech#LANG_AVAILABLE}, 178 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 179 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 180 * {@link TextToSpeech#LANG_MISSING_DATA} 181 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 182 */ 183 protected abstract int onLoadLanguage(String lang, String country, String variant); 184 185 /** 186 * Notifies the service that it should stop any in-progress speech synthesis. 187 * This method can be called even if no speech synthesis is currently in progress. 188 * 189 * Can be called on multiple threads, but not on the synthesis thread. 190 */ 191 protected abstract void onStop(); 192 193 /** 194 * Tells the service to synthesize speech from the given text. This method should 195 * block until the synthesis is finished. 196 * 197 * Called on the synthesis thread. 198 * 199 * @param request The synthesis request. 200 * @param callback The callback the the engine must use to make data available for 201 * playback or for writing to a file. 202 */ 203 protected abstract void onSynthesizeText(SynthesisRequest request, 204 SynthesisCallback callback); 205 206 /** 207 * Queries the service for a set of features supported for a given language. 208 * 209 * @param lang ISO-3 language code. 210 * @param country ISO-3 country code. May be empty or null. 211 * @param variant Language variant. May be empty or null. 212 * @return A list of features supported for the given language. 213 */ 214 protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) { 215 return null; 216 } 217 218 private int getDefaultSpeechRate() { 219 return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE); 220 } 221 222 private String[] getSettingsLocale() { 223 final String locale = mEngineHelper.getLocalePrefForEngine(mPackageName); 224 return TtsEngines.parseLocalePref(locale); 225 } 226 227 private int getSecureSettingInt(String name, int defaultValue) { 228 return Settings.Secure.getInt(getContentResolver(), name, defaultValue); 229 } 230 231 /** 232 * Synthesizer thread. This thread is used to run {@link SynthHandler}. 233 */ 234 private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler { 235 236 private boolean mFirstIdle = true; 237 238 public SynthThread() { 239 super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_DEFAULT); 240 } 241 242 @Override 243 protected void onLooperPrepared() { 244 getLooper().getQueue().addIdleHandler(this); 245 } 246 247 @Override 248 public boolean queueIdle() { 249 if (mFirstIdle) { 250 mFirstIdle = false; 251 } else { 252 broadcastTtsQueueProcessingCompleted(); 253 } 254 return true; 255 } 256 257 private void broadcastTtsQueueProcessingCompleted() { 258 Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED); 259 if (DBG) Log.d(TAG, "Broadcasting: " + i); 260 sendBroadcast(i); 261 } 262 } 263 264 private class SynthHandler extends Handler { 265 private SpeechItem mCurrentSpeechItem = null; 266 267 public SynthHandler(Looper looper) { 268 super(looper); 269 } 270 271 private synchronized SpeechItem getCurrentSpeechItem() { 272 return mCurrentSpeechItem; 273 } 274 275 private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) { 276 SpeechItem old = mCurrentSpeechItem; 277 mCurrentSpeechItem = speechItem; 278 return old; 279 } 280 281 private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) { 282 if (mCurrentSpeechItem != null && 283 (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) { 284 SpeechItem current = mCurrentSpeechItem; 285 mCurrentSpeechItem = null; 286 return current; 287 } 288 289 return null; 290 } 291 292 public boolean isSpeaking() { 293 return getCurrentSpeechItem() != null; 294 } 295 296 public void quit() { 297 // Don't process any more speech items 298 getLooper().quit(); 299 // Stop the current speech item 300 SpeechItem current = setCurrentSpeechItem(null); 301 if (current != null) { 302 current.stop(); 303 } 304 // The AudioPlaybackHandler will be destroyed by the caller. 305 } 306 307 /** 308 * Adds a speech item to the queue. 309 * 310 * Called on a service binder thread. 311 */ 312 public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) { 313 UtteranceProgressDispatcher utterenceProgress = null; 314 if (speechItem instanceof UtteranceProgressDispatcher) { 315 utterenceProgress = (UtteranceProgressDispatcher) speechItem; 316 } 317 318 if (!speechItem.isValid()) { 319 if (utterenceProgress != null) { 320 utterenceProgress.dispatchOnError(); 321 } 322 return TextToSpeech.ERROR; 323 } 324 325 if (queueMode == TextToSpeech.QUEUE_FLUSH) { 326 stopForApp(speechItem.getCallerIdentity()); 327 } else if (queueMode == TextToSpeech.QUEUE_DESTROY) { 328 stopAll(); 329 } 330 Runnable runnable = new Runnable() { 331 @Override 332 public void run() { 333 setCurrentSpeechItem(speechItem); 334 speechItem.play(); 335 setCurrentSpeechItem(null); 336 } 337 }; 338 Message msg = Message.obtain(this, runnable); 339 340 // The obj is used to remove all callbacks from the given app in 341 // stopForApp(String). 342 // 343 // Note that this string is interned, so the == comparison works. 344 msg.obj = speechItem.getCallerIdentity(); 345 if (sendMessage(msg)) { 346 return TextToSpeech.SUCCESS; 347 } else { 348 Log.w(TAG, "SynthThread has quit"); 349 if (utterenceProgress != null) { 350 utterenceProgress.dispatchOnError(); 351 } 352 return TextToSpeech.ERROR; 353 } 354 } 355 356 /** 357 * Stops all speech output and removes any utterances still in the queue for 358 * the calling app. 359 * 360 * Called on a service binder thread. 361 */ 362 public int stopForApp(Object callerIdentity) { 363 if (callerIdentity == null) { 364 return TextToSpeech.ERROR; 365 } 366 367 removeCallbacksAndMessages(callerIdentity); 368 // This stops writing data to the file / or publishing 369 // items to the audio playback handler. 370 // 371 // Note that the current speech item must be removed only if it 372 // belongs to the callingApp, else the item will be "orphaned" and 373 // not stopped correctly if a stop request comes along for the item 374 // from the app it belongs to. 375 SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity); 376 if (current != null) { 377 current.stop(); 378 } 379 380 // Remove any enqueued audio too. 381 mAudioPlaybackHandler.stopForApp(callerIdentity); 382 383 return TextToSpeech.SUCCESS; 384 } 385 386 public int stopAll() { 387 // Stop the current speech item unconditionally . 388 SpeechItem current = setCurrentSpeechItem(null); 389 if (current != null) { 390 current.stop(); 391 } 392 // Remove all other items from the queue. 393 removeCallbacksAndMessages(null); 394 // Remove all pending playback as well. 395 mAudioPlaybackHandler.stop(); 396 397 return TextToSpeech.SUCCESS; 398 } 399 } 400 401 interface UtteranceProgressDispatcher { 402 public void dispatchOnDone(); 403 public void dispatchOnStart(); 404 public void dispatchOnError(); 405 } 406 407 /** 408 * An item in the synth thread queue. 409 */ 410 private abstract class SpeechItem { 411 private final Object mCallerIdentity; 412 protected final Bundle mParams; 413 private final int mCallerUid; 414 private final int mCallerPid; 415 private boolean mStarted = false; 416 private boolean mStopped = false; 417 418 public SpeechItem(Object caller, int callerUid, int callerPid, Bundle params) { 419 mCallerIdentity = caller; 420 mParams = params; 421 mCallerUid = callerUid; 422 mCallerPid = callerPid; 423 } 424 425 public Object getCallerIdentity() { 426 return mCallerIdentity; 427 } 428 429 430 public int getCallerUid() { 431 return mCallerUid; 432 } 433 434 public int getCallerPid() { 435 return mCallerPid; 436 } 437 438 /** 439 * Checker whether the item is valid. If this method returns false, the item should not 440 * be played. 441 */ 442 public abstract boolean isValid(); 443 444 /** 445 * Plays the speech item. Blocks until playback is finished. 446 * Must not be called more than once. 447 * 448 * Only called on the synthesis thread. 449 * 450 * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. 451 */ 452 public int play() { 453 synchronized (this) { 454 if (mStarted) { 455 throw new IllegalStateException("play() called twice"); 456 } 457 mStarted = true; 458 } 459 return playImpl(); 460 } 461 462 protected abstract int playImpl(); 463 464 /** 465 * Stops the speech item. 466 * Must not be called more than once. 467 * 468 * Can be called on multiple threads, but not on the synthesis thread. 469 */ 470 public void stop() { 471 synchronized (this) { 472 if (mStopped) { 473 throw new IllegalStateException("stop() called twice"); 474 } 475 mStopped = true; 476 } 477 stopImpl(); 478 } 479 480 protected abstract void stopImpl(); 481 482 protected synchronized boolean isStopped() { 483 return mStopped; 484 } 485 } 486 487 /** 488 * An item in the synth thread queue that process utterance. 489 */ 490 private abstract class UtteranceSpeechItem extends SpeechItem 491 implements UtteranceProgressDispatcher { 492 493 public UtteranceSpeechItem(Object caller, int callerUid, int callerPid, Bundle params) { 494 super(caller, callerUid, callerPid, params); 495 } 496 497 @Override 498 public void dispatchOnDone() { 499 final String utteranceId = getUtteranceId(); 500 if (utteranceId != null) { 501 mCallbacks.dispatchOnDone(getCallerIdentity(), utteranceId); 502 } 503 } 504 505 @Override 506 public void dispatchOnStart() { 507 final String utteranceId = getUtteranceId(); 508 if (utteranceId != null) { 509 mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId); 510 } 511 } 512 513 @Override 514 public void dispatchOnError() { 515 final String utteranceId = getUtteranceId(); 516 if (utteranceId != null) { 517 mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId); 518 } 519 } 520 521 public int getStreamType() { 522 return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); 523 } 524 525 public float getVolume() { 526 return getFloatParam(Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME); 527 } 528 529 public float getPan() { 530 return getFloatParam(Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN); 531 } 532 533 public String getUtteranceId() { 534 return getStringParam(Engine.KEY_PARAM_UTTERANCE_ID, null); 535 } 536 537 protected String getStringParam(String key, String defaultValue) { 538 return mParams == null ? defaultValue : mParams.getString(key, defaultValue); 539 } 540 541 protected int getIntParam(String key, int defaultValue) { 542 return mParams == null ? defaultValue : mParams.getInt(key, defaultValue); 543 } 544 545 protected float getFloatParam(String key, float defaultValue) { 546 return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue); 547 } 548 549 } 550 551 class SynthesisSpeechItem extends UtteranceSpeechItem { 552 // Never null. 553 private final String mText; 554 private final SynthesisRequest mSynthesisRequest; 555 private final String[] mDefaultLocale; 556 // Non null after synthesis has started, and all accesses 557 // guarded by 'this'. 558 private AbstractSynthesisCallback mSynthesisCallback; 559 private final EventLogger mEventLogger; 560 561 public SynthesisSpeechItem(Object callerIdentity, int callerUid, int callerPid, 562 Bundle params, String text) { 563 super(callerIdentity, callerUid, callerPid, params); 564 mText = text; 565 mSynthesisRequest = new SynthesisRequest(mText, mParams); 566 mDefaultLocale = getSettingsLocale(); 567 setRequestParams(mSynthesisRequest); 568 mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid, 569 mPackageName); 570 } 571 572 public String getText() { 573 return mText; 574 } 575 576 @Override 577 public boolean isValid() { 578 if (mText == null) { 579 Log.e(TAG, "null synthesis text"); 580 return false; 581 } 582 if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) { 583 Log.w(TAG, "Text too long: " + mText.length() + " chars"); 584 return false; 585 } 586 return true; 587 } 588 589 @Override 590 protected int playImpl() { 591 AbstractSynthesisCallback synthesisCallback; 592 mEventLogger.onRequestProcessingStart(); 593 synchronized (this) { 594 // stop() might have been called before we enter this 595 // synchronized block. 596 if (isStopped()) { 597 return TextToSpeech.ERROR; 598 } 599 mSynthesisCallback = createSynthesisCallback(); 600 synthesisCallback = mSynthesisCallback; 601 } 602 TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); 603 return synthesisCallback.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; 604 } 605 606 protected AbstractSynthesisCallback createSynthesisCallback() { 607 return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), 608 mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger); 609 } 610 611 private void setRequestParams(SynthesisRequest request) { 612 request.setLanguage(getLanguage(), getCountry(), getVariant()); 613 request.setSpeechRate(getSpeechRate()); 614 615 request.setPitch(getPitch()); 616 } 617 618 @Override 619 protected void stopImpl() { 620 AbstractSynthesisCallback synthesisCallback; 621 synchronized (this) { 622 synthesisCallback = mSynthesisCallback; 623 } 624 if (synthesisCallback != null) { 625 // If the synthesis callback is null, it implies that we haven't 626 // entered the synchronized(this) block in playImpl which in 627 // turn implies that synthesis would not have started. 628 synthesisCallback.stop(); 629 TextToSpeechService.this.onStop(); 630 } 631 } 632 633 public String getLanguage() { 634 return getStringParam(Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); 635 } 636 637 private boolean hasLanguage() { 638 return !TextUtils.isEmpty(getStringParam(Engine.KEY_PARAM_LANGUAGE, null)); 639 } 640 641 private String getCountry() { 642 if (!hasLanguage()) return mDefaultLocale[1]; 643 return getStringParam(Engine.KEY_PARAM_COUNTRY, ""); 644 } 645 646 private String getVariant() { 647 if (!hasLanguage()) return mDefaultLocale[2]; 648 return getStringParam(Engine.KEY_PARAM_VARIANT, ""); 649 } 650 651 private int getSpeechRate() { 652 return getIntParam(Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); 653 } 654 655 private int getPitch() { 656 return getIntParam(Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); 657 } 658 } 659 660 private class SynthesisToFileOutputStreamSpeechItem extends SynthesisSpeechItem { 661 private final FileOutputStream mFileOutputStream; 662 663 public SynthesisToFileOutputStreamSpeechItem(Object callerIdentity, int callerUid, 664 int callerPid, Bundle params, String text, FileOutputStream fileOutputStream) { 665 super(callerIdentity, callerUid, callerPid, params, text); 666 mFileOutputStream = fileOutputStream; 667 } 668 669 @Override 670 protected AbstractSynthesisCallback createSynthesisCallback() { 671 return new FileSynthesisCallback(mFileOutputStream.getChannel()); 672 } 673 674 @Override 675 protected int playImpl() { 676 dispatchOnStart(); 677 int status = super.playImpl(); 678 if (status == TextToSpeech.SUCCESS) { 679 dispatchOnDone(); 680 } else { 681 dispatchOnError(); 682 } 683 try { 684 mFileOutputStream.close(); 685 } catch(IOException e) { 686 Log.w(TAG, "Failed to close output file", e); 687 } 688 return status; 689 } 690 } 691 692 private class AudioSpeechItem extends UtteranceSpeechItem { 693 private final AudioPlaybackQueueItem mItem; 694 public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid, 695 Bundle params, Uri uri) { 696 super(callerIdentity, callerUid, callerPid, params); 697 mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), 698 TextToSpeechService.this, uri, getStreamType()); 699 } 700 701 @Override 702 public boolean isValid() { 703 return true; 704 } 705 706 @Override 707 protected int playImpl() { 708 mAudioPlaybackHandler.enqueue(mItem); 709 return TextToSpeech.SUCCESS; 710 } 711 712 @Override 713 protected void stopImpl() { 714 // Do nothing. 715 } 716 } 717 718 private class SilenceSpeechItem extends UtteranceSpeechItem { 719 private final long mDuration; 720 721 public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, 722 Bundle params, long duration) { 723 super(callerIdentity, callerUid, callerPid, params); 724 mDuration = duration; 725 } 726 727 @Override 728 public boolean isValid() { 729 return true; 730 } 731 732 @Override 733 protected int playImpl() { 734 mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( 735 this, getCallerIdentity(), mDuration)); 736 return TextToSpeech.SUCCESS; 737 } 738 739 @Override 740 protected void stopImpl() { 741 // Do nothing, handled by AudioPlaybackHandler#stopForApp 742 } 743 } 744 745 private class LoadLanguageItem extends SpeechItem { 746 private final String mLanguage; 747 private final String mCountry; 748 private final String mVariant; 749 750 public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, 751 Bundle params, String language, String country, String variant) { 752 super(callerIdentity, callerUid, callerPid, params); 753 mLanguage = language; 754 mCountry = country; 755 mVariant = variant; 756 } 757 758 @Override 759 public boolean isValid() { 760 return true; 761 } 762 763 @Override 764 protected int playImpl() { 765 int result = TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); 766 if (result == TextToSpeech.LANG_AVAILABLE || 767 result == TextToSpeech.LANG_COUNTRY_AVAILABLE || 768 result == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 769 return TextToSpeech.SUCCESS; 770 } 771 return TextToSpeech.ERROR; 772 } 773 774 @Override 775 protected void stopImpl() { 776 // No-op 777 } 778 } 779 780 @Override 781 public IBinder onBind(Intent intent) { 782 if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { 783 return mBinder; 784 } 785 return null; 786 } 787 788 /** 789 * Binder returned from {@code #onBind(Intent)}. The methods in this class can be 790 * called called from several different threads. 791 */ 792 // NOTE: All calls that are passed in a calling app are interned so that 793 // they can be used as message objects (which are tested for equality using ==). 794 private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { 795 @Override 796 public int speak(IBinder caller, String text, int queueMode, Bundle params) { 797 if (!checkNonNull(caller, text, params)) { 798 return TextToSpeech.ERROR; 799 } 800 801 SpeechItem item = new SynthesisSpeechItem(caller, 802 Binder.getCallingUid(), Binder.getCallingPid(), params, text); 803 return mSynthHandler.enqueueSpeechItem(queueMode, item); 804 } 805 806 @Override 807 public int synthesizeToFileDescriptor(IBinder caller, String text, ParcelFileDescriptor 808 fileDescriptor, Bundle params) { 809 if (!checkNonNull(caller, text, fileDescriptor, params)) { 810 return TextToSpeech.ERROR; 811 } 812 813 // In test env, ParcelFileDescriptor instance may be EXACTLY the same 814 // one that is used by client. And it will be closed by a client, thus 815 // preventing us from writing anything to it. 816 final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd( 817 fileDescriptor.detachFd()); 818 819 SpeechItem item = new SynthesisToFileOutputStreamSpeechItem(caller, 820 Binder.getCallingUid(), Binder.getCallingPid(), params, text, 821 new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor)); 822 return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); 823 } 824 825 @Override 826 public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params) { 827 if (!checkNonNull(caller, audioUri, params)) { 828 return TextToSpeech.ERROR; 829 } 830 831 SpeechItem item = new AudioSpeechItem(caller, 832 Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri); 833 return mSynthHandler.enqueueSpeechItem(queueMode, item); 834 } 835 836 @Override 837 public int playSilence(IBinder caller, long duration, int queueMode, Bundle params) { 838 if (!checkNonNull(caller, params)) { 839 return TextToSpeech.ERROR; 840 } 841 842 SpeechItem item = new SilenceSpeechItem(caller, 843 Binder.getCallingUid(), Binder.getCallingPid(), params, duration); 844 return mSynthHandler.enqueueSpeechItem(queueMode, item); 845 } 846 847 @Override 848 public boolean isSpeaking() { 849 return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); 850 } 851 852 @Override 853 public int stop(IBinder caller) { 854 if (!checkNonNull(caller)) { 855 return TextToSpeech.ERROR; 856 } 857 858 return mSynthHandler.stopForApp(caller); 859 } 860 861 @Override 862 public String[] getLanguage() { 863 return onGetLanguage(); 864 } 865 866 @Override 867 public String[] getClientDefaultLanguage() { 868 return getSettingsLocale(); 869 } 870 871 /* 872 * If defaults are enforced, then no language is "available" except 873 * perhaps the default language selected by the user. 874 */ 875 @Override 876 public int isLanguageAvailable(String lang, String country, String variant) { 877 if (!checkNonNull(lang)) { 878 return TextToSpeech.ERROR; 879 } 880 881 return onIsLanguageAvailable(lang, country, variant); 882 } 883 884 @Override 885 public String[] getFeaturesForLanguage(String lang, String country, String variant) { 886 Set<String> features = onGetFeaturesForLanguage(lang, country, variant); 887 String[] featuresArray = null; 888 if (features != null) { 889 featuresArray = new String[features.size()]; 890 features.toArray(featuresArray); 891 } else { 892 featuresArray = new String[0]; 893 } 894 return featuresArray; 895 } 896 897 /* 898 * There is no point loading a non default language if defaults 899 * are enforced. 900 */ 901 @Override 902 public int loadLanguage(IBinder caller, String lang, String country, String variant) { 903 if (!checkNonNull(lang)) { 904 return TextToSpeech.ERROR; 905 } 906 int retVal = onIsLanguageAvailable(lang, country, variant); 907 908 if (retVal == TextToSpeech.LANG_AVAILABLE || 909 retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE || 910 retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 911 912 SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(), 913 Binder.getCallingPid(), null, lang, country, variant); 914 915 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) != 916 TextToSpeech.SUCCESS) { 917 return TextToSpeech.ERROR; 918 } 919 } 920 return retVal; 921 } 922 923 @Override 924 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 925 // Note that passing in a null callback is a valid use case. 926 if (!checkNonNull(caller)) { 927 return; 928 } 929 930 mCallbacks.setCallback(caller, cb); 931 } 932 933 private String intern(String in) { 934 // The input parameter will be non null. 935 return in.intern(); 936 } 937 938 private boolean checkNonNull(Object... args) { 939 for (Object o : args) { 940 if (o == null) return false; 941 } 942 return true; 943 } 944 }; 945 946 private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { 947 private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback 948 = new HashMap<IBinder, ITextToSpeechCallback>(); 949 950 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 951 synchronized (mCallerToCallback) { 952 ITextToSpeechCallback old; 953 if (cb != null) { 954 register(cb, caller); 955 old = mCallerToCallback.put(caller, cb); 956 } else { 957 old = mCallerToCallback.remove(caller); 958 } 959 if (old != null && old != cb) { 960 unregister(old); 961 } 962 } 963 } 964 965 public void dispatchOnDone(Object callerIdentity, String utteranceId) { 966 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 967 if (cb == null) return; 968 try { 969 cb.onDone(utteranceId); 970 } catch (RemoteException e) { 971 Log.e(TAG, "Callback onDone failed: " + e); 972 } 973 } 974 975 public void dispatchOnStart(Object callerIdentity, String utteranceId) { 976 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 977 if (cb == null) return; 978 try { 979 cb.onStart(utteranceId); 980 } catch (RemoteException e) { 981 Log.e(TAG, "Callback onStart failed: " + e); 982 } 983 984 } 985 986 public void dispatchOnError(Object callerIdentity, String utteranceId) { 987 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 988 if (cb == null) return; 989 try { 990 cb.onError(utteranceId); 991 } catch (RemoteException e) { 992 Log.e(TAG, "Callback onError failed: " + e); 993 } 994 } 995 996 @Override 997 public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { 998 IBinder caller = (IBinder) cookie; 999 synchronized (mCallerToCallback) { 1000 mCallerToCallback.remove(caller); 1001 } 1002 mSynthHandler.stopForApp(caller); 1003 } 1004 1005 @Override 1006 public void kill() { 1007 synchronized (mCallerToCallback) { 1008 mCallerToCallback.clear(); 1009 super.kill(); 1010 } 1011 } 1012 1013 private ITextToSpeechCallback getCallbackFor(Object caller) { 1014 ITextToSpeechCallback cb; 1015 IBinder asBinder = (IBinder) caller; 1016 synchronized (mCallerToCallback) { 1017 cb = mCallerToCallback.get(asBinder); 1018 } 1019 1020 return cb; 1021 } 1022 1023 } 1024 1025 } 1026