• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 package android.speech.tts;
17 
18 import android.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 SpeechItem setCurrentSpeechItem(SpeechItem speechItem) {
506             SpeechItem old = mCurrentSpeechItem;
507             mCurrentSpeechItem = speechItem;
508             return old;
509         }
510 
maybeRemoveCurrentSpeechItem(Object callerIdentity)511         private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) {
512             if (mCurrentSpeechItem != null &&
513                     (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) {
514                 SpeechItem current = mCurrentSpeechItem;
515                 mCurrentSpeechItem = null;
516                 return current;
517             }
518 
519             return null;
520         }
521 
isSpeaking()522         public boolean isSpeaking() {
523             return getCurrentSpeechItem() != null;
524         }
525 
quit()526         public void quit() {
527             // Don't process any more speech items
528             getLooper().quit();
529             // Stop the current speech item
530             SpeechItem current = setCurrentSpeechItem(null);
531             if (current != null) {
532                 current.stop();
533             }
534             // The AudioPlaybackHandler will be destroyed by the caller.
535         }
536 
537         /**
538          * Adds a speech item to the queue.
539          *
540          * Called on a service binder thread.
541          */
enqueueSpeechItem(int queueMode, final SpeechItem speechItem)542         public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
543             UtteranceProgressDispatcher utterenceProgress = null;
544             if (speechItem instanceof UtteranceProgressDispatcher) {
545                 utterenceProgress = (UtteranceProgressDispatcher) speechItem;
546             }
547 
548             if (!speechItem.isValid()) {
549                 if (utterenceProgress != null) {
550                     utterenceProgress.dispatchOnError(
551                             TextToSpeech.ERROR_INVALID_REQUEST);
552                 }
553                 return TextToSpeech.ERROR;
554             }
555 
556             if (queueMode == TextToSpeech.QUEUE_FLUSH) {
557                 stopForApp(speechItem.getCallerIdentity());
558             } else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
559                 stopAll();
560             }
561             Runnable runnable = new Runnable() {
562                 @Override
563                 public void run() {
564                     if (isFlushed(speechItem)) {
565                         speechItem.stop();
566                     } else {
567                         setCurrentSpeechItem(speechItem);
568                         speechItem.play();
569                         setCurrentSpeechItem(null);
570                     }
571                 }
572             };
573             Message msg = Message.obtain(this, runnable);
574 
575             // The obj is used to remove all callbacks from the given app in
576             // stopForApp(String).
577             //
578             // Note that this string is interned, so the == comparison works.
579             msg.obj = speechItem.getCallerIdentity();
580 
581             if (sendMessage(msg)) {
582                 return TextToSpeech.SUCCESS;
583             } else {
584                 Log.w(TAG, "SynthThread has quit");
585                 if (utterenceProgress != null) {
586                     utterenceProgress.dispatchOnError(TextToSpeech.ERROR_SERVICE);
587                 }
588                 return TextToSpeech.ERROR;
589             }
590         }
591 
592         /**
593          * Stops all speech output and removes any utterances still in the queue for
594          * the calling app.
595          *
596          * Called on a service binder thread.
597          */
stopForApp(final Object callerIdentity)598         public int stopForApp(final Object callerIdentity) {
599             if (callerIdentity == null) {
600                 return TextToSpeech.ERROR;
601             }
602 
603             // Flush pending messages from callerIdentity
604             startFlushingSpeechItems(callerIdentity);
605 
606             // This stops writing data to the file / or publishing
607             // items to the audio playback handler.
608             //
609             // Note that the current speech item must be removed only if it
610             // belongs to the callingApp, else the item will be "orphaned" and
611             // not stopped correctly if a stop request comes along for the item
612             // from the app it belongs to.
613             SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity);
614             if (current != null) {
615                 current.stop();
616             }
617 
618             // Remove any enqueued audio too.
619             mAudioPlaybackHandler.stopForApp(callerIdentity);
620 
621             // Stop flushing pending messages
622             Runnable runnable = new Runnable() {
623                 @Override
624                 public void run() {
625                     endFlushingSpeechItems(callerIdentity);
626                 }
627             };
628             sendMessage(Message.obtain(this, runnable));
629             return TextToSpeech.SUCCESS;
630         }
631 
stopAll()632         public int stopAll() {
633             // Order to flush pending messages
634             startFlushingSpeechItems(null);
635 
636             // Stop the current speech item unconditionally .
637             SpeechItem current = setCurrentSpeechItem(null);
638             if (current != null) {
639                 current.stop();
640             }
641             // Remove all pending playback as well.
642             mAudioPlaybackHandler.stop();
643 
644             // Message to stop flushing pending messages
645             Runnable runnable = new Runnable() {
646                 @Override
647                 public void run() {
648                     endFlushingSpeechItems(null);
649                 }
650             };
651             sendMessage(Message.obtain(this, runnable));
652 
653 
654             return TextToSpeech.SUCCESS;
655         }
656     }
657 
658     interface UtteranceProgressDispatcher {
dispatchOnStop()659         void dispatchOnStop();
660 
dispatchOnSuccess()661         void dispatchOnSuccess();
662 
dispatchOnStart()663         void dispatchOnStart();
664 
dispatchOnError(int errorCode)665         void dispatchOnError(int errorCode);
666 
dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount)667         void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount);
668 
dispatchOnAudioAvailable(byte[] audio)669         void dispatchOnAudioAvailable(byte[] audio);
670 
dispatchOnRangeStart(int start, int end, int frame)671         public void dispatchOnRangeStart(int start, int end, int frame);
672     }
673 
674     /** Set of parameters affecting audio output. */
675     static class AudioOutputParams {
676         /**
677          * Audio session identifier. May be used to associate audio playback with one of the
678          * {@link android.media.audiofx.AudioEffect} objects. If not specified by client,
679          * it should be equal to {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
680          */
681         public final int mSessionId;
682 
683         /**
684          * Volume, in the range [0.0f, 1.0f]. The default value is
685          * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f).
686          */
687         public final float mVolume;
688 
689         /**
690          * Left/right position of the audio, in the range [-1.0f, 1.0f].
691          * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f).
692          */
693         public final float mPan;
694 
695 
696         /**
697          * Audio attributes, set by {@link TextToSpeech#setAudioAttributes}
698          * or created from the value of {@link TextToSpeech.Engine#KEY_PARAM_STREAM}.
699          */
700         public final AudioAttributes mAudioAttributes;
701 
702         /** Create AudioOutputParams with default values */
AudioOutputParams()703         AudioOutputParams() {
704             mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
705             mVolume = Engine.DEFAULT_VOLUME;
706             mPan = Engine.DEFAULT_PAN;
707             mAudioAttributes = null;
708         }
709 
AudioOutputParams(int sessionId, float volume, float pan, AudioAttributes audioAttributes)710         AudioOutputParams(int sessionId, float volume, float pan,
711                 AudioAttributes audioAttributes) {
712             mSessionId = sessionId;
713             mVolume = volume;
714             mPan = pan;
715             mAudioAttributes = audioAttributes;
716         }
717 
718         /** Create AudioOutputParams from A {@link SynthesisRequest#getParams()} bundle */
createFromParamsBundle(Bundle paramsBundle, boolean isSpeech)719         static AudioOutputParams createFromParamsBundle(Bundle paramsBundle, boolean isSpeech) {
720             if (paramsBundle == null) {
721                 return new AudioOutputParams();
722             }
723 
724             AudioAttributes audioAttributes =
725                     (AudioAttributes) paramsBundle.getParcelable(
726                             Engine.KEY_PARAM_AUDIO_ATTRIBUTES);
727             if (audioAttributes == null) {
728                 int streamType = paramsBundle.getInt(
729                         Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM);
730                 audioAttributes = (new AudioAttributes.Builder())
731                         .setLegacyStreamType(streamType)
732                         .setContentType((isSpeech ?
733                                 AudioAttributes.CONTENT_TYPE_SPEECH :
734                                 AudioAttributes.CONTENT_TYPE_SONIFICATION))
735                         .build();
736             }
737 
738             return new AudioOutputParams(
739                     paramsBundle.getInt(
740                             Engine.KEY_PARAM_SESSION_ID,
741                             AudioManager.AUDIO_SESSION_ID_GENERATE),
742                     paramsBundle.getFloat(
743                             Engine.KEY_PARAM_VOLUME,
744                             Engine.DEFAULT_VOLUME),
745                     paramsBundle.getFloat(
746                             Engine.KEY_PARAM_PAN,
747                             Engine.DEFAULT_PAN),
748                     audioAttributes);
749         }
750     }
751 
752 
753     /**
754      * An item in the synth thread queue.
755      */
756     private abstract class SpeechItem {
757         private final Object mCallerIdentity;
758         private final int mCallerUid;
759         private final int mCallerPid;
760         private boolean mStarted = false;
761         private boolean mStopped = false;
762 
SpeechItem(Object caller, int callerUid, int callerPid)763         public SpeechItem(Object caller, int callerUid, int callerPid) {
764             mCallerIdentity = caller;
765             mCallerUid = callerUid;
766             mCallerPid = callerPid;
767         }
768 
getCallerIdentity()769         public Object getCallerIdentity() {
770             return mCallerIdentity;
771         }
772 
getCallerUid()773         public int getCallerUid() {
774             return mCallerUid;
775         }
776 
getCallerPid()777         public int getCallerPid() {
778             return mCallerPid;
779         }
780 
781         /**
782          * Checker whether the item is valid. If this method returns false, the item should not
783          * be played.
784          */
isValid()785         public abstract boolean isValid();
786 
787         /**
788          * Plays the speech item. Blocks until playback is finished.
789          * Must not be called more than once.
790          *
791          * Only called on the synthesis thread.
792          */
play()793         public void play() {
794             synchronized (this) {
795                 if (mStarted) {
796                     throw new IllegalStateException("play() called twice");
797                 }
798                 mStarted = true;
799             }
800             playImpl();
801         }
802 
playImpl()803         protected abstract void playImpl();
804 
805         /**
806          * Stops the speech item.
807          * Must not be called more than once.
808          *
809          * Can be called on multiple threads,  but not on the synthesis thread.
810          */
stop()811         public void stop() {
812             synchronized (this) {
813                 if (mStopped) {
814                     throw new IllegalStateException("stop() called twice");
815                 }
816                 mStopped = true;
817             }
818             stopImpl();
819         }
820 
stopImpl()821         protected abstract void stopImpl();
822 
isStopped()823         protected synchronized boolean isStopped() {
824              return mStopped;
825         }
826 
isStarted()827         protected synchronized boolean isStarted() {
828             return mStarted;
829        }
830     }
831 
832     /**
833      * An item in the synth thread queue that process utterance (and call back to client about
834      * progress).
835      */
836     private abstract class UtteranceSpeechItem extends SpeechItem
837         implements UtteranceProgressDispatcher  {
838 
UtteranceSpeechItem(Object caller, int callerUid, int callerPid)839         public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) {
840             super(caller, callerUid, callerPid);
841         }
842 
843         @Override
dispatchOnSuccess()844         public void dispatchOnSuccess() {
845             final String utteranceId = getUtteranceId();
846             if (utteranceId != null) {
847                 mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId);
848             }
849         }
850 
851         @Override
dispatchOnStop()852         public void dispatchOnStop() {
853             final String utteranceId = getUtteranceId();
854             if (utteranceId != null) {
855                 mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted());
856             }
857         }
858 
859         @Override
dispatchOnStart()860         public void dispatchOnStart() {
861             final String utteranceId = getUtteranceId();
862             if (utteranceId != null) {
863                 mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId);
864             }
865         }
866 
867         @Override
dispatchOnError(int errorCode)868         public void dispatchOnError(int errorCode) {
869             final String utteranceId = getUtteranceId();
870             if (utteranceId != null) {
871                 mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode);
872             }
873         }
874 
875         @Override
dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount)876         public void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount) {
877             final String utteranceId = getUtteranceId();
878             if (utteranceId != null) {
879                 mCallbacks.dispatchOnBeginSynthesis(getCallerIdentity(), utteranceId, sampleRateInHz, audioFormat, channelCount);
880             }
881         }
882 
883         @Override
dispatchOnAudioAvailable(byte[] audio)884         public void dispatchOnAudioAvailable(byte[] audio) {
885             final String utteranceId = getUtteranceId();
886             if (utteranceId != null) {
887                 mCallbacks.dispatchOnAudioAvailable(getCallerIdentity(), utteranceId, audio);
888             }
889         }
890 
891         @Override
dispatchOnRangeStart(int start, int end, int frame)892         public void dispatchOnRangeStart(int start, int end, int frame) {
893             final String utteranceId = getUtteranceId();
894             if (utteranceId != null) {
895                 mCallbacks.dispatchOnRangeStart(
896                         getCallerIdentity(), utteranceId, start, end, frame);
897             }
898         }
899 
getUtteranceId()900         abstract public String getUtteranceId();
901 
getStringParam(Bundle params, String key, String defaultValue)902         String getStringParam(Bundle params, String key, String defaultValue) {
903             return params == null ? defaultValue : params.getString(key, defaultValue);
904         }
905 
getIntParam(Bundle params, String key, int defaultValue)906         int getIntParam(Bundle params, String key, int defaultValue) {
907             return params == null ? defaultValue : params.getInt(key, defaultValue);
908         }
909 
getFloatParam(Bundle params, String key, float defaultValue)910         float getFloatParam(Bundle params, String key, float defaultValue) {
911             return params == null ? defaultValue : params.getFloat(key, defaultValue);
912         }
913     }
914 
915     /**
916      * Synthesis parameters are kept in a single Bundle passed as parameter. This class allow
917      * subclasses to access them conveniently.
918      */
919     private abstract class UtteranceSpeechItemWithParams extends UtteranceSpeechItem {
920         protected final Bundle mParams;
921         protected final String mUtteranceId;
922 
UtteranceSpeechItemWithParams( Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId)923         UtteranceSpeechItemWithParams(
924                 Object callerIdentity,
925                 int callerUid,
926                 int callerPid,
927                 Bundle params,
928                 String utteranceId) {
929             super(callerIdentity, callerUid, callerPid);
930             mParams = params;
931             mUtteranceId = utteranceId;
932         }
933 
hasLanguage()934         boolean hasLanguage() {
935             return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null));
936         }
937 
getSpeechRate()938         int getSpeechRate() {
939             return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate());
940         }
941 
getPitch()942         int getPitch() {
943             return getIntParam(mParams, Engine.KEY_PARAM_PITCH, getDefaultPitch());
944         }
945 
946         @Override
getUtteranceId()947         public String getUtteranceId() {
948             return mUtteranceId;
949         }
950 
getAudioParams()951         AudioOutputParams getAudioParams() {
952             return AudioOutputParams.createFromParamsBundle(mParams, true);
953         }
954     }
955 
956     class SynthesisSpeechItem extends UtteranceSpeechItemWithParams {
957         // Never null.
958         private final CharSequence mText;
959         private final SynthesisRequest mSynthesisRequest;
960         private final String[] mDefaultLocale;
961         // Non null after synthesis has started, and all accesses
962         // guarded by 'this'.
963         private AbstractSynthesisCallback mSynthesisCallback;
964         private final EventLogger mEventLogger;
965         private final int mCallerUid;
966 
SynthesisSpeechItem( Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId, CharSequence text)967         public SynthesisSpeechItem(
968                 Object callerIdentity,
969                 int callerUid,
970                 int callerPid,
971                 Bundle params,
972                 String utteranceId,
973                 CharSequence text) {
974             super(callerIdentity, callerUid, callerPid, params, utteranceId);
975             mText = text;
976             mCallerUid = callerUid;
977             mSynthesisRequest = new SynthesisRequest(mText, mParams);
978             mDefaultLocale = getSettingsLocale();
979             setRequestParams(mSynthesisRequest);
980             mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid, mPackageName);
981         }
982 
getText()983         public CharSequence getText() {
984             return mText;
985         }
986 
987         @Override
isValid()988         public boolean isValid() {
989             if (mText == null) {
990                 Log.e(TAG, "null synthesis text");
991                 return false;
992             }
993             if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) {
994                 Log.w(TAG, "Text too long: " + mText.length() + " chars");
995                 return false;
996             }
997             return true;
998         }
999 
1000         @Override
playImpl()1001         protected void playImpl() {
1002             AbstractSynthesisCallback synthesisCallback;
1003             mEventLogger.onRequestProcessingStart();
1004             synchronized (this) {
1005                 // stop() might have been called before we enter this
1006                 // synchronized block.
1007                 if (isStopped()) {
1008                     return;
1009                 }
1010                 mSynthesisCallback = createSynthesisCallback();
1011                 synthesisCallback = mSynthesisCallback;
1012             }
1013 
1014             TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
1015 
1016             // Fix for case where client called .start() & .error(), but did not called .done()
1017             if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) {
1018                 synthesisCallback.done();
1019             }
1020         }
1021 
createSynthesisCallback()1022         protected AbstractSynthesisCallback createSynthesisCallback() {
1023             return new PlaybackSynthesisCallback(getAudioParams(),
1024                     mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false);
1025         }
1026 
setRequestParams(SynthesisRequest request)1027         private void setRequestParams(SynthesisRequest request) {
1028             String voiceName = getVoiceName();
1029             request.setLanguage(getLanguage(), getCountry(), getVariant());
1030             if (!TextUtils.isEmpty(voiceName)) {
1031                 request.setVoiceName(getVoiceName());
1032             }
1033             request.setSpeechRate(getSpeechRate());
1034             request.setCallerUid(mCallerUid);
1035             request.setPitch(getPitch());
1036         }
1037 
1038         @Override
stopImpl()1039         protected void stopImpl() {
1040             AbstractSynthesisCallback synthesisCallback;
1041             synchronized (this) {
1042                 synthesisCallback = mSynthesisCallback;
1043             }
1044             if (synthesisCallback != null) {
1045                 // If the synthesis callback is null, it implies that we haven't
1046                 // entered the synchronized(this) block in playImpl which in
1047                 // turn implies that synthesis would not have started.
1048                 synthesisCallback.stop();
1049                 TextToSpeechService.this.onStop();
1050             } else {
1051                 dispatchOnStop();
1052             }
1053         }
1054 
getCountry()1055         private String getCountry() {
1056             if (!hasLanguage()) return mDefaultLocale[1];
1057             return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, "");
1058         }
1059 
getVariant()1060         private String getVariant() {
1061             if (!hasLanguage()) return mDefaultLocale[2];
1062             return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, "");
1063         }
1064 
getLanguage()1065         public String getLanguage() {
1066             return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]);
1067         }
1068 
getVoiceName()1069         public String getVoiceName() {
1070             return getStringParam(mParams, Engine.KEY_PARAM_VOICE_NAME, "");
1071         }
1072     }
1073 
1074     private class SynthesisToFileOutputStreamSpeechItem extends SynthesisSpeechItem {
1075         private final FileOutputStream mFileOutputStream;
1076 
SynthesisToFileOutputStreamSpeechItem( Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId, CharSequence text, FileOutputStream fileOutputStream)1077         public SynthesisToFileOutputStreamSpeechItem(
1078                 Object callerIdentity,
1079                 int callerUid,
1080                 int callerPid,
1081                 Bundle params,
1082                 String utteranceId,
1083                 CharSequence text,
1084                 FileOutputStream fileOutputStream) {
1085             super(callerIdentity, callerUid, callerPid, params, utteranceId, text);
1086             mFileOutputStream = fileOutputStream;
1087         }
1088 
1089         @Override
createSynthesisCallback()1090         protected AbstractSynthesisCallback createSynthesisCallback() {
1091             return new FileSynthesisCallback(mFileOutputStream.getChannel(), this, false);
1092         }
1093 
1094         @Override
playImpl()1095         protected void playImpl() {
1096             dispatchOnStart();
1097             super.playImpl();
1098             try {
1099               mFileOutputStream.close();
1100             } catch(IOException e) {
1101               Log.w(TAG, "Failed to close output file", e);
1102             }
1103         }
1104     }
1105 
1106     private class AudioSpeechItem extends UtteranceSpeechItemWithParams {
1107         private final AudioPlaybackQueueItem mItem;
1108 
AudioSpeechItem( Object callerIdentity, int callerUid, int callerPid, Bundle params, String utteranceId, Uri uri)1109         public AudioSpeechItem(
1110                 Object callerIdentity,
1111                 int callerUid,
1112                 int callerPid,
1113                 Bundle params,
1114                 String utteranceId,
1115                 Uri uri) {
1116             super(callerIdentity, callerUid, callerPid, params, utteranceId);
1117             mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(),
1118                     TextToSpeechService.this, uri, getAudioParams());
1119         }
1120 
1121         @Override
isValid()1122         public boolean isValid() {
1123             return true;
1124         }
1125 
1126         @Override
playImpl()1127         protected void playImpl() {
1128             mAudioPlaybackHandler.enqueue(mItem);
1129         }
1130 
1131         @Override
stopImpl()1132         protected void stopImpl() {
1133             // Do nothing.
1134         }
1135 
1136         @Override
getUtteranceId()1137         public String getUtteranceId() {
1138             return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null);
1139         }
1140 
1141         @Override
getAudioParams()1142         AudioOutputParams getAudioParams() {
1143             return AudioOutputParams.createFromParamsBundle(mParams, false);
1144         }
1145     }
1146 
1147     private class SilenceSpeechItem extends UtteranceSpeechItem {
1148         private final long mDuration;
1149         private final String mUtteranceId;
1150 
SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, String utteranceId, long duration)1151         public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid,
1152                 String utteranceId, long duration) {
1153             super(callerIdentity, callerUid, callerPid);
1154             mUtteranceId = utteranceId;
1155             mDuration = duration;
1156         }
1157 
1158         @Override
isValid()1159         public boolean isValid() {
1160             return true;
1161         }
1162 
1163         @Override
playImpl()1164         protected void playImpl() {
1165             mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem(
1166                     this, getCallerIdentity(), mDuration));
1167         }
1168 
1169         @Override
stopImpl()1170         protected void stopImpl() {
1171 
1172         }
1173 
1174         @Override
getUtteranceId()1175         public String getUtteranceId() {
1176             return mUtteranceId;
1177         }
1178     }
1179 
1180     /**
1181      * Call {@link TextToSpeechService#onLoadLanguage} on synth thread.
1182      */
1183     private class LoadLanguageItem extends SpeechItem {
1184         private final String mLanguage;
1185         private final String mCountry;
1186         private final String mVariant;
1187 
LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, String language, String country, String variant)1188         public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid,
1189                 String language, String country, String variant) {
1190             super(callerIdentity, callerUid, callerPid);
1191             mLanguage = language;
1192             mCountry = country;
1193             mVariant = variant;
1194         }
1195 
1196         @Override
isValid()1197         public boolean isValid() {
1198             return true;
1199         }
1200 
1201         @Override
playImpl()1202         protected void playImpl() {
1203             TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant);
1204         }
1205 
1206         @Override
stopImpl()1207         protected void stopImpl() {
1208             // No-op
1209         }
1210     }
1211 
1212     /**
1213      * Call {@link TextToSpeechService#onLoadLanguage} on synth thread.
1214      */
1215     private class LoadVoiceItem extends SpeechItem {
1216         private final String mVoiceName;
1217 
LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid, String voiceName)1218         public LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid,
1219                 String voiceName) {
1220             super(callerIdentity, callerUid, callerPid);
1221             mVoiceName = voiceName;
1222         }
1223 
1224         @Override
isValid()1225         public boolean isValid() {
1226             return true;
1227         }
1228 
1229         @Override
playImpl()1230         protected void playImpl() {
1231             TextToSpeechService.this.onLoadVoice(mVoiceName);
1232         }
1233 
1234         @Override
stopImpl()1235         protected void stopImpl() {
1236             // No-op
1237         }
1238     }
1239 
1240 
1241     @Override
onBind(Intent intent)1242     public IBinder onBind(Intent intent) {
1243         if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
1244             return mBinder;
1245         }
1246         return null;
1247     }
1248 
1249     /**
1250      * Binder returned from {@code #onBind(Intent)}. The methods in this class can be called called
1251      * from several different threads.
1252      */
1253     // NOTE: All calls that are passed in a calling app are interned so that
1254     // they can be used as message objects (which are tested for equality using ==).
1255     private final ITextToSpeechService.Stub mBinder =
1256             new ITextToSpeechService.Stub() {
1257                 @Override
1258                 public int speak(
1259                         IBinder caller,
1260                         CharSequence text,
1261                         int queueMode,
1262                         Bundle params,
1263                         String utteranceId) {
1264                     if (!checkNonNull(caller, text, params)) {
1265                         return TextToSpeech.ERROR;
1266                     }
1267 
1268                     SpeechItem item =
1269                             new SynthesisSpeechItem(
1270                                     caller,
1271                                     Binder.getCallingUid(),
1272                                     Binder.getCallingPid(),
1273                                     params,
1274                                     utteranceId,
1275                                     text);
1276                     return mSynthHandler.enqueueSpeechItem(queueMode, item);
1277                 }
1278 
1279                 @Override
1280                 public int synthesizeToFileDescriptor(
1281                         IBinder caller,
1282                         CharSequence text,
1283                         ParcelFileDescriptor fileDescriptor,
1284                         Bundle params,
1285                         String utteranceId) {
1286                     if (!checkNonNull(caller, text, fileDescriptor, params)) {
1287                         return TextToSpeech.ERROR;
1288                     }
1289 
1290                     // In test env, ParcelFileDescriptor instance may be EXACTLY the same
1291                     // one that is used by client. And it will be closed by a client, thus
1292                     // preventing us from writing anything to it.
1293                     final ParcelFileDescriptor sameFileDescriptor =
1294                             ParcelFileDescriptor.adoptFd(fileDescriptor.detachFd());
1295 
1296                     SpeechItem item =
1297                             new SynthesisToFileOutputStreamSpeechItem(
1298                                     caller,
1299                                     Binder.getCallingUid(),
1300                                     Binder.getCallingPid(),
1301                                     params,
1302                                     utteranceId,
1303                                     text,
1304                                     new ParcelFileDescriptor.AutoCloseOutputStream(
1305                                             sameFileDescriptor));
1306                     return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
1307                 }
1308 
1309                 @Override
1310                 public int playAudio(
1311                         IBinder caller,
1312                         Uri audioUri,
1313                         int queueMode,
1314                         Bundle params,
1315                         String utteranceId) {
1316                     if (!checkNonNull(caller, audioUri, params)) {
1317                         return TextToSpeech.ERROR;
1318                     }
1319 
1320                     SpeechItem item =
1321                             new AudioSpeechItem(
1322                                     caller,
1323                                     Binder.getCallingUid(),
1324                                     Binder.getCallingPid(),
1325                                     params,
1326                                     utteranceId,
1327                                     audioUri);
1328                     return mSynthHandler.enqueueSpeechItem(queueMode, item);
1329                 }
1330 
1331                 @Override
1332                 public int playSilence(
1333                         IBinder caller, long duration, int queueMode, String utteranceId) {
1334                     if (!checkNonNull(caller)) {
1335                         return TextToSpeech.ERROR;
1336                     }
1337 
1338                     SpeechItem item =
1339                             new SilenceSpeechItem(
1340                                     caller,
1341                                     Binder.getCallingUid(),
1342                                     Binder.getCallingPid(),
1343                                     utteranceId,
1344                                     duration);
1345                     return mSynthHandler.enqueueSpeechItem(queueMode, item);
1346                 }
1347 
1348                 @Override
1349                 public boolean isSpeaking() {
1350                     return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking();
1351                 }
1352 
1353                 @Override
1354                 public int stop(IBinder caller) {
1355                     if (!checkNonNull(caller)) {
1356                         return TextToSpeech.ERROR;
1357                     }
1358 
1359                     return mSynthHandler.stopForApp(caller);
1360                 }
1361 
1362                 @Override
1363                 public String[] getLanguage() {
1364                     return onGetLanguage();
1365                 }
1366 
1367                 @Override
1368                 public String[] getClientDefaultLanguage() {
1369                     return getSettingsLocale();
1370                 }
1371 
1372                 /*
1373                  * If defaults are enforced, then no language is "available" except
1374                  * perhaps the default language selected by the user.
1375                  */
1376                 @Override
1377                 public int isLanguageAvailable(String lang, String country, String variant) {
1378                     if (!checkNonNull(lang)) {
1379                         return TextToSpeech.ERROR;
1380                     }
1381 
1382                     return onIsLanguageAvailable(lang, country, variant);
1383                 }
1384 
1385                 @Override
1386                 public String[] getFeaturesForLanguage(
1387                         String lang, String country, String variant) {
1388                     Set<String> features = onGetFeaturesForLanguage(lang, country, variant);
1389                     String[] featuresArray = null;
1390                     if (features != null) {
1391                         featuresArray = new String[features.size()];
1392                         features.toArray(featuresArray);
1393                     } else {
1394                         featuresArray = new String[0];
1395                     }
1396                     return featuresArray;
1397                 }
1398 
1399                 /*
1400                  * There is no point loading a non default language if defaults
1401                  * are enforced.
1402                  */
1403                 @Override
1404                 public int loadLanguage(
1405                         IBinder caller, String lang, String country, String variant) {
1406                     if (!checkNonNull(lang)) {
1407                         return TextToSpeech.ERROR;
1408                     }
1409                     int retVal = onIsLanguageAvailable(lang, country, variant);
1410 
1411                     if (retVal == TextToSpeech.LANG_AVAILABLE
1412                             || retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE
1413                             || retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
1414 
1415                         SpeechItem item =
1416                                 new LoadLanguageItem(
1417                                         caller,
1418                                         Binder.getCallingUid(),
1419                                         Binder.getCallingPid(),
1420                                         lang,
1421                                         country,
1422                                         variant);
1423 
1424                         if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item)
1425                                 != TextToSpeech.SUCCESS) {
1426                             return TextToSpeech.ERROR;
1427                         }
1428                     }
1429                     return retVal;
1430                 }
1431 
1432                 @Override
1433                 public List<Voice> getVoices() {
1434                     return onGetVoices();
1435                 }
1436 
1437                 @Override
1438                 public int loadVoice(IBinder caller, String voiceName) {
1439                     if (!checkNonNull(voiceName)) {
1440                         return TextToSpeech.ERROR;
1441                     }
1442                     int retVal = onIsValidVoiceName(voiceName);
1443 
1444                     if (retVal == TextToSpeech.SUCCESS) {
1445                         SpeechItem item =
1446                                 new LoadVoiceItem(
1447                                         caller,
1448                                         Binder.getCallingUid(),
1449                                         Binder.getCallingPid(),
1450                                         voiceName);
1451                         if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item)
1452                                 != TextToSpeech.SUCCESS) {
1453                             return TextToSpeech.ERROR;
1454                         }
1455                     }
1456                     return retVal;
1457                 }
1458 
1459                 public String getDefaultVoiceNameFor(String lang, String country, String variant) {
1460                     if (!checkNonNull(lang)) {
1461                         return null;
1462                     }
1463                     int retVal = onIsLanguageAvailable(lang, country, variant);
1464 
1465                     if (retVal == TextToSpeech.LANG_AVAILABLE
1466                             || retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE
1467                             || retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
1468                         return onGetDefaultVoiceNameFor(lang, country, variant);
1469                     } else {
1470                         return null;
1471                     }
1472                 }
1473 
1474                 @Override
1475                 public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
1476                     // Note that passing in a null callback is a valid use case.
1477                     if (!checkNonNull(caller)) {
1478                         return;
1479                     }
1480 
1481                     mCallbacks.setCallback(caller, cb);
1482                 }
1483 
1484                 private String intern(String in) {
1485                     // The input parameter will be non null.
1486                     return in.intern();
1487                 }
1488 
1489                 private boolean checkNonNull(Object... args) {
1490                     for (Object o : args) {
1491                         if (o == null) return false;
1492                     }
1493                     return true;
1494                 }
1495             };
1496 
1497     private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
1498         private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback
1499                 = new HashMap<IBinder, ITextToSpeechCallback>();
1500 
setCallback(IBinder caller, ITextToSpeechCallback cb)1501         public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
1502             synchronized (mCallerToCallback) {
1503                 ITextToSpeechCallback old;
1504                 if (cb != null) {
1505                     register(cb, caller);
1506                     old = mCallerToCallback.put(caller, cb);
1507                 } else {
1508                     old = mCallerToCallback.remove(caller);
1509                 }
1510                 if (old != null && old != cb) {
1511                     unregister(old);
1512                 }
1513             }
1514         }
1515 
dispatchOnStop(Object callerIdentity, String utteranceId, boolean started)1516         public void dispatchOnStop(Object callerIdentity, String utteranceId, boolean started) {
1517             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1518             if (cb == null) return;
1519             try {
1520                 cb.onStop(utteranceId, started);
1521             } catch (RemoteException e) {
1522                 Log.e(TAG, "Callback onStop failed: " + e);
1523             }
1524         }
1525 
dispatchOnSuccess(Object callerIdentity, String utteranceId)1526         public void dispatchOnSuccess(Object callerIdentity, String utteranceId) {
1527             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1528             if (cb == null) return;
1529             try {
1530                 cb.onSuccess(utteranceId);
1531             } catch (RemoteException e) {
1532                 Log.e(TAG, "Callback onDone failed: " + e);
1533             }
1534         }
1535 
dispatchOnStart(Object callerIdentity, String utteranceId)1536         public void dispatchOnStart(Object callerIdentity, String utteranceId) {
1537             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1538             if (cb == null) return;
1539             try {
1540                 cb.onStart(utteranceId);
1541             } catch (RemoteException e) {
1542                 Log.e(TAG, "Callback onStart failed: " + e);
1543             }
1544         }
1545 
dispatchOnError(Object callerIdentity, String utteranceId, int errorCode)1546         public void dispatchOnError(Object callerIdentity, String utteranceId,
1547                 int errorCode) {
1548             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1549             if (cb == null) return;
1550             try {
1551                 cb.onError(utteranceId, errorCode);
1552             } catch (RemoteException e) {
1553                 Log.e(TAG, "Callback onError failed: " + e);
1554             }
1555         }
1556 
dispatchOnBeginSynthesis(Object callerIdentity, String utteranceId, int sampleRateInHz, int audioFormat, int channelCount)1557         public void dispatchOnBeginSynthesis(Object callerIdentity, String utteranceId, int sampleRateInHz, int audioFormat, int channelCount) {
1558             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1559             if (cb == null) return;
1560             try {
1561                 cb.onBeginSynthesis(utteranceId, sampleRateInHz, audioFormat, channelCount);
1562             } catch (RemoteException e) {
1563                 Log.e(TAG, "Callback dispatchOnBeginSynthesis(String, int, int, int) failed: " + e);
1564             }
1565         }
1566 
dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer)1567         public void dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer) {
1568             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1569             if (cb == null) return;
1570             try {
1571                 cb.onAudioAvailable(utteranceId, buffer);
1572             } catch (RemoteException e) {
1573                 Log.e(TAG, "Callback dispatchOnAudioAvailable(String, byte[]) failed: " + e);
1574             }
1575         }
1576 
dispatchOnRangeStart( Object callerIdentity, String utteranceId, int start, int end, int frame)1577         public void dispatchOnRangeStart(
1578                 Object callerIdentity, String utteranceId, int start, int end, int frame) {
1579             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1580             if (cb == null) return;
1581             try {
1582                 cb.onRangeStart(utteranceId, start, end, frame);
1583             } catch (RemoteException e) {
1584                 Log.e(TAG, "Callback dispatchOnRangeStart(String, int, int, int) failed: " + e);
1585             }
1586         }
1587 
1588         @Override
onCallbackDied(ITextToSpeechCallback callback, Object cookie)1589         public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
1590             IBinder caller = (IBinder) cookie;
1591             synchronized (mCallerToCallback) {
1592                 mCallerToCallback.remove(caller);
1593             }
1594             //mSynthHandler.stopForApp(caller);
1595         }
1596 
1597         @Override
kill()1598         public void kill() {
1599             synchronized (mCallerToCallback) {
1600                 mCallerToCallback.clear();
1601                 super.kill();
1602             }
1603         }
1604 
getCallbackFor(Object caller)1605         private ITextToSpeechCallback getCallbackFor(Object caller) {
1606             ITextToSpeechCallback cb;
1607             IBinder asBinder = (IBinder) caller;
1608             synchronized (mCallerToCallback) {
1609                 cb = mCallerToCallback.get(asBinder);
1610             }
1611 
1612             return cb;
1613         }
1614     }
1615 }
1616