TextToSpeechService.java revision e22b69a7de0349b99d3107349d1d3aa72d62c841
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// TODO: Add a link to the sample TTS engine once it's done. 70public abstract class TextToSpeechService extends Service { 71 72 private static final boolean DBG = false; 73 private static final String TAG = "TextToSpeechService"; 74 75 private static final int MAX_SPEECH_ITEM_CHAR_LENGTH = 4000; 76 private static final String SYNTH_THREAD_NAME = "SynthThread"; 77 78 private SynthHandler mSynthHandler; 79 // A thread and it's associated handler for playing back any audio 80 // associated with this TTS engine. Will handle all requests except synthesis 81 // to file requests, which occur on the synthesis thread. 82 private AudioPlaybackHandler mAudioPlaybackHandler; 83 84 private CallbackMap mCallbacks; 85 86 private int mDefaultAvailability = TextToSpeech.LANG_NOT_SUPPORTED; 87 88 @Override 89 public void onCreate() { 90 if (DBG) Log.d(TAG, "onCreate()"); 91 super.onCreate(); 92 93 SynthThread synthThread = new SynthThread(); 94 synthThread.start(); 95 mSynthHandler = new SynthHandler(synthThread.getLooper()); 96 97 HandlerThread audioTrackThread = new HandlerThread("TTS.audioTrackThread"); 98 audioTrackThread.start(); 99 mAudioPlaybackHandler = new AudioPlaybackHandler(audioTrackThread.getLooper()); 100 101 mCallbacks = new CallbackMap(); 102 103 // Load default language 104 mDefaultAvailability = onLoadLanguage(getDefaultLanguage(), 105 getDefaultCountry(), getDefaultVariant()); 106 } 107 108 @Override 109 public void onDestroy() { 110 if (DBG) Log.d(TAG, "onDestroy()"); 111 112 // Tell the synthesizer to stop 113 mSynthHandler.quit(); 114 // Tell the audio playback thread to stop. 115 mAudioPlaybackHandler.quit(); 116 // Unregister all callbacks. 117 mCallbacks.kill(); 118 119 super.onDestroy(); 120 } 121 122 /** 123 * Checks whether the engine supports a given language. 124 * 125 * Can be called on multiple threads. 126 * 127 * @param lang ISO-3 language code. 128 * @param country ISO-3 country code. May be empty or null. 129 * @param variant Language variant. May be empty or null. 130 * @return Code indicating the support status for the locale. 131 * One of {@link TextToSpeech#LANG_AVAILABLE}, 132 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 133 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 134 * {@link TextToSpeech#LANG_MISSING_DATA} 135 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 136 */ 137 protected abstract int onIsLanguageAvailable(String lang, String country, String variant); 138 139 /** 140 * Returns the language, country and variant currently being used by the TTS engine. 141 * 142 * Can be called on multiple threads. 143 * 144 * @return A 3-element array, containing language (ISO 3-letter code), 145 * country (ISO 3-letter code) and variant used by the engine. 146 * The country and variant may be {@code ""}. If country is empty, then variant must 147 * be empty too. 148 * @see Locale#getISO3Language() 149 * @see Locale#getISO3Country() 150 * @see Locale#getVariant() 151 */ 152 protected abstract String[] onGetLanguage(); 153 154 /** 155 * Notifies the engine that it should load a speech synthesis language. There is no guarantee 156 * that this method is always called before the language is used for synthesis. It is merely 157 * a hint to the engine that it will probably get some synthesis requests for this language 158 * at some point in the future. 159 * 160 * Can be called on multiple threads. 161 * 162 * @param lang ISO-3 language code. 163 * @param country ISO-3 country code. May be empty or null. 164 * @param variant Language variant. May be empty or null. 165 * @return Code indicating the support status for the locale. 166 * One of {@link TextToSpeech#LANG_AVAILABLE}, 167 * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, 168 * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, 169 * {@link TextToSpeech#LANG_MISSING_DATA} 170 * {@link TextToSpeech#LANG_NOT_SUPPORTED}. 171 */ 172 protected abstract int onLoadLanguage(String lang, String country, String variant); 173 174 /** 175 * Notifies the service that it should stop any in-progress speech synthesis. 176 * This method can be called even if no speech synthesis is currently in progress. 177 * 178 * Can be called on multiple threads, but not on the synthesis thread. 179 */ 180 protected abstract void onStop(); 181 182 /** 183 * Tells the service to synthesize speech from the given text. This method should 184 * block until the synthesis is finished. 185 * 186 * Called on the synthesis thread. 187 * 188 * @param request The synthesis request. 189 * @param callback The callback the the engine must use to make data available for 190 * playback or for writing to a file. 191 */ 192 protected abstract void onSynthesizeText(SynthesisRequest request, 193 SynthesisCallback callback); 194 195 private boolean areDefaultsEnforced() { 196 return getSecureSettingInt(Settings.Secure.TTS_USE_DEFAULTS, 197 TextToSpeech.Engine.USE_DEFAULTS) == 1; 198 } 199 200 private int getDefaultSpeechRate() { 201 return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE); 202 } 203 204 private String getDefaultLanguage() { 205 return getSecureSettingString(Settings.Secure.TTS_DEFAULT_LANG, 206 Locale.getDefault().getISO3Language()); 207 } 208 209 private String getDefaultCountry() { 210 return getSecureSettingString(Settings.Secure.TTS_DEFAULT_COUNTRY, 211 Locale.getDefault().getISO3Country()); 212 } 213 214 private String getDefaultVariant() { 215 return getSecureSettingString(Settings.Secure.TTS_DEFAULT_VARIANT, 216 Locale.getDefault().getVariant()); 217 } 218 219 private int getSecureSettingInt(String name, int defaultValue) { 220 return Settings.Secure.getInt(getContentResolver(), name, defaultValue); 221 } 222 223 private String getSecureSettingString(String name, String defaultValue) { 224 String value = Settings.Secure.getString(getContentResolver(), name); 225 return value != null ? value : 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_AUDIO); 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 263 private SpeechItem mCurrentSpeechItem = null; 264 265 public SynthHandler(Looper looper) { 266 super(looper); 267 } 268 269 private synchronized SpeechItem getCurrentSpeechItem() { 270 return mCurrentSpeechItem; 271 } 272 273 private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) { 274 SpeechItem old = mCurrentSpeechItem; 275 mCurrentSpeechItem = speechItem; 276 return old; 277 } 278 279 public boolean isSpeaking() { 280 return getCurrentSpeechItem() != null; 281 } 282 283 public void quit() { 284 // Don't process any more speech items 285 getLooper().quit(); 286 // Stop the current speech item 287 SpeechItem current = setCurrentSpeechItem(null); 288 if (current != null) { 289 current.stop(); 290 } 291 } 292 293 /** 294 * Adds a speech item to the queue. 295 * 296 * Called on a service binder thread. 297 */ 298 public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) { 299 if (!speechItem.isValid()) { 300 return TextToSpeech.ERROR; 301 } 302 // TODO: The old code also supported the undocumented queueMode == 2, 303 // which clears out all pending items from the calling app, as well as all 304 // non-file items from other apps. 305 if (queueMode == TextToSpeech.QUEUE_FLUSH) { 306 stop(speechItem.getCallingApp()); 307 } 308 Runnable runnable = new Runnable() { 309 @Override 310 public void run() { 311 setCurrentSpeechItem(speechItem); 312 speechItem.play(); 313 setCurrentSpeechItem(null); 314 } 315 }; 316 Message msg = Message.obtain(this, runnable); 317 // The obj is used to remove all callbacks from the given app in stop(String). 318 msg.obj = speechItem.getCallingApp(); 319 if (sendMessage(msg)) { 320 return TextToSpeech.SUCCESS; 321 } else { 322 Log.w(TAG, "SynthThread has quit"); 323 return TextToSpeech.ERROR; 324 } 325 } 326 327 /** 328 * Stops all speech output and removes any utterances still in the queue for 329 * the calling app. 330 * 331 * Called on a service binder thread. 332 */ 333 public int stop(String callingApp) { 334 if (TextUtils.isEmpty(callingApp)) { 335 return TextToSpeech.ERROR; 336 } 337 removeCallbacksAndMessages(callingApp); 338 SpeechItem current = setCurrentSpeechItem(null); 339 if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) { 340 current.stop(); 341 } 342 343 return TextToSpeech.SUCCESS; 344 } 345 } 346 347 interface UtteranceCompletedDispatcher { 348 public void dispatchUtteranceCompleted(); 349 } 350 351 /** 352 * An item in the synth thread queue. 353 */ 354 private abstract class SpeechItem implements UtteranceCompletedDispatcher { 355 private final String mCallingApp; 356 protected final Bundle mParams; 357 private boolean mStarted = false; 358 private boolean mStopped = false; 359 360 public SpeechItem(String callingApp, Bundle params) { 361 mCallingApp = callingApp; 362 mParams = params; 363 } 364 365 public String getCallingApp() { 366 return mCallingApp; 367 } 368 369 /** 370 * Checker whether the item is valid. If this method returns false, the item should not 371 * be played. 372 */ 373 public abstract boolean isValid(); 374 375 /** 376 * Plays the speech item. Blocks until playback is finished. 377 * Must not be called more than once. 378 * 379 * Only called on the synthesis thread. 380 * 381 * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. 382 */ 383 public int play() { 384 synchronized (this) { 385 if (mStarted) { 386 throw new IllegalStateException("play() called twice"); 387 } 388 mStarted = true; 389 } 390 return playImpl(); 391 } 392 393 /** 394 * Stops the speech item. 395 * Must not be called more than once. 396 * 397 * Can be called on multiple threads, but not on the synthesis thread. 398 */ 399 public void stop() { 400 synchronized (this) { 401 if (mStopped) { 402 throw new IllegalStateException("stop() called twice"); 403 } 404 mStopped = true; 405 } 406 stopImpl(); 407 } 408 409 public void dispatchUtteranceCompleted() { 410 final String utteranceId = getUtteranceId(); 411 if (!TextUtils.isEmpty(utteranceId)) { 412 mCallbacks.dispatchUtteranceCompleted(getCallingApp(), utteranceId); 413 } 414 } 415 416 protected abstract int playImpl(); 417 418 protected abstract void stopImpl(); 419 420 public int getStreamType() { 421 return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); 422 } 423 424 public float getVolume() { 425 return getFloatParam(Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME); 426 } 427 428 public float getPan() { 429 return getFloatParam(Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN); 430 } 431 432 public String getUtteranceId() { 433 return getStringParam(Engine.KEY_PARAM_UTTERANCE_ID, null); 434 } 435 436 protected String getStringParam(String key, String defaultValue) { 437 return mParams == null ? defaultValue : mParams.getString(key, defaultValue); 438 } 439 440 protected int getIntParam(String key, int defaultValue) { 441 return mParams == null ? defaultValue : mParams.getInt(key, defaultValue); 442 } 443 444 protected float getFloatParam(String key, float defaultValue) { 445 return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue); 446 } 447 } 448 449 class SynthesisSpeechItem extends SpeechItem { 450 private final String mText; 451 private final SynthesisRequest mSynthesisRequest; 452 // Non null after synthesis has started, and all accesses 453 // guarded by 'this'. 454 private AbstractSynthesisCallback mSynthesisCallback; 455 456 public SynthesisSpeechItem(String callingApp, Bundle params, String text) { 457 super(callingApp, params); 458 mText = text; 459 mSynthesisRequest = new SynthesisRequest(mText, mParams); 460 setRequestParams(mSynthesisRequest); 461 } 462 463 public String getText() { 464 return mText; 465 } 466 467 @Override 468 public boolean isValid() { 469 if (TextUtils.isEmpty(mText)) { 470 Log.w(TAG, "Got empty text"); 471 return false; 472 } 473 if (mText.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH){ 474 Log.w(TAG, "Text too long: " + mText.length() + " chars"); 475 return false; 476 } 477 return true; 478 } 479 480 @Override 481 protected int playImpl() { 482 AbstractSynthesisCallback synthesisCallback; 483 synchronized (this) { 484 mSynthesisCallback = createSynthesisCallback(); 485 synthesisCallback = mSynthesisCallback; 486 } 487 TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); 488 return synthesisCallback.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; 489 } 490 491 protected AbstractSynthesisCallback createSynthesisCallback() { 492 return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), 493 mAudioPlaybackHandler, this); 494 } 495 496 private void setRequestParams(SynthesisRequest request) { 497 if (areDefaultsEnforced()) { 498 request.setLanguage(getDefaultLanguage(), getDefaultCountry(), getDefaultVariant()); 499 request.setSpeechRate(getDefaultSpeechRate()); 500 } else { 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, getDefaultLanguage()); 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 getDefaultCountry(); 527 return getStringParam(Engine.KEY_PARAM_COUNTRY, ""); 528 } 529 530 private String getVariant() { 531 if (!hasLanguage()) return getDefaultVariant(); 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, mPlayer); 621 mAudioPlaybackHandler.enqueueAudio(mToken); 622 return TextToSpeech.SUCCESS; 623 } 624 625 @Override 626 protected void stopImpl() { 627 if (mToken != null) { 628 mAudioPlaybackHandler.stop(mToken); 629 } 630 } 631 } 632 633 private class SilenceSpeechItem extends SpeechItem { 634 private final long mDuration; 635 private SilenceMessageParams mToken; 636 637 public SilenceSpeechItem(String callingApp, Bundle params, long duration) { 638 super(callingApp, params); 639 mDuration = duration; 640 } 641 642 @Override 643 public boolean isValid() { 644 return true; 645 } 646 647 @Override 648 protected int playImpl() { 649 mToken = new SilenceMessageParams(this, mDuration); 650 mAudioPlaybackHandler.enqueueSilence(mToken); 651 return TextToSpeech.SUCCESS; 652 } 653 654 @Override 655 protected void stopImpl() { 656 if (mToken != null) { 657 mAudioPlaybackHandler.stop(mToken); 658 } 659 } 660 } 661 662 @Override 663 public IBinder onBind(Intent intent) { 664 if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { 665 return mBinder; 666 } 667 return null; 668 } 669 670 /** 671 * Binder returned from {@code #onBind(Intent)}. The methods in this class can be 672 * called called from several different threads. 673 */ 674 private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { 675 676 public int speak(String callingApp, String text, int queueMode, Bundle params) { 677 SpeechItem item = new SynthesisSpeechItem(callingApp, params, text); 678 return mSynthHandler.enqueueSpeechItem(queueMode, item); 679 } 680 681 public int synthesizeToFile(String callingApp, String text, String filename, 682 Bundle params) { 683 File file = new File(filename); 684 SpeechItem item = new SynthesisToFileSpeechItem(callingApp, params, text, file); 685 return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); 686 } 687 688 public int playAudio(String callingApp, Uri audioUri, int queueMode, Bundle params) { 689 SpeechItem item = new AudioSpeechItem(callingApp, params, audioUri); 690 return mSynthHandler.enqueueSpeechItem(queueMode, item); 691 } 692 693 public int playSilence(String callingApp, long duration, int queueMode, Bundle params) { 694 SpeechItem item = new SilenceSpeechItem(callingApp, params, duration); 695 return mSynthHandler.enqueueSpeechItem(queueMode, item); 696 } 697 698 public boolean isSpeaking() { 699 return mSynthHandler.isSpeaking(); 700 } 701 702 public int stop(String callingApp) { 703 return mSynthHandler.stop(callingApp); 704 } 705 706 public String[] getLanguage() { 707 return onGetLanguage(); 708 } 709 710 /* 711 * If defaults are enforced, then no language is "available" except 712 * perhaps the default language selected by the user. 713 */ 714 public int isLanguageAvailable(String lang, String country, String variant) { 715 if (areDefaultsEnforced()) { 716 if (isDefault(lang, country, variant)) { 717 return mDefaultAvailability; 718 } else { 719 return TextToSpeech.LANG_NOT_SUPPORTED; 720 } 721 } 722 return onIsLanguageAvailable(lang, country, variant); 723 } 724 725 /* 726 * There is no point loading a non default language if defaults 727 * are enforced. 728 */ 729 public int loadLanguage(String lang, String country, String variant) { 730 if (areDefaultsEnforced()) { 731 if (isDefault(lang, country, variant)) { 732 return mDefaultAvailability; 733 } else { 734 return TextToSpeech.LANG_NOT_SUPPORTED; 735 } 736 } 737 return onLoadLanguage(lang, country, variant); 738 } 739 740 public void setCallback(String packageName, ITextToSpeechCallback cb) { 741 mCallbacks.setCallback(packageName, cb); 742 } 743 744 private boolean isDefault(String lang, String country, String variant) { 745 return Locale.getDefault().equals(new Locale(lang, country, variant)); 746 } 747 }; 748 749 private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { 750 751 private final HashMap<String, ITextToSpeechCallback> mAppToCallback 752 = new HashMap<String, ITextToSpeechCallback>(); 753 754 public void setCallback(String packageName, ITextToSpeechCallback cb) { 755 synchronized (mAppToCallback) { 756 ITextToSpeechCallback old; 757 if (cb != null) { 758 register(cb, packageName); 759 old = mAppToCallback.put(packageName, cb); 760 } else { 761 old = mAppToCallback.remove(packageName); 762 } 763 if (old != null && old != cb) { 764 unregister(old); 765 } 766 } 767 } 768 769 public void dispatchUtteranceCompleted(String packageName, String utteranceId) { 770 ITextToSpeechCallback cb; 771 synchronized (mAppToCallback) { 772 cb = mAppToCallback.get(packageName); 773 } 774 if (cb == null) return; 775 try { 776 cb.utteranceCompleted(utteranceId); 777 } catch (RemoteException e) { 778 Log.e(TAG, "Callback failed: " + e); 779 } 780 } 781 782 @Override 783 public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { 784 String packageName = (String) cookie; 785 synchronized (mAppToCallback) { 786 mAppToCallback.remove(packageName); 787 } 788 mSynthHandler.stop(packageName); 789 } 790 791 @Override 792 public void kill() { 793 synchronized (mAppToCallback) { 794 mAppToCallback.clear(); 795 super.kill(); 796 } 797 } 798 799 } 800 801} 802