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.wtf(TAG, "Got null 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 public boolean isValid() { 645 if (!super.isValid()) { 646 return false; 647 } 648 return checkFile(mFile); 649 } 650 651 @Override 652 protected AbstractSynthesisCallback createSynthesisCallback() { 653 return new FileSynthesisCallback(mFile); 654 } 655 656 @Override 657 protected int playImpl() { 658 dispatchOnStart(); 659 int status = super.playImpl(); 660 if (status == TextToSpeech.SUCCESS) { 661 dispatchOnDone(); 662 } else { 663 dispatchOnError(); 664 } 665 return status; 666 } 667 668 /** 669 * Checks that the given file can be used for synthesis output. 670 */ 671 private boolean checkFile(File file) { 672 try { 673 if (file.exists()) { 674 Log.v(TAG, "File " + file + " exists, deleting."); 675 if (!file.delete()) { 676 Log.e(TAG, "Failed to delete " + file); 677 return false; 678 } 679 } 680 if (!file.createNewFile()) { 681 Log.e(TAG, "Can't create file " + file); 682 return false; 683 } 684 if (!file.delete()) { 685 Log.e(TAG, "Failed to delete " + file); 686 return false; 687 } 688 return true; 689 } catch (IOException e) { 690 Log.e(TAG, "Can't use " + file + " due to exception " + e); 691 return false; 692 } 693 } 694 } 695 696 private class AudioSpeechItem extends SpeechItem { 697 private final AudioPlaybackQueueItem mItem; 698 public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid, 699 Bundle params, Uri uri) { 700 super(callerIdentity, callerUid, callerPid, params); 701 mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), 702 TextToSpeechService.this, uri, getStreamType()); 703 } 704 705 @Override 706 public boolean isValid() { 707 return true; 708 } 709 710 @Override 711 protected int playImpl() { 712 mAudioPlaybackHandler.enqueue(mItem); 713 return TextToSpeech.SUCCESS; 714 } 715 716 @Override 717 protected void stopImpl() { 718 // Do nothing. 719 } 720 } 721 722 private class SilenceSpeechItem extends SpeechItem { 723 private final long mDuration; 724 725 public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, 726 Bundle params, long duration) { 727 super(callerIdentity, callerUid, callerPid, params); 728 mDuration = duration; 729 } 730 731 @Override 732 public boolean isValid() { 733 return true; 734 } 735 736 @Override 737 protected int playImpl() { 738 mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( 739 this, getCallerIdentity(), mDuration)); 740 return TextToSpeech.SUCCESS; 741 } 742 743 @Override 744 protected void stopImpl() { 745 // Do nothing, handled by AudioPlaybackHandler#stopForApp 746 } 747 } 748 749 @Override 750 public IBinder onBind(Intent intent) { 751 if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { 752 return mBinder; 753 } 754 return null; 755 } 756 757 /** 758 * Binder returned from {@code #onBind(Intent)}. The methods in this class can be 759 * called called from several different threads. 760 */ 761 // NOTE: All calls that are passed in a calling app are interned so that 762 // they can be used as message objects (which are tested for equality using ==). 763 private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { 764 @Override 765 public int speak(IBinder caller, String text, int queueMode, Bundle params) { 766 if (!checkNonNull(caller, text, params)) { 767 return TextToSpeech.ERROR; 768 } 769 770 SpeechItem item = new SynthesisSpeechItem(caller, 771 Binder.getCallingUid(), Binder.getCallingPid(), params, text); 772 return mSynthHandler.enqueueSpeechItem(queueMode, item); 773 } 774 775 @Override 776 public int synthesizeToFile(IBinder caller, String text, String filename, 777 Bundle params) { 778 if (!checkNonNull(caller, text, filename, params)) { 779 return TextToSpeech.ERROR; 780 } 781 782 File file = new File(filename); 783 SpeechItem item = new SynthesisToFileSpeechItem(caller, Binder.getCallingUid(), 784 Binder.getCallingPid(), params, text, file); 785 return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); 786 } 787 788 @Override 789 public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params) { 790 if (!checkNonNull(caller, audioUri, params)) { 791 return TextToSpeech.ERROR; 792 } 793 794 SpeechItem item = new AudioSpeechItem(caller, 795 Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri); 796 return mSynthHandler.enqueueSpeechItem(queueMode, item); 797 } 798 799 @Override 800 public int playSilence(IBinder caller, long duration, int queueMode, Bundle params) { 801 if (!checkNonNull(caller, params)) { 802 return TextToSpeech.ERROR; 803 } 804 805 SpeechItem item = new SilenceSpeechItem(caller, 806 Binder.getCallingUid(), Binder.getCallingPid(), params, duration); 807 return mSynthHandler.enqueueSpeechItem(queueMode, item); 808 } 809 810 @Override 811 public boolean isSpeaking() { 812 return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); 813 } 814 815 @Override 816 public int stop(IBinder caller) { 817 if (!checkNonNull(caller)) { 818 return TextToSpeech.ERROR; 819 } 820 821 return mSynthHandler.stopForApp(caller); 822 } 823 824 @Override 825 public String[] getLanguage() { 826 return onGetLanguage(); 827 } 828 829 /* 830 * If defaults are enforced, then no language is "available" except 831 * perhaps the default language selected by the user. 832 */ 833 @Override 834 public int isLanguageAvailable(String lang, String country, String variant) { 835 if (!checkNonNull(lang)) { 836 return TextToSpeech.ERROR; 837 } 838 839 return onIsLanguageAvailable(lang, country, variant); 840 } 841 842 @Override 843 public String[] getFeaturesForLanguage(String lang, String country, String variant) { 844 Set<String> features = onGetFeaturesForLanguage(lang, country, variant); 845 String[] featuresArray = null; 846 if (features != null) { 847 featuresArray = new String[features.size()]; 848 features.toArray(featuresArray); 849 } else { 850 featuresArray = new String[0]; 851 } 852 return featuresArray; 853 } 854 855 /* 856 * There is no point loading a non default language if defaults 857 * are enforced. 858 */ 859 @Override 860 public int loadLanguage(String lang, String country, String variant) { 861 if (!checkNonNull(lang)) { 862 return TextToSpeech.ERROR; 863 } 864 865 return onLoadLanguage(lang, country, variant); 866 } 867 868 @Override 869 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 870 // Note that passing in a null callback is a valid use case. 871 if (!checkNonNull(caller)) { 872 return; 873 } 874 875 mCallbacks.setCallback(caller, cb); 876 } 877 878 private String intern(String in) { 879 // The input parameter will be non null. 880 return in.intern(); 881 } 882 883 private boolean checkNonNull(Object... args) { 884 for (Object o : args) { 885 if (o == null) return false; 886 } 887 return true; 888 } 889 }; 890 891 private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { 892 private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback 893 = new HashMap<IBinder, ITextToSpeechCallback>(); 894 895 public void setCallback(IBinder caller, ITextToSpeechCallback cb) { 896 synchronized (mCallerToCallback) { 897 ITextToSpeechCallback old; 898 if (cb != null) { 899 register(cb, caller); 900 old = mCallerToCallback.put(caller, cb); 901 } else { 902 old = mCallerToCallback.remove(caller); 903 } 904 if (old != null && old != cb) { 905 unregister(old); 906 } 907 } 908 } 909 910 public void dispatchOnDone(Object callerIdentity, String utteranceId) { 911 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 912 if (cb == null) return; 913 try { 914 cb.onDone(utteranceId); 915 } catch (RemoteException e) { 916 Log.e(TAG, "Callback onDone failed: " + e); 917 } 918 } 919 920 public void dispatchOnStart(Object callerIdentity, String utteranceId) { 921 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 922 if (cb == null) return; 923 try { 924 cb.onStart(utteranceId); 925 } catch (RemoteException e) { 926 Log.e(TAG, "Callback onStart failed: " + e); 927 } 928 929 } 930 931 public void dispatchOnError(Object callerIdentity, String utteranceId) { 932 ITextToSpeechCallback cb = getCallbackFor(callerIdentity); 933 if (cb == null) return; 934 try { 935 cb.onError(utteranceId); 936 } catch (RemoteException e) { 937 Log.e(TAG, "Callback onError failed: " + e); 938 } 939 } 940 941 @Override 942 public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { 943 IBinder caller = (IBinder) cookie; 944 synchronized (mCallerToCallback) { 945 mCallerToCallback.remove(caller); 946 } 947 mSynthHandler.stopForApp(caller); 948 } 949 950 @Override 951 public void kill() { 952 synchronized (mCallerToCallback) { 953 mCallerToCallback.clear(); 954 super.kill(); 955 } 956 } 957 958 private ITextToSpeechCallback getCallbackFor(Object caller) { 959 ITextToSpeechCallback cb; 960 IBinder asBinder = (IBinder) caller; 961 synchronized (mCallerToCallback) { 962 cb = mCallerToCallback.get(asBinder); 963 } 964 965 return cb; 966 } 967 968 } 969 970} 971