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