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