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