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