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