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