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