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