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 private final int mCallerUid; 561 562 public SynthesisSpeechItem(Object callerIdentity, int callerUid, int callerPid, 563 Bundle params, String text) { 564 super(callerIdentity, callerUid, callerPid, params); 565 mText = text; 566 mCallerUid = callerUid; 567 mSynthesisRequest = new SynthesisRequest(mText, mParams); 568 mDefaultLocale = getSettingsLocale(); 569 setRequestParams(mSynthesisRequest); 570 mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid, 571 mPackageName); 572 } 573 574 public String getText() { 575 return mText; 576 } 577 578 @Override 579 public boolean isValid() { 580 if (mText == null) { 581 Log.e(TAG, "null synthesis text"); 582 return false; 583 } 584 if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) { 585 Log.w(TAG, "Text too long: " + mText.length() + " chars"); 586 return false; 587 } 588 return true; 589 } 590 591 @Override 592 protected int playImpl() { 593 AbstractSynthesisCallback synthesisCallback; 594 mEventLogger.onRequestProcessingStart(); 595 synchronized (this) { 596 // stop() might have been called before we enter this 597 // synchronized block. 598 if (isStopped()) { 599 return TextToSpeech.ERROR; 600 } 601 mSynthesisCallback = createSynthesisCallback(); 602 synthesisCallback = mSynthesisCallback; 603 } 604 TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); 605 return synthesisCallback.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; 606 } 607 608 protected AbstractSynthesisCallback createSynthesisCallback() { 609 return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), 610 mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger); 611 } 612 613 private void setRequestParams(SynthesisRequest request) { 614 request.setLanguage(getLanguage(), getCountry(), getVariant()); 615 request.setSpeechRate(getSpeechRate()); 616 request.setCallerUid(mCallerUid); 617 request.setPitch(getPitch()); 618 } 619 620 @Override 621 protected void stopImpl() { 622 AbstractSynthesisCallback synthesisCallback; 623 synchronized (this) { 624 synthesisCallback = mSynthesisCallback; 625 } 626 if (synthesisCallback != null) { 627 // If the synthesis callback is null, it implies that we haven't 628 // entered the synchronized(this) block in playImpl which in 629 // turn implies that synthesis would not have started. 630 synthesisCallback.stop(); 631 TextToSpeechService.this.onStop(); 632 } 633 } 634 635 public String getLanguage() { 636 return getStringParam(Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); 637 } 638 639 private boolean hasLanguage() { 640 return !TextUtils.isEmpty(getStringParam(Engine.KEY_PARAM_LANGUAGE, null)); 641 } 642 643 private String getCountry() { 644 if (!hasLanguage()) return mDefaultLocale[1]; 645 return getStringParam(Engine.KEY_PARAM_COUNTRY, ""); 646 } 647 648 private String getVariant() { 649 if (!hasLanguage()) return mDefaultLocale[2]; 650 return getStringParam(Engine.KEY_PARAM_VARIANT, ""); 651 } 652 653 private int getSpeechRate() { 654 return getIntParam(Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); 655 } 656 657 private int getPitch() { 658 return getIntParam(Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); 659 } 660 } 661 662 private class SynthesisToFileOutputStreamSpeechItem extends SynthesisSpeechItem { 663 private final FileOutputStream mFileOutputStream; 664 665 public SynthesisToFileOutputStreamSpeechItem(Object callerIdentity, int callerUid, 666 int callerPid, Bundle params, String text, FileOutputStream fileOutputStream) { 667 super(callerIdentity, callerUid, callerPid, params, text); 668 mFileOutputStream = fileOutputStream; 669 } 670 671 @Override 672 protected AbstractSynthesisCallback createSynthesisCallback() { 673 return new FileSynthesisCallback(mFileOutputStream.getChannel()); 674 } 675 676 @Override 677 protected int playImpl() { 678 dispatchOnStart(); 679 int status = super.playImpl(); 680 if (status == TextToSpeech.SUCCESS) { 681 dispatchOnDone(); 682 } else { 683 dispatchOnError(); 684 } 685 try { 686 mFileOutputStream.close(); 687 } catch(IOException e) { 688 Log.w(TAG, "Failed to close output file", e); 689 } 690 return status; 691 } 692 } 693 694 private class AudioSpeechItem extends UtteranceSpeechItem { 695 private final AudioPlaybackQueueItem mItem; 696 public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid, 697 Bundle params, Uri uri) { 698 super(callerIdentity, callerUid, callerPid, params); 699 mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), 700 TextToSpeechService.this, uri, getStreamType()); 701 } 702 703 @Override 704 public boolean isValid() { 705 return true; 706 } 707 708 @Override 709 protected int playImpl() { 710 mAudioPlaybackHandler.enqueue(mItem); 711 return TextToSpeech.SUCCESS; 712 } 713 714 @Override 715 protected void stopImpl() { 716 // Do nothing. 717 } 718 } 719 720 private class SilenceSpeechItem extends UtteranceSpeechItem { 721 private final long mDuration; 722 723 public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, 724 Bundle params, long duration) { 725 super(callerIdentity, callerUid, callerPid, params); 726 mDuration = duration; 727 } 728 729 @Override 730 public boolean isValid() { 731 return true; 732 } 733 734 @Override 735 protected int playImpl() { 736 mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( 737 this, getCallerIdentity(), mDuration)); 738 return TextToSpeech.SUCCESS; 739 } 740 741 @Override 742 protected void stopImpl() { 743 // Do nothing, handled by AudioPlaybackHandler#stopForApp 744 } 745 } 746 747 private class LoadLanguageItem extends SpeechItem { 748 private final String mLanguage; 749 private final String mCountry; 750 private final String mVariant; 751 752 public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, 753 Bundle params, String language, String country, String variant) { 754 super(callerIdentity, callerUid, callerPid, params); 755 mLanguage = language; 756 mCountry = country; 757 mVariant = variant; 758 } 759 760 @Override 761 public boolean isValid() { 762 return true; 763 } 764 765 @Override 766 protected int playImpl() { 767 int result = TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); 768 if (result == TextToSpeech.LANG_AVAILABLE || 769 result == TextToSpeech.LANG_COUNTRY_AVAILABLE || 770 result == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 771 return TextToSpeech.SUCCESS; 772 } 773 return TextToSpeech.ERROR; 774 } 775 776 @Override 777 protected void stopImpl() { 778 // No-op 779 } 780 } 781 782 @Override 783 public IBinder onBind(Intent intent) { 784 if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { 785 return mBinder; 786 } 787 return null; 788 } 789 790 /** 791 * Binder returned from {@code #onBind(Intent)}. The methods in this class can be 792 * called called from several different threads. 793 */ 794 // NOTE: All calls that are passed in a calling app are interned so that 795 // they can be used as message objects (which are tested for equality using ==). 796 private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { 797 @Override 798 public int speak(IBinder caller, String text, int queueMode, Bundle params) { 799 if (!checkNonNull(caller, text, params)) { 800 return TextToSpeech.ERROR; 801 } 802 803 SpeechItem item = new SynthesisSpeechItem(caller, 804 Binder.getCallingUid(), Binder.getCallingPid(), params, text); 805 return mSynthHandler.enqueueSpeechItem(queueMode, item); 806 } 807 808 @Override 809 public int synthesizeToFileDescriptor(IBinder caller, String text, ParcelFileDescriptor 810 fileDescriptor, Bundle params) { 811 if (!checkNonNull(caller, text, fileDescriptor, params)) { 812 return TextToSpeech.ERROR; 813 } 814 815 // In test env, ParcelFileDescriptor instance may be EXACTLY the same 816 // one that is used by client. And it will be closed by a client, thus 817 // preventing us from writing anything to it. 818 final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd( 819 fileDescriptor.detachFd()); 820 821 SpeechItem item = new SynthesisToFileOutputStreamSpeechItem(caller, 822 Binder.getCallingUid(), Binder.getCallingPid(), params, text, 823 new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor)); 824 return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); 825 } 826 827 @Override 828 public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params) { 829 if (!checkNonNull(caller, audioUri, params)) { 830 return TextToSpeech.ERROR; 831 } 832 833 SpeechItem item = new AudioSpeechItem(caller, 834 Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri); 835 return mSynthHandler.enqueueSpeechItem(queueMode, item); 836 } 837 838 @Override 839 public int playSilence(IBinder caller, long duration, int queueMode, Bundle params) { 840 if (!checkNonNull(caller, params)) { 841 return TextToSpeech.ERROR; 842 } 843 844 SpeechItem item = new SilenceSpeechItem(caller, 845 Binder.getCallingUid(), Binder.getCallingPid(), params, duration); 846 return mSynthHandler.enqueueSpeechItem(queueMode, item); 847 } 848 849 @Override 850 public boolean isSpeaking() { 851 return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); 852 } 853 854 @Override 855 public int stop(IBinder caller) { 856 if (!checkNonNull(caller)) { 857 return TextToSpeech.ERROR; 858 } 859 860 return mSynthHandler.stopForApp(caller); 861 } 862 863 @Override 864 public String[] getLanguage() { 865 return onGetLanguage(); 866 } 867 868 @Override 869 public String[] getClientDefaultLanguage() { 870 return getSettingsLocale(); 871 } 872 873 /* 874 * If defaults are enforced, then no language is "available" except 875 * perhaps the default language selected by the user. 876 */ 877 @Override 878 public int isLanguageAvailable(String lang, String country, String variant) { 879 if (!checkNonNull(lang)) { 880 return TextToSpeech.ERROR; 881 } 882 883 return onIsLanguageAvailable(lang, country, variant); 884 } 885 886 @Override 887 public String[] getFeaturesForLanguage(String lang, String country, String variant) { 888 Set<String> features = onGetFeaturesForLanguage(lang, country, variant); 889 String[] featuresArray = null; 890 if (features != null) { 891 featuresArray = new String[features.size()]; 892 features.toArray(featuresArray); 893 } else { 894 featuresArray = new String[0]; 895 } 896 return featuresArray; 897 } 898 899 /* 900 * There is no point loading a non default language if defaults 901 * are enforced. 902 */ 903 @Override 904 public int loadLanguage(IBinder caller, String lang, String country, String variant) { 905 if (!checkNonNull(lang)) { 906 return TextToSpeech.ERROR; 907 } 908 int retVal = onIsLanguageAvailable(lang, country, variant); 909 910 if (retVal == TextToSpeech.LANG_AVAILABLE || 911 retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE || 912 retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 913 914 SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(), 915 Binder.getCallingPid(), null, lang, country, variant); 916 917 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) != 918 TextToSpeech.SUCCESS) { 919 return TextToSpeech.ERROR; 920 } 921 } 922 return retVal; 923 } 924 925 @Override 926 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 927 // Note that passing in a null callback is a valid use case. 928 if (!checkNonNull(caller)) { 929 return; 930 } 931 932 mCallbacks.setCallback(caller, cb); 933 } 934 935 private String intern(String in) { 936 // The input parameter will be non null. 937 return in.intern(); 938 } 939 940 private boolean checkNonNull(Object... args) { 941 for (Object o : args) { 942 if (o == null) return false; 943 } 944 return true; 945 } 946 }; 947 948 private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { 949 private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback 950 = new HashMap<IBinder, ITextToSpeechCallback>(); 951 952 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 953 synchronized (mCallerToCallback) { 954 ITextToSpeechCallback old; 955 if (cb != null) { 956 register(cb, caller); 957 old = mCallerToCallback.put(caller, cb); 958 } else { 959 old = mCallerToCallback.remove(caller); 960 } 961 if (old != null && old != cb) { 962 unregister(old); 963 } 964 } 965 } 966 967 public void dispatchOnDone(Object callerIdentity, String utteranceId) { 968 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 969 if (cb == null) return; 970 try { 971 cb.onDone(utteranceId); 972 } catch (RemoteException e) { 973 Log.e(TAG, "Callback onDone failed: " + e); 974 } 975 } 976 977 public void dispatchOnStart(Object callerIdentity, String utteranceId) { 978 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 979 if (cb == null) return; 980 try { 981 cb.onStart(utteranceId); 982 } catch (RemoteException e) { 983 Log.e(TAG, "Callback onStart failed: " + e); 984 } 985 986 } 987 988 public void dispatchOnError(Object callerIdentity, String utteranceId) { 989 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 990 if (cb == null) return; 991 try { 992 cb.onError(utteranceId); 993 } catch (RemoteException e) { 994 Log.e(TAG, "Callback onError failed: " + e); 995 } 996 } 997 998 @Override 999 public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { 1000 IBinder caller = (IBinder) cookie; 1001 synchronized (mCallerToCallback) { 1002 mCallerToCallback.remove(caller); 1003 } 1004 mSynthHandler.stopForApp(caller); 1005 } 1006 1007 @Override 1008 public void kill() { 1009 synchronized (mCallerToCallback) { 1010 mCallerToCallback.clear(); 1011 super.kill(); 1012 } 1013 } 1014 1015 private ITextToSpeechCallback getCallbackFor(Object caller) { 1016 ITextToSpeechCallback cb; 1017 IBinder asBinder = (IBinder) caller; 1018 synchronized (mCallerToCallback) { 1019 cb = mCallerToCallback.get(asBinder); 1020 } 1021 1022 return cb; 1023 } 1024 1025 } 1026 1027 } 1028