1/* 2 * Copyright (C) 2009 Google Inc. 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.tts; 17 18import android.app.Service; 19import android.content.ContentResolver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.SharedPreferences; 23import android.content.pm.PackageManager; 24import android.content.pm.PackageManager.NameNotFoundException; 25import android.media.AudioManager; 26import android.media.MediaPlayer; 27import android.media.MediaPlayer.OnCompletionListener; 28import android.net.Uri; 29import android.os.IBinder; 30import android.os.RemoteCallbackList; 31import android.os.RemoteException; 32import android.preference.PreferenceManager; 33import android.speech.tts.ITts.Stub; 34import android.speech.tts.ITtsCallback; 35import android.speech.tts.TextToSpeech; 36import android.util.Log; 37import java.util.ArrayList; 38import java.util.Arrays; 39import java.util.HashMap; 40import java.util.Locale; 41import java.util.concurrent.locks.ReentrantLock; 42import java.util.concurrent.TimeUnit; 43 44 45/** 46 * @hide Synthesizes speech from text. This is implemented as a service so that 47 * other applications can call the TTS without needing to bundle the TTS 48 * in the build. 49 * 50 */ 51public class TtsService extends Service implements OnCompletionListener { 52 53 private static class SpeechItem { 54 public static final int TEXT = 0; 55 public static final int EARCON = 1; 56 public static final int SILENCE = 2; 57 public static final int TEXT_TO_FILE = 3; 58 public String mText = ""; 59 public ArrayList<String> mParams = null; 60 public int mType = TEXT; 61 public long mDuration = 0; 62 public String mFilename = null; 63 public String mCallingApp = ""; 64 65 public SpeechItem(String source, String text, ArrayList<String> params, int itemType) { 66 mText = text; 67 mParams = params; 68 mType = itemType; 69 mCallingApp = source; 70 } 71 72 public SpeechItem(String source, long silenceTime, ArrayList<String> params) { 73 mDuration = silenceTime; 74 mParams = params; 75 mType = SILENCE; 76 mCallingApp = source; 77 } 78 79 public SpeechItem(String source, String text, ArrayList<String> params, 80 int itemType, String filename) { 81 mText = text; 82 mParams = params; 83 mType = itemType; 84 mFilename = filename; 85 mCallingApp = source; 86 } 87 88 } 89 90 /** 91 * Contains the information needed to access a sound resource; the name of 92 * the package that contains the resource and the resID of the resource 93 * within that package. 94 */ 95 private static class SoundResource { 96 public String mSourcePackageName = null; 97 public int mResId = -1; 98 public String mFilename = null; 99 100 public SoundResource(String packageName, int id) { 101 mSourcePackageName = packageName; 102 mResId = id; 103 mFilename = null; 104 } 105 106 public SoundResource(String file) { 107 mSourcePackageName = null; 108 mResId = -1; 109 mFilename = file; 110 } 111 } 112 // If the speech queue is locked for more than 5 seconds, something has gone 113 // very wrong with processSpeechQueue. 114 private static final int SPEECHQUEUELOCK_TIMEOUT = 5000; 115 private static final int MAX_SPEECH_ITEM_CHAR_LENGTH = 4000; 116 private static final int MAX_FILENAME_LENGTH = 250; 117 // TODO use the TTS stream type when available 118 private static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_MUSIC; 119 120 private static final String ACTION = "android.intent.action.START_TTS_SERVICE"; 121 private static final String CATEGORY = "android.intent.category.TTS"; 122 private static final String PKGNAME = "android.tts"; 123 124 private final RemoteCallbackList<ITtsCallback> mCallbacks 125 = new RemoteCallbackList<ITtsCallback>(); 126 127 private HashMap<String, ITtsCallback> mCallbacksMap; 128 129 private Boolean mIsSpeaking; 130 private ArrayList<SpeechItem> mSpeechQueue; 131 private HashMap<String, SoundResource> mEarcons; 132 private HashMap<String, SoundResource> mUtterances; 133 private MediaPlayer mPlayer; 134 private SpeechItem mCurrentSpeechItem; 135 private HashMap<SpeechItem, Boolean> mKillList; // Used to ensure that in-flight synth calls 136 // are killed when stop is used. 137 private TtsService mSelf; 138 139 private ContentResolver mResolver; 140 141 private final ReentrantLock speechQueueLock = new ReentrantLock(); 142 private final ReentrantLock synthesizerLock = new ReentrantLock(); 143 144 private static SynthProxy sNativeSynth = null; 145 @Override 146 public void onCreate() { 147 super.onCreate(); 148 Log.i("TtsService", "TtsService.onCreate()"); 149 150 mResolver = getContentResolver(); 151 152 String soLibPath = "/system/lib/libttspico.so"; 153 if (sNativeSynth == null) { 154 sNativeSynth = new SynthProxy(soLibPath); 155 } 156 157 mSelf = this; 158 mIsSpeaking = false; 159 160 mEarcons = new HashMap<String, SoundResource>(); 161 mUtterances = new HashMap<String, SoundResource>(); 162 mCallbacksMap = new HashMap<String, android.speech.tts.ITtsCallback>(); 163 164 mSpeechQueue = new ArrayList<SpeechItem>(); 165 mPlayer = null; 166 mCurrentSpeechItem = null; 167 mKillList = new HashMap<SpeechItem, Boolean>(); 168 169 setDefaultSettings(); 170 } 171 172 @Override 173 public void onDestroy() { 174 super.onDestroy(); 175 176 // TODO replace the call to stopAll() with a method to clear absolutely all upcoming 177 // uses of the native synth, including synthesis to a file, and delete files for which 178 // synthesis was not complete. 179 stopAll(""); 180 181 // Don't hog the media player 182 cleanUpPlayer(); 183 184 if (sNativeSynth != null) { 185 sNativeSynth.shutdown(); 186 } 187 sNativeSynth = null; 188 189 // Unregister all callbacks. 190 mCallbacks.kill(); 191 } 192 193 194 private void setDefaultSettings() { 195 setLanguage("", this.getDefaultLanguage(), getDefaultCountry(), getDefaultLocVariant()); 196 197 // speech rate 198 setSpeechRate("", getDefaultRate()); 199 } 200 201 202 private boolean isDefaultEnforced() { 203 return (android.provider.Settings.Secure.getInt(mResolver, 204 android.provider.Settings.Secure.TTS_USE_DEFAULTS, 205 TextToSpeech.Engine.USE_DEFAULTS) 206 == 1 ); 207 } 208 209 210 private int getDefaultRate() { 211 return android.provider.Settings.Secure.getInt(mResolver, 212 android.provider.Settings.Secure.TTS_DEFAULT_RATE, 213 TextToSpeech.Engine.DEFAULT_RATE); 214 } 215 216 217 private String getDefaultLanguage() { 218 String defaultLang = android.provider.Settings.Secure.getString(mResolver, 219 android.provider.Settings.Secure.TTS_DEFAULT_LANG); 220 if (defaultLang == null) { 221 // no setting found, use the current Locale to determine the default language 222 return Locale.getDefault().getISO3Language(); 223 } else { 224 return defaultLang; 225 } 226 } 227 228 229 private String getDefaultCountry() { 230 String defaultCountry = android.provider.Settings.Secure.getString(mResolver, 231 android.provider.Settings.Secure.TTS_DEFAULT_COUNTRY); 232 if (defaultCountry == null) { 233 // no setting found, use the current Locale to determine the default country 234 return Locale.getDefault().getISO3Country(); 235 } else { 236 return defaultCountry; 237 } 238 } 239 240 241 private String getDefaultLocVariant() { 242 String defaultVar = android.provider.Settings.Secure.getString(mResolver, 243 android.provider.Settings.Secure.TTS_DEFAULT_VARIANT); 244 if (defaultVar == null) { 245 // no setting found, use the current Locale to determine the default variant 246 return Locale.getDefault().getVariant(); 247 } else { 248 return defaultVar; 249 } 250 } 251 252 253 private int setSpeechRate(String callingApp, int rate) { 254 int res = TextToSpeech.ERROR; 255 try { 256 if (isDefaultEnforced()) { 257 res = sNativeSynth.setSpeechRate(getDefaultRate()); 258 } else { 259 res = sNativeSynth.setSpeechRate(rate); 260 } 261 } catch (NullPointerException e) { 262 // synth will become null during onDestroy() 263 res = TextToSpeech.ERROR; 264 } 265 return res; 266 } 267 268 269 private int setPitch(String callingApp, int pitch) { 270 int res = TextToSpeech.ERROR; 271 try { 272 res = sNativeSynth.setPitch(pitch); 273 } catch (NullPointerException e) { 274 // synth will become null during onDestroy() 275 res = TextToSpeech.ERROR; 276 } 277 return res; 278 } 279 280 281 private int isLanguageAvailable(String lang, String country, String variant) { 282 //Log.v("TtsService", "TtsService.isLanguageAvailable(" + lang + ", " + country + ", " +variant+")"); 283 int res = TextToSpeech.LANG_NOT_SUPPORTED; 284 try { 285 res = sNativeSynth.isLanguageAvailable(lang, country, variant); 286 } catch (NullPointerException e) { 287 // synth will become null during onDestroy() 288 res = TextToSpeech.LANG_NOT_SUPPORTED; 289 } 290 return res; 291 } 292 293 294 private String[] getLanguage() { 295 try { 296 return sNativeSynth.getLanguage(); 297 } catch (Exception e) { 298 return null; 299 } 300 } 301 302 303 private int setLanguage(String callingApp, String lang, String country, String variant) { 304 Log.v("TtsService", "TtsService.setLanguage(" + lang + ", " + country + ", " + variant + ")"); 305 int res = TextToSpeech.ERROR; 306 try { 307 if (isDefaultEnforced()) { 308 res = sNativeSynth.setLanguage(getDefaultLanguage(), getDefaultCountry(), 309 getDefaultLocVariant()); 310 } else { 311 res = sNativeSynth.setLanguage(lang, country, variant); 312 } 313 } catch (NullPointerException e) { 314 // synth will become null during onDestroy() 315 res = TextToSpeech.ERROR; 316 } 317 return res; 318 } 319 320 321 /** 322 * Adds a sound resource to the TTS. 323 * 324 * @param text 325 * The text that should be associated with the sound resource 326 * @param packageName 327 * The name of the package which has the sound resource 328 * @param resId 329 * The resource ID of the sound within its package 330 */ 331 private void addSpeech(String callingApp, String text, String packageName, int resId) { 332 mUtterances.put(text, new SoundResource(packageName, resId)); 333 } 334 335 /** 336 * Adds a sound resource to the TTS. 337 * 338 * @param text 339 * The text that should be associated with the sound resource 340 * @param filename 341 * The filename of the sound resource. This must be a complete 342 * path like: (/sdcard/mysounds/mysoundbite.mp3). 343 */ 344 private void addSpeech(String callingApp, String text, String filename) { 345 mUtterances.put(text, new SoundResource(filename)); 346 } 347 348 /** 349 * Adds a sound resource to the TTS as an earcon. 350 * 351 * @param earcon 352 * The text that should be associated with the sound resource 353 * @param packageName 354 * The name of the package which has the sound resource 355 * @param resId 356 * The resource ID of the sound within its package 357 */ 358 private void addEarcon(String callingApp, String earcon, String packageName, int resId) { 359 mEarcons.put(earcon, new SoundResource(packageName, resId)); 360 } 361 362 /** 363 * Adds a sound resource to the TTS as an earcon. 364 * 365 * @param earcon 366 * The text that should be associated with the sound resource 367 * @param filename 368 * The filename of the sound resource. This must be a complete 369 * path like: (/sdcard/mysounds/mysoundbite.mp3). 370 */ 371 private void addEarcon(String callingApp, String earcon, String filename) { 372 mEarcons.put(earcon, new SoundResource(filename)); 373 } 374 375 /** 376 * Speaks the given text using the specified queueing mode and parameters. 377 * 378 * @param text 379 * The text that should be spoken 380 * @param queueMode 381 * TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances), 382 * TextToSpeech.TTS_QUEUE_ADD for queued 383 * @param params 384 * An ArrayList of parameters. This is not implemented for all 385 * engines. 386 */ 387 private int speak(String callingApp, String text, int queueMode, ArrayList<String> params) { 388 Log.v("TtsService", "TTS service received " + text); 389 if (queueMode == TextToSpeech.QUEUE_FLUSH) { 390 stop(callingApp); 391 } else if (queueMode == 2) { 392 stopAll(callingApp); 393 } 394 mSpeechQueue.add(new SpeechItem(callingApp, text, params, SpeechItem.TEXT)); 395 if (!mIsSpeaking) { 396 processSpeechQueue(); 397 } 398 return TextToSpeech.SUCCESS; 399 } 400 401 /** 402 * Plays the earcon using the specified queueing mode and parameters. 403 * 404 * @param earcon 405 * The earcon that should be played 406 * @param queueMode 407 * TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances), 408 * TextToSpeech.TTS_QUEUE_ADD for queued 409 * @param params 410 * An ArrayList of parameters. This is not implemented for all 411 * engines. 412 */ 413 private int playEarcon(String callingApp, String earcon, int queueMode, 414 ArrayList<String> params) { 415 if (queueMode == TextToSpeech.QUEUE_FLUSH) { 416 stop(callingApp); 417 } else if (queueMode == 2) { 418 stopAll(callingApp); 419 } 420 mSpeechQueue.add(new SpeechItem(callingApp, earcon, params, SpeechItem.EARCON)); 421 if (!mIsSpeaking) { 422 processSpeechQueue(); 423 } 424 return TextToSpeech.SUCCESS; 425 } 426 427 /** 428 * Stops all speech output and removes any utterances still in the queue for the calling app. 429 */ 430 private int stop(String callingApp) { 431 int result = TextToSpeech.ERROR; 432 boolean speechQueueAvailable = false; 433 try{ 434 speechQueueAvailable = 435 speechQueueLock.tryLock(SPEECHQUEUELOCK_TIMEOUT, TimeUnit.MILLISECONDS); 436 if (speechQueueAvailable) { 437 Log.i("TtsService", "Stopping"); 438 for (int i = mSpeechQueue.size() - 1; i > -1; i--){ 439 if (mSpeechQueue.get(i).mCallingApp.equals(callingApp)){ 440 mSpeechQueue.remove(i); 441 } 442 } 443 if ((mCurrentSpeechItem != null) && 444 mCurrentSpeechItem.mCallingApp.equals(callingApp)) { 445 try { 446 result = sNativeSynth.stop(); 447 } catch (NullPointerException e1) { 448 // synth will become null during onDestroy() 449 result = TextToSpeech.ERROR; 450 } 451 mKillList.put(mCurrentSpeechItem, true); 452 if (mPlayer != null) { 453 try { 454 mPlayer.stop(); 455 } catch (IllegalStateException e) { 456 // Do nothing, the player is already stopped. 457 } 458 } 459 mIsSpeaking = false; 460 mCurrentSpeechItem = null; 461 } else { 462 result = TextToSpeech.SUCCESS; 463 } 464 Log.i("TtsService", "Stopped"); 465 } 466 } catch (InterruptedException e) { 467 Log.e("TtsService", "TTS stop: tryLock interrupted"); 468 e.printStackTrace(); 469 } finally { 470 // This check is needed because finally will always run; even if the 471 // method returns somewhere in the try block. 472 if (speechQueueAvailable) { 473 speechQueueLock.unlock(); 474 } 475 return result; 476 } 477 } 478 479 480 481 /** 482 * Stops all speech output and removes any utterances still in the queue globally, except 483 * those intended to be synthesized to file. 484 */ 485 private int stopAll(String callingApp) { 486 int result = TextToSpeech.ERROR; 487 boolean speechQueueAvailable = false; 488 try{ 489 speechQueueAvailable = 490 speechQueueLock.tryLock(SPEECHQUEUELOCK_TIMEOUT, TimeUnit.MILLISECONDS); 491 if (speechQueueAvailable) { 492 for (int i = mSpeechQueue.size() - 1; i > -1; i--){ 493 if (mSpeechQueue.get(i).mType != SpeechItem.TEXT_TO_FILE){ 494 mSpeechQueue.remove(i); 495 } 496 } 497 if ((mCurrentSpeechItem != null) && 498 ((mCurrentSpeechItem.mType != SpeechItem.TEXT_TO_FILE) || 499 mCurrentSpeechItem.mCallingApp.equals(callingApp))) { 500 try { 501 result = sNativeSynth.stop(); 502 } catch (NullPointerException e1) { 503 // synth will become null during onDestroy() 504 result = TextToSpeech.ERROR; 505 } 506 mKillList.put(mCurrentSpeechItem, true); 507 if (mPlayer != null) { 508 try { 509 mPlayer.stop(); 510 } catch (IllegalStateException e) { 511 // Do nothing, the player is already stopped. 512 } 513 } 514 mIsSpeaking = false; 515 mCurrentSpeechItem = null; 516 } else { 517 result = TextToSpeech.SUCCESS; 518 } 519 Log.i("TtsService", "Stopped all"); 520 } 521 } catch (InterruptedException e) { 522 Log.e("TtsService", "TTS stopAll: tryLock interrupted"); 523 e.printStackTrace(); 524 } finally { 525 // This check is needed because finally will always run; even if the 526 // method returns somewhere in the try block. 527 if (speechQueueAvailable) { 528 speechQueueLock.unlock(); 529 } 530 return result; 531 } 532 } 533 534 public void onCompletion(MediaPlayer arg0) { 535 String callingApp = mCurrentSpeechItem.mCallingApp; 536 ArrayList<String> params = mCurrentSpeechItem.mParams; 537 String utteranceId = ""; 538 if (params != null){ 539 for (int i = 0; i < params.size() - 1; i = i + 2){ 540 String param = params.get(i); 541 if (param.equals(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID)){ 542 utteranceId = params.get(i+1); 543 } 544 } 545 } 546 if (utteranceId.length() > 0){ 547 dispatchUtteranceCompletedCallback(utteranceId, callingApp); 548 } 549 processSpeechQueue(); 550 } 551 552 private int playSilence(String callingApp, long duration, int queueMode, 553 ArrayList<String> params) { 554 if (queueMode == TextToSpeech.QUEUE_FLUSH) { 555 stop(callingApp); 556 } 557 mSpeechQueue.add(new SpeechItem(callingApp, duration, params)); 558 if (!mIsSpeaking) { 559 processSpeechQueue(); 560 } 561 return TextToSpeech.SUCCESS; 562 } 563 564 private void silence(final SpeechItem speechItem) { 565 class SilenceThread implements Runnable { 566 public void run() { 567 String utteranceId = ""; 568 if (speechItem.mParams != null){ 569 for (int i = 0; i < speechItem.mParams.size() - 1; i = i + 2){ 570 String param = speechItem.mParams.get(i); 571 if (param.equals(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID)){ 572 utteranceId = speechItem.mParams.get(i+1); 573 } 574 } 575 } 576 try { 577 Thread.sleep(speechItem.mDuration); 578 } catch (InterruptedException e) { 579 e.printStackTrace(); 580 } finally { 581 if (utteranceId.length() > 0){ 582 dispatchUtteranceCompletedCallback(utteranceId, speechItem.mCallingApp); 583 } 584 processSpeechQueue(); 585 } 586 } 587 } 588 Thread slnc = (new Thread(new SilenceThread())); 589 slnc.setPriority(Thread.MIN_PRIORITY); 590 slnc.start(); 591 } 592 593 private void speakInternalOnly(final SpeechItem speechItem) { 594 class SynthThread implements Runnable { 595 public void run() { 596 boolean synthAvailable = false; 597 String utteranceId = ""; 598 try { 599 synthAvailable = synthesizerLock.tryLock(); 600 if (!synthAvailable) { 601 Thread.sleep(100); 602 Thread synth = (new Thread(new SynthThread())); 603 //synth.setPriority(Thread.MIN_PRIORITY); 604 synth.start(); 605 return; 606 } 607 int streamType = DEFAULT_STREAM_TYPE; 608 String language = ""; 609 String country = ""; 610 String variant = ""; 611 String speechRate = ""; 612 if (speechItem.mParams != null){ 613 for (int i = 0; i < speechItem.mParams.size() - 1; i = i + 2){ 614 String param = speechItem.mParams.get(i); 615 if (param != null) { 616 if (param.equals(TextToSpeech.Engine.KEY_PARAM_RATE)) { 617 speechRate = speechItem.mParams.get(i+1); 618 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_LANGUAGE)){ 619 language = speechItem.mParams.get(i+1); 620 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_COUNTRY)){ 621 country = speechItem.mParams.get(i+1); 622 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_VARIANT)){ 623 variant = speechItem.mParams.get(i+1); 624 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID)){ 625 utteranceId = speechItem.mParams.get(i+1); 626 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_STREAM)) { 627 try { 628 streamType 629 = Integer.parseInt(speechItem.mParams.get(i + 1)); 630 } catch (NumberFormatException e) { 631 streamType = DEFAULT_STREAM_TYPE; 632 } 633 } 634 } 635 } 636 } 637 // Only do the synthesis if it has not been killed by a subsequent utterance. 638 if (mKillList.get(speechItem) == null) { 639 if (language.length() > 0){ 640 setLanguage("", language, country, variant); 641 } 642 if (speechRate.length() > 0){ 643 setSpeechRate("", Integer.parseInt(speechRate)); 644 } 645 try { 646 sNativeSynth.speak(speechItem.mText, streamType); 647 } catch (NullPointerException e) { 648 // synth will become null during onDestroy() 649 Log.v("TtsService", " null synth, can't speak"); 650 } 651 } 652 } catch (InterruptedException e) { 653 Log.e("TtsService", "TTS speakInternalOnly(): tryLock interrupted"); 654 e.printStackTrace(); 655 } finally { 656 // This check is needed because finally will always run; 657 // even if the 658 // method returns somewhere in the try block. 659 if (utteranceId.length() > 0){ 660 dispatchUtteranceCompletedCallback(utteranceId, speechItem.mCallingApp); 661 } 662 if (synthAvailable) { 663 synthesizerLock.unlock(); 664 } 665 processSpeechQueue(); 666 } 667 } 668 } 669 Thread synth = (new Thread(new SynthThread())); 670 //synth.setPriority(Thread.MIN_PRIORITY); 671 synth.start(); 672 } 673 674 private void synthToFileInternalOnly(final SpeechItem speechItem) { 675 class SynthThread implements Runnable { 676 public void run() { 677 boolean synthAvailable = false; 678 String utteranceId = ""; 679 Log.i("TtsService", "Synthesizing to " + speechItem.mFilename); 680 try { 681 synthAvailable = synthesizerLock.tryLock(); 682 if (!synthAvailable) { 683 Thread.sleep(100); 684 Thread synth = (new Thread(new SynthThread())); 685 //synth.setPriority(Thread.MIN_PRIORITY); 686 synth.start(); 687 return; 688 } 689 String language = ""; 690 String country = ""; 691 String variant = ""; 692 String speechRate = ""; 693 if (speechItem.mParams != null){ 694 for (int i = 0; i < speechItem.mParams.size() - 1; i = i + 2){ 695 String param = speechItem.mParams.get(i); 696 if (param != null) { 697 if (param.equals(TextToSpeech.Engine.KEY_PARAM_RATE)) { 698 speechRate = speechItem.mParams.get(i+1); 699 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_LANGUAGE)){ 700 language = speechItem.mParams.get(i+1); 701 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_COUNTRY)){ 702 country = speechItem.mParams.get(i+1); 703 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_VARIANT)){ 704 variant = speechItem.mParams.get(i+1); 705 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID)){ 706 utteranceId = speechItem.mParams.get(i+1); 707 } 708 } 709 } 710 } 711 // Only do the synthesis if it has not been killed by a subsequent utterance. 712 if (mKillList.get(speechItem) == null){ 713 if (language.length() > 0){ 714 setLanguage("", language, country, variant); 715 } 716 if (speechRate.length() > 0){ 717 setSpeechRate("", Integer.parseInt(speechRate)); 718 } 719 try { 720 sNativeSynth.synthesizeToFile(speechItem.mText, speechItem.mFilename); 721 } catch (NullPointerException e) { 722 // synth will become null during onDestroy() 723 Log.v("TtsService", " null synth, can't synthesize to file"); 724 } 725 } 726 } catch (InterruptedException e) { 727 Log.e("TtsService", "TTS synthToFileInternalOnly(): tryLock interrupted"); 728 e.printStackTrace(); 729 } finally { 730 // This check is needed because finally will always run; 731 // even if the 732 // method returns somewhere in the try block. 733 if (utteranceId.length() > 0){ 734 dispatchUtteranceCompletedCallback(utteranceId, speechItem.mCallingApp); 735 } 736 if (synthAvailable) { 737 synthesizerLock.unlock(); 738 } 739 processSpeechQueue(); 740 } 741 } 742 } 743 Thread synth = (new Thread(new SynthThread())); 744 //synth.setPriority(Thread.MIN_PRIORITY); 745 synth.start(); 746 } 747 748 private SoundResource getSoundResource(SpeechItem speechItem) { 749 SoundResource sr = null; 750 String text = speechItem.mText; 751 if (speechItem.mType == SpeechItem.SILENCE) { 752 // Do nothing if this is just silence 753 } else if (speechItem.mType == SpeechItem.EARCON) { 754 sr = mEarcons.get(text); 755 } else { 756 sr = mUtterances.get(text); 757 } 758 return sr; 759 } 760 761 private void broadcastTtsQueueProcessingCompleted(){ 762 Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED); 763 sendBroadcast(i); 764 } 765 766 767 private void dispatchUtteranceCompletedCallback(String utteranceId, String packageName) { 768 ITtsCallback cb = mCallbacksMap.get(packageName); 769 if (cb == null){ 770 return; 771 } 772 Log.i("TtsService", "TTS callback: dispatch started"); 773 // Broadcast to all clients the new value. 774 final int N = mCallbacks.beginBroadcast(); 775 try { 776 cb.utteranceCompleted(utteranceId); 777 } catch (RemoteException e) { 778 // The RemoteCallbackList will take care of removing 779 // the dead object for us. 780 } 781 mCallbacks.finishBroadcast(); 782 Log.i("TtsService", "TTS callback: dispatch completed to " + N); 783 } 784 785 private SpeechItem splitCurrentTextIfNeeded(SpeechItem currentSpeechItem){ 786 if (currentSpeechItem.mText.length() < MAX_SPEECH_ITEM_CHAR_LENGTH){ 787 return currentSpeechItem; 788 } else { 789 String callingApp = currentSpeechItem.mCallingApp; 790 ArrayList<SpeechItem> splitItems = new ArrayList<SpeechItem>(); 791 int start = 0; 792 int end = start + MAX_SPEECH_ITEM_CHAR_LENGTH - 1; 793 String splitText; 794 SpeechItem splitItem; 795 while (end < currentSpeechItem.mText.length()){ 796 splitText = currentSpeechItem.mText.substring(start, end); 797 splitItem = new SpeechItem(callingApp, splitText, null, SpeechItem.TEXT); 798 splitItems.add(splitItem); 799 start = end; 800 end = start + MAX_SPEECH_ITEM_CHAR_LENGTH - 1; 801 } 802 splitText = currentSpeechItem.mText.substring(start); 803 splitItem = new SpeechItem(callingApp, splitText, null, SpeechItem.TEXT); 804 splitItems.add(splitItem); 805 mSpeechQueue.remove(0); 806 for (int i = splitItems.size() - 1; i >= 0; i--){ 807 mSpeechQueue.add(0, splitItems.get(i)); 808 } 809 return mSpeechQueue.get(0); 810 } 811 } 812 813 private void processSpeechQueue() { 814 boolean speechQueueAvailable = false; 815 try { 816 speechQueueAvailable = 817 speechQueueLock.tryLock(SPEECHQUEUELOCK_TIMEOUT, TimeUnit.MILLISECONDS); 818 if (!speechQueueAvailable) { 819 Log.e("TtsService", "processSpeechQueue - Speech queue is unavailable."); 820 return; 821 } 822 if (mSpeechQueue.size() < 1) { 823 mIsSpeaking = false; 824 broadcastTtsQueueProcessingCompleted(); 825 return; 826 } 827 828 mCurrentSpeechItem = mSpeechQueue.get(0); 829 mIsSpeaking = true; 830 SoundResource sr = getSoundResource(mCurrentSpeechItem); 831 // Synth speech as needed - synthesizer should call 832 // processSpeechQueue to continue running the queue 833 Log.i("TtsService", "TTS processing: " + mCurrentSpeechItem.mText); 834 if (sr == null) { 835 if (mCurrentSpeechItem.mType == SpeechItem.TEXT) { 836 mCurrentSpeechItem = splitCurrentTextIfNeeded(mCurrentSpeechItem); 837 speakInternalOnly(mCurrentSpeechItem); 838 } else if (mCurrentSpeechItem.mType == SpeechItem.TEXT_TO_FILE) { 839 synthToFileInternalOnly(mCurrentSpeechItem); 840 } else { 841 // This is either silence or an earcon that was missing 842 silence(mCurrentSpeechItem); 843 } 844 } else { 845 cleanUpPlayer(); 846 if (sr.mSourcePackageName == PKGNAME) { 847 // Utterance is part of the TTS library 848 mPlayer = MediaPlayer.create(this, sr.mResId); 849 } else if (sr.mSourcePackageName != null) { 850 // Utterance is part of the app calling the library 851 Context ctx; 852 try { 853 ctx = this.createPackageContext(sr.mSourcePackageName, 0); 854 } catch (NameNotFoundException e) { 855 e.printStackTrace(); 856 mSpeechQueue.remove(0); // Remove it from the queue and 857 // move on 858 mIsSpeaking = false; 859 return; 860 } 861 mPlayer = MediaPlayer.create(ctx, sr.mResId); 862 } else { 863 // Utterance is coming from a file 864 mPlayer = MediaPlayer.create(this, Uri.parse(sr.mFilename)); 865 } 866 867 // Check if Media Server is dead; if it is, clear the queue and 868 // give up for now - hopefully, it will recover itself. 869 if (mPlayer == null) { 870 mSpeechQueue.clear(); 871 mIsSpeaking = false; 872 return; 873 } 874 mPlayer.setOnCompletionListener(this); 875 try { 876 mPlayer.setAudioStreamType(getStreamTypeFromParams(mCurrentSpeechItem.mParams)); 877 mPlayer.start(); 878 } catch (IllegalStateException e) { 879 mSpeechQueue.clear(); 880 mIsSpeaking = false; 881 cleanUpPlayer(); 882 return; 883 } 884 } 885 if (mSpeechQueue.size() > 0) { 886 mSpeechQueue.remove(0); 887 } 888 } catch (InterruptedException e) { 889 Log.e("TtsService", "TTS processSpeechQueue: tryLock interrupted"); 890 e.printStackTrace(); 891 } finally { 892 // This check is needed because finally will always run; even if the 893 // method returns somewhere in the try block. 894 if (speechQueueAvailable) { 895 speechQueueLock.unlock(); 896 } 897 } 898 } 899 900 private int getStreamTypeFromParams(ArrayList<String> paramList) { 901 int streamType = DEFAULT_STREAM_TYPE; 902 if (paramList == null) { 903 return streamType; 904 } 905 for (int i = 0; i < paramList.size() - 1; i = i + 2) { 906 String param = paramList.get(i); 907 if ((param != null) && (param.equals(TextToSpeech.Engine.KEY_PARAM_STREAM))) { 908 try { 909 streamType = Integer.parseInt(paramList.get(i + 1)); 910 } catch (NumberFormatException e) { 911 streamType = DEFAULT_STREAM_TYPE; 912 } 913 } 914 } 915 return streamType; 916 } 917 918 private void cleanUpPlayer() { 919 if (mPlayer != null) { 920 mPlayer.release(); 921 mPlayer = null; 922 } 923 } 924 925 /** 926 * Synthesizes the given text to a file using the specified parameters. 927 * 928 * @param text 929 * The String of text that should be synthesized 930 * @param params 931 * An ArrayList of parameters. The first element of this array 932 * controls the type of voice to use. 933 * @param filename 934 * The string that gives the full output filename; it should be 935 * something like "/sdcard/myappsounds/mysound.wav". 936 * @return A boolean that indicates if the synthesis succeeded 937 */ 938 private boolean synthesizeToFile(String callingApp, String text, ArrayList<String> params, 939 String filename) { 940 // Don't allow a filename that is too long 941 if (filename.length() > MAX_FILENAME_LENGTH) { 942 return false; 943 } 944 // Don't allow anything longer than the max text length; since this 945 // is synthing to a file, don't even bother splitting it. 946 if (text.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH){ 947 return false; 948 } 949 mSpeechQueue.add(new SpeechItem(callingApp, text, params, SpeechItem.TEXT_TO_FILE, filename)); 950 if (!mIsSpeaking) { 951 processSpeechQueue(); 952 } 953 return true; 954 } 955 956 @Override 957 public IBinder onBind(Intent intent) { 958 if (ACTION.equals(intent.getAction())) { 959 for (String category : intent.getCategories()) { 960 if (category.equals(CATEGORY)) { 961 return mBinder; 962 } 963 } 964 } 965 return null; 966 } 967 968 private final android.speech.tts.ITts.Stub mBinder = new Stub() { 969 970 public int registerCallback(String packageName, ITtsCallback cb) { 971 if (cb != null) { 972 mCallbacks.register(cb); 973 mCallbacksMap.put(packageName, cb); 974 return TextToSpeech.SUCCESS; 975 } 976 return TextToSpeech.ERROR; 977 } 978 979 public int unregisterCallback(String packageName, ITtsCallback cb) { 980 if (cb != null) { 981 mCallbacksMap.remove(packageName); 982 mCallbacks.unregister(cb); 983 return TextToSpeech.SUCCESS; 984 } 985 return TextToSpeech.ERROR; 986 } 987 988 /** 989 * Speaks the given text using the specified queueing mode and 990 * parameters. 991 * 992 * @param text 993 * The text that should be spoken 994 * @param queueMode 995 * TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances) 996 * TextToSpeech.TTS_QUEUE_ADD for queued 997 * @param params 998 * An ArrayList of parameters. The first element of this 999 * array controls the type of voice to use. 1000 */ 1001 public int speak(String callingApp, String text, int queueMode, String[] params) { 1002 ArrayList<String> speakingParams = new ArrayList<String>(); 1003 if (params != null) { 1004 speakingParams = new ArrayList<String>(Arrays.asList(params)); 1005 } 1006 return mSelf.speak(callingApp, text, queueMode, speakingParams); 1007 } 1008 1009 /** 1010 * Plays the earcon using the specified queueing mode and parameters. 1011 * 1012 * @param earcon 1013 * The earcon that should be played 1014 * @param queueMode 1015 * TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances) 1016 * TextToSpeech.TTS_QUEUE_ADD for queued 1017 * @param params 1018 * An ArrayList of parameters. 1019 */ 1020 public int playEarcon(String callingApp, String earcon, int queueMode, String[] params) { 1021 ArrayList<String> speakingParams = new ArrayList<String>(); 1022 if (params != null) { 1023 speakingParams = new ArrayList<String>(Arrays.asList(params)); 1024 } 1025 return mSelf.playEarcon(callingApp, earcon, queueMode, speakingParams); 1026 } 1027 1028 /** 1029 * Plays the silence using the specified queueing mode and parameters. 1030 * 1031 * @param duration 1032 * The duration of the silence that should be played 1033 * @param queueMode 1034 * TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances) 1035 * TextToSpeech.TTS_QUEUE_ADD for queued 1036 * @param params 1037 * An ArrayList of parameters. 1038 */ 1039 public int playSilence(String callingApp, long duration, int queueMode, String[] params) { 1040 ArrayList<String> speakingParams = new ArrayList<String>(); 1041 if (params != null) { 1042 speakingParams = new ArrayList<String>(Arrays.asList(params)); 1043 } 1044 return mSelf.playSilence(callingApp, duration, queueMode, speakingParams); 1045 } 1046 1047 /** 1048 * Stops all speech output and removes any utterances still in the 1049 * queue. 1050 */ 1051 public int stop(String callingApp) { 1052 return mSelf.stop(callingApp); 1053 } 1054 1055 /** 1056 * Returns whether or not the TTS is speaking. 1057 * 1058 * @return Boolean to indicate whether or not the TTS is speaking 1059 */ 1060 public boolean isSpeaking() { 1061 return (mSelf.mIsSpeaking && (mSpeechQueue.size() < 1)); 1062 } 1063 1064 /** 1065 * Adds a sound resource to the TTS. 1066 * 1067 * @param text 1068 * The text that should be associated with the sound resource 1069 * @param packageName 1070 * The name of the package which has the sound resource 1071 * @param resId 1072 * The resource ID of the sound within its package 1073 */ 1074 public void addSpeech(String callingApp, String text, String packageName, int resId) { 1075 mSelf.addSpeech(callingApp, text, packageName, resId); 1076 } 1077 1078 /** 1079 * Adds a sound resource to the TTS. 1080 * 1081 * @param text 1082 * The text that should be associated with the sound resource 1083 * @param filename 1084 * The filename of the sound resource. This must be a 1085 * complete path like: (/sdcard/mysounds/mysoundbite.mp3). 1086 */ 1087 public void addSpeechFile(String callingApp, String text, String filename) { 1088 mSelf.addSpeech(callingApp, text, filename); 1089 } 1090 1091 /** 1092 * Adds a sound resource to the TTS as an earcon. 1093 * 1094 * @param earcon 1095 * The text that should be associated with the sound resource 1096 * @param packageName 1097 * The name of the package which has the sound resource 1098 * @param resId 1099 * The resource ID of the sound within its package 1100 */ 1101 public void addEarcon(String callingApp, String earcon, String packageName, int resId) { 1102 mSelf.addEarcon(callingApp, earcon, packageName, resId); 1103 } 1104 1105 /** 1106 * Adds a sound resource to the TTS as an earcon. 1107 * 1108 * @param earcon 1109 * The text that should be associated with the sound resource 1110 * @param filename 1111 * The filename of the sound resource. This must be a 1112 * complete path like: (/sdcard/mysounds/mysoundbite.mp3). 1113 */ 1114 public void addEarconFile(String callingApp, String earcon, String filename) { 1115 mSelf.addEarcon(callingApp, earcon, filename); 1116 } 1117 1118 /** 1119 * Sets the speech rate for the TTS. Note that this will only have an 1120 * effect on synthesized speech; it will not affect pre-recorded speech. 1121 * 1122 * @param speechRate 1123 * The speech rate that should be used 1124 */ 1125 public int setSpeechRate(String callingApp, int speechRate) { 1126 return mSelf.setSpeechRate(callingApp, speechRate); 1127 } 1128 1129 /** 1130 * Sets the pitch for the TTS. Note that this will only have an 1131 * effect on synthesized speech; it will not affect pre-recorded speech. 1132 * 1133 * @param pitch 1134 * The pitch that should be used for the synthesized voice 1135 */ 1136 public int setPitch(String callingApp, int pitch) { 1137 return mSelf.setPitch(callingApp, pitch); 1138 } 1139 1140 /** 1141 * Returns the level of support for the specified language. 1142 * 1143 * @param lang the three letter ISO language code. 1144 * @param country the three letter ISO country code. 1145 * @param variant the variant code associated with the country and language pair. 1146 * @return one of TTS_LANG_NOT_SUPPORTED, TTS_LANG_MISSING_DATA, TTS_LANG_AVAILABLE, 1147 * TTS_LANG_COUNTRY_AVAILABLE, TTS_LANG_COUNTRY_VAR_AVAILABLE as defined in 1148 * android.speech.tts.TextToSpeech. 1149 */ 1150 public int isLanguageAvailable(String lang, String country, String variant) { 1151 return mSelf.isLanguageAvailable(lang, country, variant); 1152 } 1153 1154 /** 1155 * Returns the currently set language / country / variant strings representing the 1156 * language used by the TTS engine. 1157 * @return null is no language is set, or an array of 3 string containing respectively 1158 * the language, country and variant. 1159 */ 1160 public String[] getLanguage() { 1161 return mSelf.getLanguage(); 1162 } 1163 1164 /** 1165 * Sets the speech rate for the TTS, which affects the synthesized voice. 1166 * 1167 * @param lang the three letter ISO language code. 1168 * @param country the three letter ISO country code. 1169 * @param variant the variant code associated with the country and language pair. 1170 */ 1171 public int setLanguage(String callingApp, String lang, String country, String variant) { 1172 return mSelf.setLanguage(callingApp, lang, country, variant); 1173 } 1174 1175 /** 1176 * Synthesizes the given text to a file using the specified 1177 * parameters. 1178 * 1179 * @param text 1180 * The String of text that should be synthesized 1181 * @param params 1182 * An ArrayList of parameters. The first element of this 1183 * array controls the type of voice to use. 1184 * @param filename 1185 * The string that gives the full output filename; it should 1186 * be something like "/sdcard/myappsounds/mysound.wav". 1187 * @return A boolean that indicates if the synthesis succeeded 1188 */ 1189 public boolean synthesizeToFile(String callingApp, String text, String[] params, 1190 String filename) { 1191 ArrayList<String> speakingParams = new ArrayList<String>(); 1192 if (params != null) { 1193 speakingParams = new ArrayList<String>(Arrays.asList(params)); 1194 } 1195 return mSelf.synthesizeToFile(callingApp, text, speakingParams, filename); 1196 } 1197 1198 }; 1199 1200} 1201