TextToSpeechService.java revision 68e2af55d65d2e61fbf8096eccaa2e4ca02b6c5a
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 speechItem.dispatchOnError(); 310 return TextToSpeech.ERROR; 311 } 312 313 if (queueMode == TextToSpeech.QUEUE_FLUSH) { 314 stopForApp(speechItem.getCallingApp()); 315 } else if (queueMode == TextToSpeech.QUEUE_DESTROY) { 316 stopAll(); 317 } 318 Runnable runnable = new Runnable() { 319 @Override 320 public void run() { 321 setCurrentSpeechItem(speechItem); 322 speechItem.play(); 323 setCurrentSpeechItem(null); 324 } 325 }; 326 Message msg = Message.obtain(this, runnable); 327 // The obj is used to remove all callbacks from the given app in 328 // stopForApp(String). 329 // 330 // Note that this string is interned, so the == comparison works. 331 msg.obj = speechItem.getCallingApp(); 332 if (sendMessage(msg)) { 333 return TextToSpeech.SUCCESS; 334 } else { 335 Log.w(TAG, "SynthThread has quit"); 336 speechItem.dispatchOnError(); 337 return TextToSpeech.ERROR; 338 } 339 } 340 341 /** 342 * Stops all speech output and removes any utterances still in the queue for 343 * the calling app. 344 * 345 * Called on a service binder thread. 346 */ 347 public int stopForApp(String callingApp) { 348 if (TextUtils.isEmpty(callingApp)) { 349 return TextToSpeech.ERROR; 350 } 351 352 removeCallbacksAndMessages(callingApp); 353 // This stops writing data to the file / or publishing 354 // items to the audio playback handler. 355 // 356 // Note that the current speech item must be removed only if it 357 // belongs to the callingApp, else the item will be "orphaned" and 358 // not stopped correctly if a stop request comes along for the item 359 // from the app it belongs to. 360 SpeechItem current = maybeRemoveCurrentSpeechItem(callingApp); 361 if (current != null) { 362 current.stop(); 363 } 364 365 // Remove any enqueued audio too. 366 mAudioPlaybackHandler.removePlaybackItems(callingApp); 367 368 return TextToSpeech.SUCCESS; 369 } 370 371 public int stopAll() { 372 // Stop the current speech item unconditionally. 373 SpeechItem current = setCurrentSpeechItem(null); 374 if (current != null) { 375 current.stop(); 376 } 377 // Remove all other items from the queue. 378 removeCallbacksAndMessages(null); 379 // Remove all pending playback as well. 380 mAudioPlaybackHandler.removeAllItems(); 381 382 return TextToSpeech.SUCCESS; 383 } 384 } 385 386 interface UtteranceProgressDispatcher { 387 public void dispatchOnDone(); 388 public void dispatchOnStart(); 389 public void dispatchOnError(); 390 } 391 392 /** 393 * An item in the synth thread queue. 394 */ 395 private abstract class SpeechItem implements UtteranceProgressDispatcher { 396 private final String mCallingApp; 397 protected final Bundle mParams; 398 private boolean mStarted = false; 399 private boolean mStopped = false; 400 401 public SpeechItem(String callingApp, Bundle params) { 402 mCallingApp = callingApp; 403 mParams = params; 404 } 405 406 public String getCallingApp() { 407 return mCallingApp; 408 } 409 410 /** 411 * Checker whether the item is valid. If this method returns false, the item should not 412 * be played. 413 */ 414 public abstract boolean isValid(); 415 416 /** 417 * Plays the speech item. Blocks until playback is finished. 418 * Must not be called more than once. 419 * 420 * Only called on the synthesis thread. 421 * 422 * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. 423 */ 424 public int play() { 425 synchronized (this) { 426 if (mStarted) { 427 throw new IllegalStateException("play() called twice"); 428 } 429 mStarted = true; 430 } 431 return playImpl(); 432 } 433 434 /** 435 * Stops the speech item. 436 * Must not be called more than once. 437 * 438 * Can be called on multiple threads, but not on the synthesis thread. 439 */ 440 public void stop() { 441 synchronized (this) { 442 if (mStopped) { 443 throw new IllegalStateException("stop() called twice"); 444 } 445 mStopped = true; 446 } 447 stopImpl(); 448 } 449 450 @Override 451 public void dispatchOnDone() { 452 final String utteranceId = getUtteranceId(); 453 if (utteranceId != null) { 454 mCallbacks.dispatchOnDone(getCallingApp(), utteranceId); 455 } 456 } 457 458 @Override 459 public void dispatchOnStart() { 460 final String utteranceId = getUtteranceId(); 461 if (utteranceId != null) { 462 mCallbacks.dispatchOnStart(getCallingApp(), utteranceId); 463 } 464 } 465 466 @Override 467 public void dispatchOnError() { 468 final String utteranceId = getUtteranceId(); 469 if (utteranceId != null) { 470 mCallbacks.dispatchOnError(getCallingApp(), utteranceId); 471 } 472 } 473 474 protected synchronized boolean isStopped() { 475 return mStopped; 476 } 477 478 protected abstract int playImpl(); 479 480 protected abstract void stopImpl(); 481 482 public int getStreamType() { 483 return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); 484 } 485 486 public float getVolume() { 487 return getFloatParam(Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME); 488 } 489 490 public float getPan() { 491 return getFloatParam(Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN); 492 } 493 494 public String getUtteranceId() { 495 return getStringParam(Engine.KEY_PARAM_UTTERANCE_ID, null); 496 } 497 498 protected String getStringParam(String key, String defaultValue) { 499 return mParams == null ? defaultValue : mParams.getString(key, defaultValue); 500 } 501 502 protected int getIntParam(String key, int defaultValue) { 503 return mParams == null ? defaultValue : mParams.getInt(key, defaultValue); 504 } 505 506 protected float getFloatParam(String key, float defaultValue) { 507 return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue); 508 } 509 } 510 511 class SynthesisSpeechItem extends SpeechItem { 512 // Never null. 513 private final String mText; 514 private final SynthesisRequest mSynthesisRequest; 515 private final String[] mDefaultLocale; 516 // Non null after synthesis has started, and all accesses 517 // guarded by 'this'. 518 private AbstractSynthesisCallback mSynthesisCallback; 519 private final EventLogger mEventLogger; 520 521 public SynthesisSpeechItem(String callingApp, Bundle params, String text) { 522 super(callingApp, params); 523 mText = text; 524 mSynthesisRequest = new SynthesisRequest(mText, mParams); 525 mDefaultLocale = getSettingsLocale(); 526 setRequestParams(mSynthesisRequest); 527 mEventLogger = new EventLogger(mSynthesisRequest, getCallingApp(), mPackageName); 528 } 529 530 public String getText() { 531 return mText; 532 } 533 534 @Override 535 public boolean isValid() { 536 if (mText == null) { 537 Log.wtf(TAG, "Got null text"); 538 return false; 539 } 540 if (mText.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH) { 541 Log.w(TAG, "Text too long: " + mText.length() + " chars"); 542 return false; 543 } 544 return true; 545 } 546 547 @Override 548 protected int playImpl() { 549 AbstractSynthesisCallback synthesisCallback; 550 mEventLogger.onRequestProcessingStart(); 551 synchronized (this) { 552 // stop() might have been called before we enter this 553 // synchronized block. 554 if (isStopped()) { 555 return TextToSpeech.ERROR; 556 } 557 mSynthesisCallback = createSynthesisCallback(); 558 synthesisCallback = mSynthesisCallback; 559 } 560 TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); 561 return synthesisCallback.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; 562 } 563 564 protected AbstractSynthesisCallback createSynthesisCallback() { 565 return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), 566 mAudioPlaybackHandler, this, getCallingApp(), mEventLogger); 567 } 568 569 private void setRequestParams(SynthesisRequest request) { 570 request.setLanguage(getLanguage(), getCountry(), getVariant()); 571 request.setSpeechRate(getSpeechRate()); 572 573 request.setPitch(getPitch()); 574 } 575 576 @Override 577 protected void stopImpl() { 578 AbstractSynthesisCallback synthesisCallback; 579 synchronized (this) { 580 synthesisCallback = mSynthesisCallback; 581 } 582 if (synthesisCallback != null) { 583 // If the synthesis callback is null, it implies that we haven't 584 // entered the synchronized(this) block in playImpl which in 585 // turn implies that synthesis would not have started. 586 synthesisCallback.stop(); 587 TextToSpeechService.this.onStop(); 588 } 589 } 590 591 public String getLanguage() { 592 return getStringParam(Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); 593 } 594 595 private boolean hasLanguage() { 596 return !TextUtils.isEmpty(getStringParam(Engine.KEY_PARAM_LANGUAGE, null)); 597 } 598 599 private String getCountry() { 600 if (!hasLanguage()) return mDefaultLocale[1]; 601 return getStringParam(Engine.KEY_PARAM_COUNTRY, ""); 602 } 603 604 private String getVariant() { 605 if (!hasLanguage()) return mDefaultLocale[2]; 606 return getStringParam(Engine.KEY_PARAM_VARIANT, ""); 607 } 608 609 private int getSpeechRate() { 610 return getIntParam(Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); 611 } 612 613 private int getPitch() { 614 return getIntParam(Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); 615 } 616 } 617 618 private class SynthesisToFileSpeechItem extends SynthesisSpeechItem { 619 private final File mFile; 620 621 public SynthesisToFileSpeechItem(String callingApp, Bundle params, String text, 622 File file) { 623 super(callingApp, params, text); 624 mFile = file; 625 } 626 627 @Override 628 public boolean isValid() { 629 if (!super.isValid()) { 630 return false; 631 } 632 return checkFile(mFile); 633 } 634 635 @Override 636 protected AbstractSynthesisCallback createSynthesisCallback() { 637 return new FileSynthesisCallback(mFile); 638 } 639 640 @Override 641 protected int playImpl() { 642 dispatchOnStart(); 643 int status = super.playImpl(); 644 if (status == TextToSpeech.SUCCESS) { 645 dispatchOnDone(); 646 } else { 647 dispatchOnError(); 648 } 649 return status; 650 } 651 652 /** 653 * Checks that the given file can be used for synthesis output. 654 */ 655 private boolean checkFile(File file) { 656 try { 657 if (file.exists()) { 658 Log.v(TAG, "File " + file + " exists, deleting."); 659 if (!file.delete()) { 660 Log.e(TAG, "Failed to delete " + file); 661 return false; 662 } 663 } 664 if (!file.createNewFile()) { 665 Log.e(TAG, "Can't create file " + file); 666 return false; 667 } 668 if (!file.delete()) { 669 Log.e(TAG, "Failed to delete " + file); 670 return false; 671 } 672 return true; 673 } catch (IOException e) { 674 Log.e(TAG, "Can't use " + file + " due to exception " + e); 675 return false; 676 } 677 } 678 } 679 680 private class AudioSpeechItem extends SpeechItem { 681 682 private final BlockingMediaPlayer mPlayer; 683 private AudioMessageParams mToken; 684 685 public AudioSpeechItem(String callingApp, Bundle params, Uri uri) { 686 super(callingApp, params); 687 mPlayer = new BlockingMediaPlayer(TextToSpeechService.this, uri, getStreamType()); 688 } 689 690 @Override 691 public boolean isValid() { 692 return true; 693 } 694 695 @Override 696 protected int playImpl() { 697 mToken = new AudioMessageParams(this, getCallingApp(), mPlayer); 698 mAudioPlaybackHandler.enqueueAudio(mToken); 699 return TextToSpeech.SUCCESS; 700 } 701 702 @Override 703 protected void stopImpl() { 704 // Do nothing. 705 } 706 } 707 708 private class SilenceSpeechItem extends SpeechItem { 709 private final long mDuration; 710 private SilenceMessageParams mToken; 711 712 public SilenceSpeechItem(String callingApp, Bundle params, long duration) { 713 super(callingApp, params); 714 mDuration = duration; 715 } 716 717 @Override 718 public boolean isValid() { 719 return true; 720 } 721 722 @Override 723 protected int playImpl() { 724 mToken = new SilenceMessageParams(this, getCallingApp(), mDuration); 725 mAudioPlaybackHandler.enqueueSilence(mToken); 726 return TextToSpeech.SUCCESS; 727 } 728 729 @Override 730 protected void stopImpl() { 731 // Do nothing. 732 } 733 } 734 735 @Override 736 public IBinder onBind(Intent intent) { 737 if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { 738 return mBinder; 739 } 740 return null; 741 } 742 743 /** 744 * Binder returned from {@code #onBind(Intent)}. The methods in this class can be 745 * called called from several different threads. 746 */ 747 // NOTE: All calls that are passed in a calling app are interned so that 748 // they can be used as message objects (which are tested for equality using ==). 749 private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { 750 751 public int speak(String callingApp, String text, int queueMode, Bundle params) { 752 if (!checkNonNull(callingApp, text, params)) { 753 return TextToSpeech.ERROR; 754 } 755 756 SpeechItem item = new SynthesisSpeechItem(intern(callingApp), params, text); 757 return mSynthHandler.enqueueSpeechItem(queueMode, item); 758 } 759 760 public int synthesizeToFile(String callingApp, String text, String filename, 761 Bundle params) { 762 if (!checkNonNull(callingApp, text, filename, params)) { 763 return TextToSpeech.ERROR; 764 } 765 766 File file = new File(filename); 767 SpeechItem item = new SynthesisToFileSpeechItem(intern(callingApp), 768 params, text, file); 769 return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); 770 } 771 772 public int playAudio(String callingApp, Uri audioUri, int queueMode, Bundle params) { 773 if (!checkNonNull(callingApp, audioUri, params)) { 774 return TextToSpeech.ERROR; 775 } 776 777 SpeechItem item = new AudioSpeechItem(intern(callingApp), params, audioUri); 778 return mSynthHandler.enqueueSpeechItem(queueMode, item); 779 } 780 781 public int playSilence(String callingApp, long duration, int queueMode, Bundle params) { 782 if (!checkNonNull(callingApp, params)) { 783 return TextToSpeech.ERROR; 784 } 785 786 SpeechItem item = new SilenceSpeechItem(intern(callingApp), params, duration); 787 return mSynthHandler.enqueueSpeechItem(queueMode, item); 788 } 789 790 public boolean isSpeaking() { 791 return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); 792 } 793 794 public int stop(String callingApp) { 795 if (!checkNonNull(callingApp)) { 796 return TextToSpeech.ERROR; 797 } 798 799 return mSynthHandler.stopForApp(intern(callingApp)); 800 } 801 802 public String[] getLanguage() { 803 return onGetLanguage(); 804 } 805 806 /* 807 * If defaults are enforced, then no language is "available" except 808 * perhaps the default language selected by the user. 809 */ 810 public int isLanguageAvailable(String lang, String country, String variant) { 811 if (!checkNonNull(lang)) { 812 return TextToSpeech.ERROR; 813 } 814 815 return onIsLanguageAvailable(lang, country, variant); 816 } 817 818 public String[] getFeaturesForLanguage(String lang, String country, String variant) { 819 Set<String> features = onGetFeaturesForLanguage(lang, country, variant); 820 String[] featuresArray = null; 821 if (features != null) { 822 featuresArray = new String[features.size()]; 823 features.toArray(featuresArray); 824 } else { 825 featuresArray = new String[0]; 826 } 827 return featuresArray; 828 } 829 830 /* 831 * There is no point loading a non default language if defaults 832 * are enforced. 833 */ 834 public int loadLanguage(String lang, String country, String variant) { 835 if (!checkNonNull(lang)) { 836 return TextToSpeech.ERROR; 837 } 838 839 return onLoadLanguage(lang, country, variant); 840 } 841 842 public void setCallback(String packageName, ITextToSpeechCallback cb) { 843 // Note that passing in a null callback is a valid use case. 844 if (!checkNonNull(packageName)) { 845 return; 846 } 847 848 mCallbacks.setCallback(packageName, cb); 849 } 850 851 private String intern(String in) { 852 // The input parameter will be non null. 853 return in.intern(); 854 } 855 856 private boolean checkNonNull(Object... args) { 857 for (Object o : args) { 858 if (o == null) return false; 859 } 860 return true; 861 } 862 }; 863 864 private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { 865 866 private final HashMap<String, ITextToSpeechCallback> mAppToCallback 867 = new HashMap<String, ITextToSpeechCallback>(); 868 869 public void setCallback(String packageName, ITextToSpeechCallback cb) { 870 synchronized (mAppToCallback) { 871 ITextToSpeechCallback old; 872 if (cb != null) { 873 register(cb, packageName); 874 old = mAppToCallback.put(packageName, cb); 875 } else { 876 old = mAppToCallback.remove(packageName); 877 } 878 if (old != null && old != cb) { 879 unregister(old); 880 } 881 } 882 } 883 884 public void dispatchOnDone(String packageName, String utteranceId) { 885 ITextToSpeechCallback cb = getCallbackFor(packageName); 886 if (cb == null) return; 887 try { 888 cb.onDone(utteranceId); 889 } catch (RemoteException e) { 890 Log.e(TAG, "Callback onDone failed: " + e); 891 } 892 } 893 894 public void dispatchOnStart(String packageName, String utteranceId) { 895 ITextToSpeechCallback cb = getCallbackFor(packageName); 896 if (cb == null) return; 897 try { 898 cb.onStart(utteranceId); 899 } catch (RemoteException e) { 900 Log.e(TAG, "Callback onStart failed: " + e); 901 } 902 903 } 904 905 public void dispatchOnError(String packageName, String utteranceId) { 906 ITextToSpeechCallback cb = getCallbackFor(packageName); 907 if (cb == null) return; 908 try { 909 cb.onError(utteranceId); 910 } catch (RemoteException e) { 911 Log.e(TAG, "Callback onError failed: " + e); 912 } 913 } 914 915 @Override 916 public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { 917 String packageName = (String) cookie; 918 synchronized (mAppToCallback) { 919 mAppToCallback.remove(packageName); 920 } 921 mSynthHandler.stopForApp(packageName); 922 } 923 924 @Override 925 public void kill() { 926 synchronized (mAppToCallback) { 927 mAppToCallback.clear(); 928 super.kill(); 929 } 930 } 931 932 private ITextToSpeechCallback getCallbackFor(String packageName) { 933 ITextToSpeechCallback cb; 934 synchronized (mAppToCallback) { 935 cb = mAppToCallback.get(packageName); 936 } 937 938 return cb; 939 } 940 941 } 942 943} 944