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