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