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.annotation.NonNull;
19 import android.app.Service;
20 import android.content.Intent;
21 import android.media.AudioAttributes;
22 import android.media.AudioManager;
23 import android.net.Uri;
24 import android.os.Binder;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.HandlerThread;
28 import android.os.IBinder;
29 import android.os.Looper;
30 import android.os.Message;
31 import android.os.MessageQueue;
32 import android.os.ParcelFileDescriptor;
33 import android.os.RemoteCallbackList;
34 import android.os.RemoteException;
35 import android.provider.Settings;
36 import android.speech.tts.TextToSpeech.Engine;
37 import android.text.TextUtils;
38 import android.util.Log;
39 
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.util.ArrayList;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.MissingResourceException;
48 import java.util.Set;
49 
50 
51 /**
52  * Abstract base class for TTS engine implementations. The following methods
53  * need to be implemented:
54  * <ul>
55  * <li>{@link #onIsLanguageAvailable}</li>
56  * <li>{@link #onLoadLanguage}</li>
57  * <li>{@link #onGetLanguage}</li>
58  * <li>{@link #onSynthesizeText}</li>
59  * <li>{@link #onStop}</li>
60  * </ul>
61  * The first three deal primarily with language management, and are used to
62  * query the engine for it's support for a given language and indicate to it
63  * that requests in a given language are imminent.
64  *
65  * {@link #onSynthesizeText} is central to the engine implementation. The
66  * implementation should synthesize text as per the request parameters and
67  * return synthesized data via the supplied callback. This class and its helpers
68  * will then consume that data, which might mean queuing it for playback or writing
69  * it to a file or similar. All calls to this method will be on a single thread,
70  * which will be different from the main thread of the service. Synthesis must be
71  * synchronous which means the engine must NOT hold on to the callback or call any
72  * methods on it after the method returns.
73  *
74  * {@link #onStop} tells the engine that it should stop
75  * all ongoing synthesis, if any. Any pending data from the current synthesis
76  * will be discarded.
77  *
78  * {@link #onGetLanguage} is not required as of JELLYBEAN_MR2 (API 18) and later, it is only
79  * called on earlier versions of Android.
80  *
81  * API Level 20 adds support for Voice objects. Voices are an abstraction that allow the TTS
82  * service to expose multiple backends for a single locale. Each one of them can have a different
83  * features set. In order to fully take advantage of voices, an engine should implement
84  * the following methods:
85  * <ul>
86  * <li>{@link #onGetVoices()}</li>
87  * <li>{@link #onIsValidVoiceName(String)}</li>
88  * <li>{@link #onLoadVoice(String)}</li>
89  * <li>{@link #onGetDefaultVoiceNameFor(String, String, String)}</li>
90  * </ul>
91  * The first three methods are siblings of the {@link #onGetLanguage},
92  * {@link #onIsLanguageAvailable} and {@link #onLoadLanguage} methods. The last one,
93  * {@link #onGetDefaultVoiceNameFor(String, String, String)} is a link between locale and voice
94  * based methods. Since API level 21 {@link TextToSpeech#setLanguage} is implemented by
95  * calling {@link TextToSpeech#setVoice} with the voice returned by
96  * {@link #onGetDefaultVoiceNameFor(String, String, String)}.
97  *
98  * If the client uses a voice instead of a locale, {@link SynthesisRequest} will contain the
99  * requested voice name.
100  *
101  * The default implementations of Voice-related methods implement them using the
102  * pre-existing locale-based implementation.
103  */
104 public abstract class TextToSpeechService extends Service {
105 
106     private static final boolean DBG = false;
107     private static final String TAG = "TextToSpeechService";
108 
109     private static final String SYNTH_THREAD_NAME = "SynthThread";
110 
111     private SynthHandler mSynthHandler;
112     // A thread and it's associated handler for playing back any audio
113     // associated with this TTS engine. Will handle all requests except synthesis
114     // to file requests, which occur on the synthesis thread.
115     @NonNull private AudioPlaybackHandler mAudioPlaybackHandler;
116     private TtsEngines mEngineHelper;
117 
118     private CallbackMap mCallbacks;
119     private String mPackageName;
120 
121     private final Object mVoicesInfoLock = new Object();
122 
123     @Override
onCreate()124     public void onCreate() {
125         if (DBG) Log.d(TAG, "onCreate()");
126         super.onCreate();
127 
128         SynthThread synthThread = new SynthThread();
129         synthThread.start();
130         mSynthHandler = new SynthHandler(synthThread.getLooper());
131 
132         mAudioPlaybackHandler = new AudioPlaybackHandler();
133         mAudioPlaybackHandler.start();
134 
135         mEngineHelper = new TtsEngines(this);
136 
137         mCallbacks = new CallbackMap();
138 
139         mPackageName = getApplicationInfo().packageName;
140 
141         String[] defaultLocale = getSettingsLocale();
142 
143         // Load default language
144         onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]);
145     }
146 
147     @Override
onDestroy()148     public void onDestroy() {
149         if (DBG) Log.d(TAG, "onDestroy()");
150 
151         // Tell the synthesizer to stop
152         mSynthHandler.quit();
153         // Tell the audio playback thread to stop.
154         mAudioPlaybackHandler.quit();
155         // Unregister all callbacks.
156         mCallbacks.kill();
157 
158         super.onDestroy();
159     }
160 
161     /**
162      * Checks whether the engine supports a given language.
163      *
164      * Can be called on multiple threads.
165      *
166      * Its return values HAVE to be consistent with onLoadLanguage.
167      *
168      * @param lang ISO-3 language code.
169      * @param country ISO-3 country code. May be empty or null.
170      * @param variant Language variant. May be empty or null.
171      * @return Code indicating the support status for the locale.
172      *         One of {@link TextToSpeech#LANG_AVAILABLE},
173      *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
174      *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
175      *         {@link TextToSpeech#LANG_MISSING_DATA}
176      *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
177      */
onIsLanguageAvailable(String lang, String country, String variant)178     protected abstract int onIsLanguageAvailable(String lang, String country, String variant);
179 
180     /**
181      * Returns the language, country and variant currently being used by the TTS engine.
182      *
183      * This method will be called only on Android 4.2 and before (API <= 17). In later versions
184      * this method is not called by the Android TTS framework.
185      *
186      * Can be called on multiple threads.
187      *
188      * @return A 3-element array, containing language (ISO 3-letter code),
189      *         country (ISO 3-letter code) and variant used by the engine.
190      *         The country and variant may be {@code ""}. If country is empty, then variant must
191      *         be empty too.
192      * @see Locale#getISO3Language()
193      * @see Locale#getISO3Country()
194      * @see Locale#getVariant()
195      */
onGetLanguage()196     protected abstract String[] onGetLanguage();
197 
198     /**
199      * Notifies the engine that it should load a speech synthesis language. There is no guarantee
200      * that this method is always called before the language is used for synthesis. It is merely
201      * a hint to the engine that it will probably get some synthesis requests for this language
202      * at some point in the future.
203      *
204      * Can be called on multiple threads.
205      * In <= Android 4.2 (<= API 17) can be called on main and service binder threads.
206      * In > Android 4.2 (> API 17) can be called on main and synthesis threads.
207      *
208      * @param lang ISO-3 language code.
209      * @param country ISO-3 country code. May be empty or null.
210      * @param variant Language variant. May be empty or null.
211      * @return Code indicating the support status for the locale.
212      *         One of {@link TextToSpeech#LANG_AVAILABLE},
213      *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
214      *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
215      *         {@link TextToSpeech#LANG_MISSING_DATA}
216      *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
217      */
onLoadLanguage(String lang, String country, String variant)218     protected abstract int onLoadLanguage(String lang, String country, String variant);
219 
220     /**
221      * Notifies the service that it should stop any in-progress speech synthesis.
222      * This method can be called even if no speech synthesis is currently in progress.
223      *
224      * Can be called on multiple threads, but not on the synthesis thread.
225      */
onStop()226     protected abstract void onStop();
227 
228     /**
229      * Tells the service to synthesize speech from the given text. This method
230      * should block until the synthesis is finished. Used for requests from V1
231      * clients ({@link android.speech.tts.TextToSpeech}). Called on the synthesis
232      * thread.
233      *
234      * @param request The synthesis request.
235      * @param callback The callback that the engine must use to make data
236      *            available for playback or for writing to a file.
237      */
onSynthesizeText(SynthesisRequest request, SynthesisCallback callback)238     protected abstract void onSynthesizeText(SynthesisRequest request,
239             SynthesisCallback callback);
240 
241     /**
242      * Queries the service for a set of features supported for a given language.
243      *
244      * Can be called on multiple threads.
245      *
246      * @param lang ISO-3 language code.
247      * @param country ISO-3 country code. May be empty or null.
248      * @param variant Language variant. May be empty or null.
249      * @return A list of features supported for the given language.
250      */
onGetFeaturesForLanguage(String lang, String country, String variant)251     protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) {
252         return new HashSet<String>();
253     }
254 
getExpectedLanguageAvailableStatus(Locale locale)255     private int getExpectedLanguageAvailableStatus(Locale locale) {
256         int expectedStatus = TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE;
257         if (locale.getVariant().isEmpty()) {
258             if (locale.getCountry().isEmpty()) {
259                 expectedStatus = TextToSpeech.LANG_AVAILABLE;
260             } else {
261                 expectedStatus = TextToSpeech.LANG_COUNTRY_AVAILABLE;
262             }
263         }
264         return expectedStatus;
265     }
266 
267     /**
268      * Queries the service for a set of supported voices.
269      *
270      * Can be called on multiple threads.
271      *
272      * The default implementation tries to enumerate all available locales, pass them to
273      * {@link #onIsLanguageAvailable(String, String, String)} and create Voice instances (using
274      * the locale's BCP-47 language tag as the voice name) for the ones that are supported.
275      * Note, that this implementation is suitable only for engines that don't have multiple voices
276      * for a single locale. Also, this implementation won't work with Locales not listed in the
277      * set returned by the {@link Locale#getAvailableLocales()} method.
278      *
279      * @return A list of voices supported.
280      */
onGetVoices()281     public List<Voice> onGetVoices() {
282         // Enumerate all locales and check if they are available
283         ArrayList<Voice> voices = new ArrayList<Voice>();
284         for (Locale locale : Locale.getAvailableLocales()) {
285             int expectedStatus = getExpectedLanguageAvailableStatus(locale);
286             try {
287                 int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
288                         locale.getISO3Country(), locale.getVariant());
289                 if (localeStatus != expectedStatus) {
290                     continue;
291                 }
292             } catch (MissingResourceException e) {
293                 // Ignore locale without iso 3 codes
294                 continue;
295             }
296             Set<String> features = onGetFeaturesForLanguage(locale.getISO3Language(),
297                     locale.getISO3Country(), locale.getVariant());
298             String voiceName = onGetDefaultVoiceNameFor(locale.getISO3Language(),
299                     locale.getISO3Country(), locale.getVariant());
300             voices.add(new Voice(voiceName, locale, Voice.QUALITY_NORMAL,
301                     Voice.LATENCY_NORMAL, false, features));
302         }
303         return voices;
304     }
305 
306     /**
307      * Return a name of the default voice for a given locale.
308      *
309      * This method provides a mapping between locales and available voices. This method is
310      * used in {@link TextToSpeech#setLanguage}, which calls this method and then calls
311      * {@link TextToSpeech#setVoice} with the voice returned by this method.
312      *
313      * Also, it's used by {@link TextToSpeech#getDefaultVoice()} to find a default voice for
314      * the default locale.
315      *
316      * @param lang ISO-3 language code.
317      * @param country ISO-3 country code. May be empty or null.
318      * @param variant Language variant. May be empty or null.
319 
320      * @return A name of the default voice for a given locale.
321      */
onGetDefaultVoiceNameFor(String lang, String country, String variant)322     public String onGetDefaultVoiceNameFor(String lang, String country, String variant) {
323         int localeStatus = onIsLanguageAvailable(lang, country, variant);
324         Locale iso3Locale = null;
325         switch (localeStatus) {
326             case TextToSpeech.LANG_AVAILABLE:
327                 iso3Locale = new Locale(lang);
328                 break;
329             case TextToSpeech.LANG_COUNTRY_AVAILABLE:
330                 iso3Locale = new Locale(lang, country);
331                 break;
332             case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE:
333                 iso3Locale = new Locale(lang, country, variant);
334                 break;
335             default:
336                 return null;
337         }
338         Locale properLocale = TtsEngines.normalizeTTSLocale(iso3Locale);
339         String voiceName = properLocale.toLanguageTag();
340         if (onIsValidVoiceName(voiceName) == TextToSpeech.SUCCESS) {
341             return voiceName;
342         } else {
343             return null;
344         }
345     }
346 
347     /**
348      * Notifies the engine that it should load a speech synthesis voice. There is no guarantee
349      * that this method is always called before the voice is used for synthesis. It is merely
350      * a hint to the engine that it will probably get some synthesis requests for this voice
351      * at some point in the future.
352      *
353      * Will be called only on synthesis thread.
354      *
355      * The default implementation creates a Locale from the voice name (by interpreting the name as
356      * a BCP-47 tag for the locale), and passes it to
357      * {@link #onLoadLanguage(String, String, String)}.
358      *
359      * @param voiceName Name of the voice.
360      * @return {@link TextToSpeech#ERROR} or {@link TextToSpeech#SUCCESS}.
361      */
onLoadVoice(String voiceName)362     public int onLoadVoice(String voiceName) {
363         Locale locale = Locale.forLanguageTag(voiceName);
364         if (locale == null) {
365             return TextToSpeech.ERROR;
366         }
367         int expectedStatus = getExpectedLanguageAvailableStatus(locale);
368         try {
369             int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
370                     locale.getISO3Country(), locale.getVariant());
371             if (localeStatus != expectedStatus) {
372                 return TextToSpeech.ERROR;
373             }
374             onLoadLanguage(locale.getISO3Language(),
375                     locale.getISO3Country(), locale.getVariant());
376             return TextToSpeech.SUCCESS;
377         } catch (MissingResourceException e) {
378             return TextToSpeech.ERROR;
379         }
380     }
381 
382     /**
383      * Checks whether the engine supports a voice with a given name.
384      *
385      * Can be called on multiple threads.
386      *
387      * The default implementation treats the voice name as a language tag, creating a Locale from
388      * the voice name, and passes it to {@link #onIsLanguageAvailable(String, String, String)}.
389      *
390      * @param voiceName Name of the voice.
391      * @return {@link TextToSpeech#ERROR} or {@link TextToSpeech#SUCCESS}.
392      */
onIsValidVoiceName(String voiceName)393     public int onIsValidVoiceName(String voiceName) {
394         Locale locale = Locale.forLanguageTag(voiceName);
395         if (locale == null) {
396             return TextToSpeech.ERROR;
397         }
398         int expectedStatus = getExpectedLanguageAvailableStatus(locale);
399         try {
400             int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
401                     locale.getISO3Country(), locale.getVariant());
402             if (localeStatus != expectedStatus) {
403                 return TextToSpeech.ERROR;
404             }
405             return TextToSpeech.SUCCESS;
406         } catch (MissingResourceException e) {
407             return TextToSpeech.ERROR;
408         }
409     }
410 
getDefaultSpeechRate()411     private int getDefaultSpeechRate() {
412         return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE);
413     }
414 
getSettingsLocale()415     private String[] getSettingsLocale() {
416         final Locale locale = mEngineHelper.getLocalePrefForEngine(mPackageName);
417         return TtsEngines.toOldLocaleStringFormat(locale);
418     }
419 
getSecureSettingInt(String name, int defaultValue)420     private int getSecureSettingInt(String name, int defaultValue) {
421         return Settings.Secure.getInt(getContentResolver(), name, defaultValue);
422     }
423 
424     /**
425      * Synthesizer thread. This thread is used to run {@link SynthHandler}.
426      */
427     private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler {
428 
429         private boolean mFirstIdle = true;
430 
SynthThread()431         public SynthThread() {
432             super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_DEFAULT);
433         }
434 
435         @Override
onLooperPrepared()436         protected void onLooperPrepared() {
437             getLooper().getQueue().addIdleHandler(this);
438         }
439 
440         @Override
queueIdle()441         public boolean queueIdle() {
442             if (mFirstIdle) {
443                 mFirstIdle = false;
444             } else {
445                 broadcastTtsQueueProcessingCompleted();
446             }
447             return true;
448         }
449 
broadcastTtsQueueProcessingCompleted()450         private void broadcastTtsQueueProcessingCompleted() {
451             Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED);
452             if (DBG) Log.d(TAG, "Broadcasting: " + i);
453             sendBroadcast(i);
454         }
455     }
456 
457     private class SynthHandler extends Handler {
458         private SpeechItem mCurrentSpeechItem = null;
459 
460         // When a message with QUEUE_FLUSH arrives we add the caller identity to the List and when a
461         // message with QUEUE_DESTROY arrives we increment mFlushAll. Then a message is added to the
462         // handler queue that removes the caller identify from the list and decrements the mFlushAll
463         // counter. This is so that when a message is processed and the caller identity is in the
464         // list or mFlushAll is not zero, we know that the message should be flushed.
465         // It's important that mFlushedObjects is a List and not a Set, and that mFlushAll is an
466         // int and not a bool. This is because when multiple messages arrive with QUEUE_FLUSH or
467         // QUEUE_DESTROY, we want to keep flushing messages until we arrive at the last QUEUE_FLUSH
468         // or QUEUE_DESTROY message.
469         private List<Object> mFlushedObjects = new ArrayList<>();
470         private int mFlushAll = 0;
471 
SynthHandler(Looper looper)472         public SynthHandler(Looper looper) {
473             super(looper);
474         }
475 
startFlushingSpeechItems(Object callerIdentity)476         private void startFlushingSpeechItems(Object callerIdentity) {
477             synchronized (mFlushedObjects) {
478                 if (callerIdentity == null) {
479                     mFlushAll += 1;
480                 } else {
481                     mFlushedObjects.add(callerIdentity);
482                 }
483             }
484         }
endFlushingSpeechItems(Object callerIdentity)485         private void endFlushingSpeechItems(Object callerIdentity) {
486             synchronized (mFlushedObjects) {
487                 if (callerIdentity == null) {
488                     mFlushAll -= 1;
489                 } else {
490                     mFlushedObjects.remove(callerIdentity);
491                 }
492             }
493         }
isFlushed(SpeechItem speechItem)494         private boolean isFlushed(SpeechItem speechItem) {
495             synchronized (mFlushedObjects) {
496                 return mFlushAll > 0 || mFlushedObjects.contains(speechItem.getCallerIdentity());
497             }
498         }
499 
getCurrentSpeechItem()500         private synchronized SpeechItem getCurrentSpeechItem() {
501             return mCurrentSpeechItem;
502         }
503 
setCurrentSpeechItem(SpeechItem speechItem)504         private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) {
505             SpeechItem old = mCurrentSpeechItem;
506             mCurrentSpeechItem = speechItem;
507             return old;
508         }
509 
maybeRemoveCurrentSpeechItem(Object callerIdentity)510         private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) {
511             if (mCurrentSpeechItem != null &&
512                     (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) {
513                 SpeechItem current = mCurrentSpeechItem;
514                 mCurrentSpeechItem = null;
515                 return current;
516             }
517 
518             return null;
519         }
520 
isSpeaking()521         public boolean isSpeaking() {
522             return getCurrentSpeechItem() != null;
523         }
524 
quit()525         public void quit() {
526             // Don't process any more speech items
527             getLooper().quit();
528             // Stop the current speech item
529             SpeechItem current = setCurrentSpeechItem(null);
530             if (current != null) {
531                 current.stop();
532             }
533             // The AudioPlaybackHandler will be destroyed by the caller.
534         }
535 
536         /**
537          * Adds a speech item to the queue.
538          *
539          * Called on a service binder thread.
540          */
enqueueSpeechItem(int queueMode, final SpeechItem speechItem)541         public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
542             UtteranceProgressDispatcher utterenceProgress = null;
543             if (speechItem instanceof UtteranceProgressDispatcher) {
544                 utterenceProgress = (UtteranceProgressDispatcher) speechItem;
545             }
546 
547             if (!speechItem.isValid()) {
548                 if (utterenceProgress != null) {
549                     utterenceProgress.dispatchOnError(
550                             TextToSpeech.ERROR_INVALID_REQUEST);
551                 }
552                 return TextToSpeech.ERROR;
553             }
554 
555             if (queueMode == TextToSpeech.QUEUE_FLUSH) {
556                 stopForApp(speechItem.getCallerIdentity());
557             } else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
558                 stopAll();
559             }
560             Runnable runnable = new Runnable() {
561                 @Override
562                 public void run() {
563                     if (isFlushed(speechItem)) {
564                         speechItem.stop();
565                     } else {
566                         setCurrentSpeechItem(speechItem);
567                         speechItem.play();
568                         setCurrentSpeechItem(null);
569                     }
570                 }
571             };
572             Message msg = Message.obtain(this, runnable);
573 
574             // The obj is used to remove all callbacks from the given app in
575             // stopForApp(String).
576             //
577             // Note that this string is interned, so the == comparison works.
578             msg.obj = speechItem.getCallerIdentity();
579 
580             if (sendMessage(msg)) {
581                 return TextToSpeech.SUCCESS;
582             } else {
583                 Log.w(TAG, "SynthThread has quit");
584                 if (utterenceProgress != null) {
585                     utterenceProgress.dispatchOnError(TextToSpeech.ERROR_SERVICE);
586                 }
587                 return TextToSpeech.ERROR;
588             }
589         }
590 
591         /**
592          * Stops all speech output and removes any utterances still in the queue for
593          * the calling app.
594          *
595          * Called on a service binder thread.
596          */
stopForApp(final Object callerIdentity)597         public int stopForApp(final Object callerIdentity) {
598             if (callerIdentity == null) {
599                 return TextToSpeech.ERROR;
600             }
601 
602             // Flush pending messages from callerIdentity
603             startFlushingSpeechItems(callerIdentity);
604 
605             // This stops writing data to the file / or publishing
606             // items to the audio playback handler.
607             //
608             // Note that the current speech item must be removed only if it
609             // belongs to the callingApp, else the item will be "orphaned" and
610             // not stopped correctly if a stop request comes along for the item
611             // from the app it belongs to.
612             SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity);
613             if (current != null) {
614                 current.stop();
615             }
616 
617             // Remove any enqueued audio too.
618             mAudioPlaybackHandler.stopForApp(callerIdentity);
619 
620             // Stop flushing pending messages
621             Runnable runnable = new Runnable() {
622                 @Override
623                 public void run() {
624                     endFlushingSpeechItems(callerIdentity);
625                 }
626             };
627             sendMessage(Message.obtain(this, runnable));
628             return TextToSpeech.SUCCESS;
629         }
630 
stopAll()631         public int stopAll() {
632             // Order to flush pending messages
633             startFlushingSpeechItems(null);
634 
635             // Stop the current speech item unconditionally .
636             SpeechItem current = setCurrentSpeechItem(null);
637             if (current != null) {
638                 current.stop();
639             }
640             // Remove all pending playback as well.
641             mAudioPlaybackHandler.stop();
642 
643             // Message to stop flushing pending messages
644             Runnable runnable = new Runnable() {
645                 @Override
646                 public void run() {
647                     endFlushingSpeechItems(null);
648                 }
649             };
650             sendMessage(Message.obtain(this, runnable));
651 
652 
653             return TextToSpeech.SUCCESS;
654         }
655     }
656 
657     interface UtteranceProgressDispatcher {
dispatchOnStop()658         public void dispatchOnStop();
dispatchOnSuccess()659         public void dispatchOnSuccess();
dispatchOnStart()660         public void dispatchOnStart();
dispatchOnError(int errorCode)661         public void dispatchOnError(int errorCode);
dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount)662         public void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount);
dispatchOnAudioAvailable(byte[] audio)663         public void dispatchOnAudioAvailable(byte[] audio);
664     }
665 
666     /** Set of parameters affecting audio output. */
667     static class AudioOutputParams {
668         /**
669          * Audio session identifier. May be used to associate audio playback with one of the
670          * {@link android.media.audiofx.AudioEffect} objects. If not specified by client,
671          * it should be equal to {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
672          */
673         public final int mSessionId;
674 
675         /**
676          * Volume, in the range [0.0f, 1.0f]. The default value is
677          * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f).
678          */
679         public final float mVolume;
680 
681         /**
682          * Left/right position of the audio, in the range [-1.0f, 1.0f].
683          * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f).
684          */
685         public final float mPan;
686 
687 
688         /**
689          * Audio attributes, set by {@link TextToSpeech#setAudioAttributes}
690          * or created from the value of {@link TextToSpeech.Engine#KEY_PARAM_STREAM}.
691          */
692         public final AudioAttributes mAudioAttributes;
693 
694         /** Create AudioOutputParams with default values */
AudioOutputParams()695         AudioOutputParams() {
696             mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
697             mVolume = Engine.DEFAULT_VOLUME;
698             mPan = Engine.DEFAULT_PAN;
699             mAudioAttributes = null;
700         }
701 
AudioOutputParams(int sessionId, float volume, float pan, AudioAttributes audioAttributes)702         AudioOutputParams(int sessionId, float volume, float pan,
703                 AudioAttributes audioAttributes) {
704             mSessionId = sessionId;
705             mVolume = volume;
706             mPan = pan;
707             mAudioAttributes = audioAttributes;
708         }
709 
710         /** Create AudioOutputParams from A {@link SynthesisRequest#getParams()} bundle */
createFromV1ParamsBundle(Bundle paramsBundle, boolean isSpeech)711         static AudioOutputParams createFromV1ParamsBundle(Bundle paramsBundle,
712                 boolean isSpeech) {
713             if (paramsBundle == null) {
714                 return new AudioOutputParams();
715             }
716 
717             AudioAttributes audioAttributes =
718                     (AudioAttributes) paramsBundle.getParcelable(
719                             Engine.KEY_PARAM_AUDIO_ATTRIBUTES);
720             if (audioAttributes == null) {
721                 int streamType = paramsBundle.getInt(
722                         Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM);
723                 audioAttributes = (new AudioAttributes.Builder())
724                         .setLegacyStreamType(streamType)
725                         .setContentType((isSpeech ?
726                                 AudioAttributes.CONTENT_TYPE_SPEECH :
727                                 AudioAttributes.CONTENT_TYPE_SONIFICATION))
728                         .build();
729             }
730 
731             return new AudioOutputParams(
732                     paramsBundle.getInt(
733                             Engine.KEY_PARAM_SESSION_ID,
734                             AudioManager.AUDIO_SESSION_ID_GENERATE),
735                     paramsBundle.getFloat(
736                             Engine.KEY_PARAM_VOLUME,
737                             Engine.DEFAULT_VOLUME),
738                     paramsBundle.getFloat(
739                             Engine.KEY_PARAM_PAN,
740                             Engine.DEFAULT_PAN),
741                     audioAttributes);
742         }
743     }
744 
745 
746     /**
747      * An item in the synth thread queue.
748      */
749     private abstract class SpeechItem {
750         private final Object mCallerIdentity;
751         private final int mCallerUid;
752         private final int mCallerPid;
753         private boolean mStarted = false;
754         private boolean mStopped = false;
755 
SpeechItem(Object caller, int callerUid, int callerPid)756         public SpeechItem(Object caller, int callerUid, int callerPid) {
757             mCallerIdentity = caller;
758             mCallerUid = callerUid;
759             mCallerPid = callerPid;
760         }
761 
getCallerIdentity()762         public Object getCallerIdentity() {
763             return mCallerIdentity;
764         }
765 
getCallerUid()766         public int getCallerUid() {
767             return mCallerUid;
768         }
769 
getCallerPid()770         public int getCallerPid() {
771             return mCallerPid;
772         }
773 
774         /**
775          * Checker whether the item is valid. If this method returns false, the item should not
776          * be played.
777          */
isValid()778         public abstract boolean isValid();
779 
780         /**
781          * Plays the speech item. Blocks until playback is finished.
782          * Must not be called more than once.
783          *
784          * Only called on the synthesis thread.
785          */
play()786         public void play() {
787             synchronized (this) {
788                 if (mStarted) {
789                     throw new IllegalStateException("play() called twice");
790                 }
791                 mStarted = true;
792             }
793             playImpl();
794         }
795 
playImpl()796         protected abstract void playImpl();
797 
798         /**
799          * Stops the speech item.
800          * Must not be called more than once.
801          *
802          * Can be called on multiple threads,  but not on the synthesis thread.
803          */
stop()804         public void stop() {
805             synchronized (this) {
806                 if (mStopped) {
807                     throw new IllegalStateException("stop() called twice");
808                 }
809                 mStopped = true;
810             }
811             stopImpl();
812         }
813 
stopImpl()814         protected abstract void stopImpl();
815 
isStopped()816         protected synchronized boolean isStopped() {
817              return mStopped;
818         }
819 
isStarted()820         protected synchronized boolean isStarted() {
821             return mStarted;
822        }
823     }
824 
825     /**
826      * An item in the synth thread queue that process utterance (and call back to client about
827      * progress).
828      */
829     private abstract class UtteranceSpeechItem extends SpeechItem
830         implements UtteranceProgressDispatcher  {
831 
UtteranceSpeechItem(Object caller, int callerUid, int callerPid)832         public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) {
833             super(caller, callerUid, callerPid);
834         }
835 
836         @Override
dispatchOnSuccess()837         public void dispatchOnSuccess() {
838             final String utteranceId = getUtteranceId();
839             if (utteranceId != null) {
840                 mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId);
841             }
842         }
843 
844         @Override
dispatchOnStop()845         public void dispatchOnStop() {
846             final String utteranceId = getUtteranceId();
847             if (utteranceId != null) {
848                 mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted());
849             }
850         }
851 
852         @Override
dispatchOnStart()853         public void dispatchOnStart() {
854             final String utteranceId = getUtteranceId();
855             if (utteranceId != null) {
856                 mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId);
857             }
858         }
859 
860         @Override
dispatchOnError(int errorCode)861         public void dispatchOnError(int errorCode) {
862             final String utteranceId = getUtteranceId();
863             if (utteranceId != null) {
864                 mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode);
865             }
866         }
867 
868         @Override
dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount)869         public void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount) {
870             final String utteranceId = getUtteranceId();
871             if (utteranceId != null) {
872                 mCallbacks.dispatchOnBeginSynthesis(getCallerIdentity(), utteranceId, sampleRateInHz, audioFormat, channelCount);
873             }
874         }
875 
876         @Override
dispatchOnAudioAvailable(byte[] audio)877         public void dispatchOnAudioAvailable(byte[] audio) {
878             final String utteranceId = getUtteranceId();
879             if (utteranceId != null) {
880                 mCallbacks.dispatchOnAudioAvailable(getCallerIdentity(), utteranceId, audio);
881             }
882         }
883 
getUtteranceId()884         abstract public String getUtteranceId();
885 
getStringParam(Bundle params, String key, String defaultValue)886         String getStringParam(Bundle params, String key, String defaultValue) {
887             return params == null ? defaultValue : params.getString(key, defaultValue);
888         }
889 
getIntParam(Bundle params, String key, int defaultValue)890         int getIntParam(Bundle params, String key, int defaultValue) {
891             return params == null ? defaultValue : params.getInt(key, defaultValue);
892         }
893 
getFloatParam(Bundle params, String key, float defaultValue)894         float getFloatParam(Bundle params, String key, float defaultValue) {
895             return params == null ? defaultValue : params.getFloat(key, defaultValue);
896         }
897     }
898 
899     /**
900      * UtteranceSpeechItem for V1 API speech items. V1 API speech items keep
901      * synthesis parameters in a single Bundle passed as parameter. This class
902      * allow subclasses to access them conveniently.
903      */
904     private abstract class SpeechItemV1 extends UtteranceSpeechItem {
905         protected final Bundle mParams;
906         protected final String mUtteranceId;
907 
SpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId)908         SpeechItemV1(Object callerIdentity, int callerUid, int callerPid,
909                 Bundle params, String utteranceId) {
910             super(callerIdentity, callerUid, callerPid);
911             mParams = params;
912             mUtteranceId = utteranceId;
913         }
914 
hasLanguage()915         boolean hasLanguage() {
916             return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null));
917         }
918 
getSpeechRate()919         int getSpeechRate() {
920             return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate());
921         }
922 
getPitch()923         int getPitch() {
924             return getIntParam(mParams, Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH);
925         }
926 
927         @Override
getUtteranceId()928         public String getUtteranceId() {
929             return mUtteranceId;
930         }
931 
getAudioParams()932         AudioOutputParams getAudioParams() {
933             return AudioOutputParams.createFromV1ParamsBundle(mParams, true);
934         }
935     }
936 
937     class SynthesisSpeechItemV1 extends SpeechItemV1 {
938         // Never null.
939         private final CharSequence mText;
940         private final SynthesisRequest mSynthesisRequest;
941         private final String[] mDefaultLocale;
942         // Non null after synthesis has started, and all accesses
943         // guarded by 'this'.
944         private AbstractSynthesisCallback mSynthesisCallback;
945         private final EventLoggerV1 mEventLogger;
946         private final int mCallerUid;
947 
SynthesisSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId, CharSequence text)948         public SynthesisSpeechItemV1(Object callerIdentity, int callerUid, int callerPid,
949                 Bundle params, String utteranceId, CharSequence text) {
950             super(callerIdentity, callerUid, callerPid, params, utteranceId);
951             mText = text;
952             mCallerUid = callerUid;
953             mSynthesisRequest = new SynthesisRequest(mText, mParams);
954             mDefaultLocale = getSettingsLocale();
955             setRequestParams(mSynthesisRequest);
956             mEventLogger = new EventLoggerV1(mSynthesisRequest, callerUid, callerPid,
957                     mPackageName);
958         }
959 
getText()960         public CharSequence getText() {
961             return mText;
962         }
963 
964         @Override
isValid()965         public boolean isValid() {
966             if (mText == null) {
967                 Log.e(TAG, "null synthesis text");
968                 return false;
969             }
970             if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) {
971                 Log.w(TAG, "Text too long: " + mText.length() + " chars");
972                 return false;
973             }
974             return true;
975         }
976 
977         @Override
playImpl()978         protected void playImpl() {
979             AbstractSynthesisCallback synthesisCallback;
980             mEventLogger.onRequestProcessingStart();
981             synchronized (this) {
982                 // stop() might have been called before we enter this
983                 // synchronized block.
984                 if (isStopped()) {
985                     return;
986                 }
987                 mSynthesisCallback = createSynthesisCallback();
988                 synthesisCallback = mSynthesisCallback;
989             }
990 
991             TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
992 
993             // Fix for case where client called .start() & .error(), but did not called .done()
994             if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) {
995                 synthesisCallback.done();
996             }
997         }
998 
createSynthesisCallback()999         protected AbstractSynthesisCallback createSynthesisCallback() {
1000             return new PlaybackSynthesisCallback(getAudioParams(),
1001                     mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false);
1002         }
1003 
setRequestParams(SynthesisRequest request)1004         private void setRequestParams(SynthesisRequest request) {
1005             String voiceName = getVoiceName();
1006             request.setLanguage(getLanguage(), getCountry(), getVariant());
1007             if (!TextUtils.isEmpty(voiceName)) {
1008                 request.setVoiceName(getVoiceName());
1009             }
1010             request.setSpeechRate(getSpeechRate());
1011             request.setCallerUid(mCallerUid);
1012             request.setPitch(getPitch());
1013         }
1014 
1015         @Override
stopImpl()1016         protected void stopImpl() {
1017             AbstractSynthesisCallback synthesisCallback;
1018             synchronized (this) {
1019                 synthesisCallback = mSynthesisCallback;
1020             }
1021             if (synthesisCallback != null) {
1022                 // If the synthesis callback is null, it implies that we haven't
1023                 // entered the synchronized(this) block in playImpl which in
1024                 // turn implies that synthesis would not have started.
1025                 synthesisCallback.stop();
1026                 TextToSpeechService.this.onStop();
1027             } else {
1028                 dispatchOnStop();
1029             }
1030         }
1031 
getCountry()1032         private String getCountry() {
1033             if (!hasLanguage()) return mDefaultLocale[1];
1034             return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, "");
1035         }
1036 
getVariant()1037         private String getVariant() {
1038             if (!hasLanguage()) return mDefaultLocale[2];
1039             return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, "");
1040         }
1041 
getLanguage()1042         public String getLanguage() {
1043             return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]);
1044         }
1045 
getVoiceName()1046         public String getVoiceName() {
1047             return getStringParam(mParams, Engine.KEY_PARAM_VOICE_NAME, "");
1048         }
1049     }
1050 
1051     private class SynthesisToFileOutputStreamSpeechItemV1 extends SynthesisSpeechItemV1 {
1052         private final FileOutputStream mFileOutputStream;
1053 
SynthesisToFileOutputStreamSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId, CharSequence text, FileOutputStream fileOutputStream)1054         public SynthesisToFileOutputStreamSpeechItemV1(Object callerIdentity, int callerUid,
1055                 int callerPid, Bundle params, String utteranceId, CharSequence text,
1056                 FileOutputStream fileOutputStream) {
1057             super(callerIdentity, callerUid, callerPid, params, utteranceId, text);
1058             mFileOutputStream = fileOutputStream;
1059         }
1060 
1061         @Override
createSynthesisCallback()1062         protected AbstractSynthesisCallback createSynthesisCallback() {
1063             return new FileSynthesisCallback(mFileOutputStream.getChannel(), this, false);
1064         }
1065 
1066         @Override
playImpl()1067         protected void playImpl() {
1068             dispatchOnStart();
1069             super.playImpl();
1070             try {
1071               mFileOutputStream.close();
1072             } catch(IOException e) {
1073               Log.w(TAG, "Failed to close output file", e);
1074             }
1075         }
1076     }
1077 
1078     private class AudioSpeechItemV1 extends SpeechItemV1 {
1079         private final AudioPlaybackQueueItem mItem;
1080 
AudioSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId, Uri uri)1081         public AudioSpeechItemV1(Object callerIdentity, int callerUid, int callerPid,
1082                 Bundle params, String utteranceId, Uri uri) {
1083             super(callerIdentity, callerUid, callerPid, params, utteranceId);
1084             mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(),
1085                     TextToSpeechService.this, uri, getAudioParams());
1086         }
1087 
1088         @Override
isValid()1089         public boolean isValid() {
1090             return true;
1091         }
1092 
1093         @Override
playImpl()1094         protected void playImpl() {
1095             mAudioPlaybackHandler.enqueue(mItem);
1096         }
1097 
1098         @Override
stopImpl()1099         protected void stopImpl() {
1100             // Do nothing.
1101         }
1102 
1103         @Override
getUtteranceId()1104         public String getUtteranceId() {
1105             return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null);
1106         }
1107 
1108         @Override
getAudioParams()1109         AudioOutputParams getAudioParams() {
1110             return AudioOutputParams.createFromV1ParamsBundle(mParams, false);
1111         }
1112     }
1113 
1114     private class SilenceSpeechItem extends UtteranceSpeechItem {
1115         private final long mDuration;
1116         private final String mUtteranceId;
1117 
SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, String utteranceId, long duration)1118         public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid,
1119                 String utteranceId, long duration) {
1120             super(callerIdentity, callerUid, callerPid);
1121             mUtteranceId = utteranceId;
1122             mDuration = duration;
1123         }
1124 
1125         @Override
isValid()1126         public boolean isValid() {
1127             return true;
1128         }
1129 
1130         @Override
playImpl()1131         protected void playImpl() {
1132             mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem(
1133                     this, getCallerIdentity(), mDuration));
1134         }
1135 
1136         @Override
stopImpl()1137         protected void stopImpl() {
1138 
1139         }
1140 
1141         @Override
getUtteranceId()1142         public String getUtteranceId() {
1143             return mUtteranceId;
1144         }
1145     }
1146 
1147     /**
1148      * Call {@link TextToSpeechService#onLoadLanguage} on synth thread.
1149      */
1150     private class LoadLanguageItem extends SpeechItem {
1151         private final String mLanguage;
1152         private final String mCountry;
1153         private final String mVariant;
1154 
LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, String language, String country, String variant)1155         public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid,
1156                 String language, String country, String variant) {
1157             super(callerIdentity, callerUid, callerPid);
1158             mLanguage = language;
1159             mCountry = country;
1160             mVariant = variant;
1161         }
1162 
1163         @Override
isValid()1164         public boolean isValid() {
1165             return true;
1166         }
1167 
1168         @Override
playImpl()1169         protected void playImpl() {
1170             TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant);
1171         }
1172 
1173         @Override
stopImpl()1174         protected void stopImpl() {
1175             // No-op
1176         }
1177     }
1178 
1179     /**
1180      * Call {@link TextToSpeechService#onLoadLanguage} on synth thread.
1181      */
1182     private class LoadVoiceItem extends SpeechItem {
1183         private final String mVoiceName;
1184 
LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid, String voiceName)1185         public LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid,
1186                 String voiceName) {
1187             super(callerIdentity, callerUid, callerPid);
1188             mVoiceName = voiceName;
1189         }
1190 
1191         @Override
isValid()1192         public boolean isValid() {
1193             return true;
1194         }
1195 
1196         @Override
playImpl()1197         protected void playImpl() {
1198             TextToSpeechService.this.onLoadVoice(mVoiceName);
1199         }
1200 
1201         @Override
stopImpl()1202         protected void stopImpl() {
1203             // No-op
1204         }
1205     }
1206 
1207 
1208     @Override
onBind(Intent intent)1209     public IBinder onBind(Intent intent) {
1210         if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
1211             return mBinder;
1212         }
1213         return null;
1214     }
1215 
1216     /**
1217      * Binder returned from {@code #onBind(Intent)}. The methods in this class can be
1218      * called called from several different threads.
1219      */
1220     // NOTE: All calls that are passed in a calling app are interned so that
1221     // they can be used as message objects (which are tested for equality using ==).
1222     private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() {
1223         @Override
1224         public int speak(IBinder caller, CharSequence text, int queueMode, Bundle params,
1225                 String utteranceId) {
1226             if (!checkNonNull(caller, text, params)) {
1227                 return TextToSpeech.ERROR;
1228             }
1229 
1230             SpeechItem item = new SynthesisSpeechItemV1(caller,
1231                     Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text);
1232             return mSynthHandler.enqueueSpeechItem(queueMode, item);
1233         }
1234 
1235         @Override
1236         public int synthesizeToFileDescriptor(IBinder caller, CharSequence text, ParcelFileDescriptor
1237                 fileDescriptor, Bundle params, String utteranceId) {
1238             if (!checkNonNull(caller, text, fileDescriptor, params)) {
1239                 return TextToSpeech.ERROR;
1240             }
1241 
1242             // In test env, ParcelFileDescriptor instance may be EXACTLY the same
1243             // one that is used by client. And it will be closed by a client, thus
1244             // preventing us from writing anything to it.
1245             final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd(
1246                     fileDescriptor.detachFd());
1247 
1248             SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV1(caller,
1249                     Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text,
1250                     new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor));
1251             return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
1252         }
1253 
1254         @Override
1255         public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params,
1256                 String utteranceId) {
1257             if (!checkNonNull(caller, audioUri, params)) {
1258                 return TextToSpeech.ERROR;
1259             }
1260 
1261             SpeechItem item = new AudioSpeechItemV1(caller,
1262                     Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, audioUri);
1263             return mSynthHandler.enqueueSpeechItem(queueMode, item);
1264         }
1265 
1266         @Override
1267         public int playSilence(IBinder caller, long duration, int queueMode, String utteranceId) {
1268             if (!checkNonNull(caller)) {
1269                 return TextToSpeech.ERROR;
1270             }
1271 
1272             SpeechItem item = new SilenceSpeechItem(caller,
1273                     Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, duration);
1274             return mSynthHandler.enqueueSpeechItem(queueMode, item);
1275         }
1276 
1277         @Override
1278         public boolean isSpeaking() {
1279             return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking();
1280         }
1281 
1282         @Override
1283         public int stop(IBinder caller) {
1284             if (!checkNonNull(caller)) {
1285                 return TextToSpeech.ERROR;
1286             }
1287 
1288             return mSynthHandler.stopForApp(caller);
1289         }
1290 
1291         @Override
1292         public String[] getLanguage() {
1293             return onGetLanguage();
1294         }
1295 
1296         @Override
1297         public String[] getClientDefaultLanguage() {
1298             return getSettingsLocale();
1299         }
1300 
1301         /*
1302          * If defaults are enforced, then no language is "available" except
1303          * perhaps the default language selected by the user.
1304          */
1305         @Override
1306         public int isLanguageAvailable(String lang, String country, String variant) {
1307             if (!checkNonNull(lang)) {
1308                 return TextToSpeech.ERROR;
1309             }
1310 
1311             return onIsLanguageAvailable(lang, country, variant);
1312         }
1313 
1314         @Override
1315         public String[] getFeaturesForLanguage(String lang, String country, String variant) {
1316             Set<String> features = onGetFeaturesForLanguage(lang, country, variant);
1317             String[] featuresArray = null;
1318             if (features != null) {
1319                 featuresArray = new String[features.size()];
1320                 features.toArray(featuresArray);
1321             } else {
1322                 featuresArray = new String[0];
1323             }
1324             return featuresArray;
1325         }
1326 
1327         /*
1328          * There is no point loading a non default language if defaults
1329          * are enforced.
1330          */
1331         @Override
1332         public int loadLanguage(IBinder caller, String lang, String country, String variant) {
1333             if (!checkNonNull(lang)) {
1334                 return TextToSpeech.ERROR;
1335             }
1336             int retVal = onIsLanguageAvailable(lang, country, variant);
1337 
1338             if (retVal == TextToSpeech.LANG_AVAILABLE ||
1339                     retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
1340                     retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
1341 
1342                 SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(),
1343                         Binder.getCallingPid(), lang, country, variant);
1344 
1345                 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) !=
1346                         TextToSpeech.SUCCESS) {
1347                     return TextToSpeech.ERROR;
1348                 }
1349             }
1350             return retVal;
1351         }
1352 
1353         @Override
1354         public List<Voice> getVoices() {
1355             return onGetVoices();
1356         }
1357 
1358         @Override
1359         public int loadVoice(IBinder caller, String voiceName) {
1360             if (!checkNonNull(voiceName)) {
1361                 return TextToSpeech.ERROR;
1362             }
1363             int retVal = onIsValidVoiceName(voiceName);
1364 
1365             if (retVal == TextToSpeech.SUCCESS) {
1366                 SpeechItem item = new LoadVoiceItem(caller, Binder.getCallingUid(),
1367                         Binder.getCallingPid(), voiceName);
1368                 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) !=
1369                         TextToSpeech.SUCCESS) {
1370                     return TextToSpeech.ERROR;
1371                 }
1372             }
1373             return retVal;
1374         }
1375 
1376         public String getDefaultVoiceNameFor(String lang, String country, String variant) {
1377             if (!checkNonNull(lang)) {
1378                 return null;
1379             }
1380             int retVal = onIsLanguageAvailable(lang, country, variant);
1381 
1382             if (retVal == TextToSpeech.LANG_AVAILABLE ||
1383                     retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
1384                     retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
1385                 return onGetDefaultVoiceNameFor(lang, country, variant);
1386             } else {
1387                 return null;
1388             }
1389         }
1390 
1391         @Override
1392         public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
1393             // Note that passing in a null callback is a valid use case.
1394             if (!checkNonNull(caller)) {
1395                 return;
1396             }
1397 
1398             mCallbacks.setCallback(caller, cb);
1399         }
1400 
1401         private String intern(String in) {
1402             // The input parameter will be non null.
1403             return in.intern();
1404         }
1405 
1406         private boolean checkNonNull(Object... args) {
1407             for (Object o : args) {
1408                 if (o == null) return false;
1409             }
1410             return true;
1411         }
1412     };
1413 
1414     private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
1415         private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback
1416                 = new HashMap<IBinder, ITextToSpeechCallback>();
1417 
setCallback(IBinder caller, ITextToSpeechCallback cb)1418         public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
1419             synchronized (mCallerToCallback) {
1420                 ITextToSpeechCallback old;
1421                 if (cb != null) {
1422                     register(cb, caller);
1423                     old = mCallerToCallback.put(caller, cb);
1424                 } else {
1425                     old = mCallerToCallback.remove(caller);
1426                 }
1427                 if (old != null && old != cb) {
1428                     unregister(old);
1429                 }
1430             }
1431         }
1432 
dispatchOnStop(Object callerIdentity, String utteranceId, boolean started)1433         public void dispatchOnStop(Object callerIdentity, String utteranceId, boolean started) {
1434             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1435             if (cb == null) return;
1436             try {
1437                 cb.onStop(utteranceId, started);
1438             } catch (RemoteException e) {
1439                 Log.e(TAG, "Callback onStop failed: " + e);
1440             }
1441         }
1442 
dispatchOnSuccess(Object callerIdentity, String utteranceId)1443         public void dispatchOnSuccess(Object callerIdentity, String utteranceId) {
1444             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1445             if (cb == null) return;
1446             try {
1447                 cb.onSuccess(utteranceId);
1448             } catch (RemoteException e) {
1449                 Log.e(TAG, "Callback onDone failed: " + e);
1450             }
1451         }
1452 
dispatchOnStart(Object callerIdentity, String utteranceId)1453         public void dispatchOnStart(Object callerIdentity, String utteranceId) {
1454             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1455             if (cb == null) return;
1456             try {
1457                 cb.onStart(utteranceId);
1458             } catch (RemoteException e) {
1459                 Log.e(TAG, "Callback onStart failed: " + e);
1460             }
1461         }
1462 
dispatchOnError(Object callerIdentity, String utteranceId, int errorCode)1463         public void dispatchOnError(Object callerIdentity, String utteranceId,
1464                 int errorCode) {
1465             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1466             if (cb == null) return;
1467             try {
1468                 cb.onError(utteranceId, errorCode);
1469             } catch (RemoteException e) {
1470                 Log.e(TAG, "Callback onError failed: " + e);
1471             }
1472         }
1473 
dispatchOnBeginSynthesis(Object callerIdentity, String utteranceId, int sampleRateInHz, int audioFormat, int channelCount)1474         public void dispatchOnBeginSynthesis(Object callerIdentity, String utteranceId, int sampleRateInHz, int audioFormat, int channelCount) {
1475             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1476             if (cb == null) return;
1477             try {
1478                 cb.onBeginSynthesis(utteranceId, sampleRateInHz, audioFormat, channelCount);
1479             } catch (RemoteException e) {
1480                 Log.e(TAG, "Callback dispatchOnBeginSynthesis(String, int, int, int) failed: " + e);
1481             }
1482         }
1483 
dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer)1484         public void dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer) {
1485             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1486             if (cb == null) return;
1487             try {
1488                 cb.onAudioAvailable(utteranceId, buffer);
1489             } catch (RemoteException e) {
1490                 Log.e(TAG, "Callback dispatchOnAudioAvailable(String, byte[]) failed: " + e);
1491             }
1492         }
1493 
1494         @Override
onCallbackDied(ITextToSpeechCallback callback, Object cookie)1495         public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
1496             IBinder caller = (IBinder) cookie;
1497             synchronized (mCallerToCallback) {
1498                 mCallerToCallback.remove(caller);
1499             }
1500             //mSynthHandler.stopForApp(caller);
1501         }
1502 
1503         @Override
kill()1504         public void kill() {
1505             synchronized (mCallerToCallback) {
1506                 mCallerToCallback.clear();
1507                 super.kill();
1508             }
1509         }
1510 
getCallbackFor(Object caller)1511         private ITextToSpeechCallback getCallbackFor(Object caller) {
1512             ITextToSpeechCallback cb;
1513             IBinder asBinder = (IBinder) caller;
1514             synchronized (mCallerToCallback) {
1515                 cb = mCallerToCallback.get(asBinder);
1516             }
1517 
1518             return cb;
1519         }
1520     }
1521 }
1522