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