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.net.Uri;
     21 import android.os.Binder;
     22 import android.os.Bundle;
     23 import android.os.Handler;
     24 import android.os.HandlerThread;
     25 import android.os.IBinder;
     26 import android.os.Looper;
     27 import android.os.Message;
     28 import android.os.MessageQueue;
     29 import android.os.ParcelFileDescriptor;
     30 import android.os.RemoteCallbackList;
     31 import android.os.RemoteException;
     32 import android.provider.Settings;
     33 import android.speech.tts.TextToSpeech.Engine;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 
     37 import java.io.FileDescriptor;
     38 import java.io.FileOutputStream;
     39 import java.io.IOException;
     40 import java.util.HashMap;
     41 import java.util.Locale;
     42 import java.util.Set;
     43 
     44 
     45 /**
     46  * Abstract base class for TTS engine implementations. The following methods
     47  * need to be implemented.
     48  *
     49  * <ul>
     50  *   <li>{@link #onIsLanguageAvailable}</li>
     51  *   <li>{@link #onLoadLanguage}</li>
     52  *   <li>{@link #onGetLanguage}</li>
     53  *   <li>{@link #onSynthesizeText}</li>
     54  *   <li>{@link #onStop}</li>
     55  * </ul>
     56  *
     57  * The first three deal primarily with language management, and are used to
     58  * query the engine for it's support for a given language and indicate to it
     59  * that requests in a given language are imminent.
     60  *
     61  * {@link #onSynthesizeText} is central to the engine implementation. The
     62  * implementation should synthesize text as per the request parameters and
     63  * return synthesized data via the supplied callback. This class and its helpers
     64  * will then consume that data, which might mean queueing it for playback or writing
     65  * it to a file or similar. All calls to this method will be on a single
     66  * thread, which will be different from the main thread of the service. Synthesis
     67  * must be synchronous which means the engine must NOT hold on the callback or call
     68  * any methods on it after the method returns
     69  *
     70  * {@link #onStop} tells the engine that it should stop all ongoing synthesis, if
     71  * any. Any pending data from the current synthesis will be discarded.
     72  *
     73  */
     74 public abstract class TextToSpeechService extends Service {
     75 
     76     private static final boolean DBG = false;
     77     private static final String TAG = "TextToSpeechService";
     78 
     79 
     80     private static final String SYNTH_THREAD_NAME = "SynthThread";
     81 
     82     private SynthHandler mSynthHandler;
     83     // A thread and it's associated handler for playing back any audio
     84     // associated with this TTS engine. Will handle all requests except synthesis
     85     // to file requests, which occur on the synthesis thread.
     86     private AudioPlaybackHandler mAudioPlaybackHandler;
     87     private TtsEngines mEngineHelper;
     88 
     89     private CallbackMap mCallbacks;
     90     private String mPackageName;
     91 
     92     @Override
     93     public void onCreate() {
     94         if (DBG) Log.d(TAG, "onCreate()");
     95         super.onCreate();
     96 
     97         SynthThread synthThread = new SynthThread();
     98         synthThread.start();
     99         mSynthHandler = new SynthHandler(synthThread.getLooper());
    100 
    101         mAudioPlaybackHandler = new AudioPlaybackHandler();
    102         mAudioPlaybackHandler.start();
    103 
    104         mEngineHelper = new TtsEngines(this);
    105 
    106         mCallbacks = new CallbackMap();
    107 
    108         mPackageName = getApplicationInfo().packageName;
    109 
    110         String[] defaultLocale = getSettingsLocale();
    111         // Load default language
    112         onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]);
    113     }
    114 
    115     @Override
    116     public void onDestroy() {
    117         if (DBG) Log.d(TAG, "onDestroy()");
    118 
    119         // Tell the synthesizer to stop
    120         mSynthHandler.quit();
    121         // Tell the audio playback thread to stop.
    122         mAudioPlaybackHandler.quit();
    123         // Unregister all callbacks.
    124         mCallbacks.kill();
    125 
    126         super.onDestroy();
    127     }
    128 
    129     /**
    130      * Checks whether the engine supports a given language.
    131      *
    132      * Can be called on multiple threads.
    133      *
    134      * Its return values HAVE to be consistent with onLoadLanguage.
    135      *
    136      * @param lang ISO-3 language code.
    137      * @param country ISO-3 country code. May be empty or null.
    138      * @param variant Language variant. May be empty or null.
    139      * @return Code indicating the support status for the locale.
    140      *         One of {@link TextToSpeech#LANG_AVAILABLE},
    141      *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
    142      *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
    143      *         {@link TextToSpeech#LANG_MISSING_DATA}
    144      *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
    145      */
    146     protected abstract int onIsLanguageAvailable(String lang, String country, String variant);
    147 
    148     /**
    149      * Returns the language, country and variant currently being used by the TTS engine.
    150      *
    151      * Can be called on multiple threads.
    152      *
    153      * @return A 3-element array, containing language (ISO 3-letter code),
    154      *         country (ISO 3-letter code) and variant used by the engine.
    155      *         The country and variant may be {@code ""}. If country is empty, then variant must
    156      *         be empty too.
    157      * @see Locale#getISO3Language()
    158      * @see Locale#getISO3Country()
    159      * @see Locale#getVariant()
    160      */
    161     protected abstract String[] onGetLanguage();
    162 
    163     /**
    164      * Notifies the engine that it should load a speech synthesis language. There is no guarantee
    165      * that this method is always called before the language is used for synthesis. It is merely
    166      * a hint to the engine that it will probably get some synthesis requests for this language
    167      * at some point in the future.
    168      *
    169      * Can be called on multiple threads.
    170      * In <= Android 4.2 (<= API 17) can be called on main and service binder threads.
    171      * In > Android 4.2 (> API 17) can be called on main and synthesis threads.
    172      *
    173      * @param lang ISO-3 language code.
    174      * @param country ISO-3 country code. May be empty or null.
    175      * @param variant Language variant. May be empty or null.
    176      * @return Code indicating the support status for the locale.
    177      *         One of {@link TextToSpeech#LANG_AVAILABLE},
    178      *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
    179      *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
    180      *         {@link TextToSpeech#LANG_MISSING_DATA}
    181      *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
    182      */
    183     protected abstract int onLoadLanguage(String lang, String country, String variant);
    184 
    185     /**
    186      * Notifies the service that it should stop any in-progress speech synthesis.
    187      * This method can be called even if no speech synthesis is currently in progress.
    188      *
    189      * Can be called on multiple threads, but not on the synthesis thread.
    190      */
    191     protected abstract void onStop();
    192 
    193     /**
    194      * Tells the service to synthesize speech from the given text. This method should
    195      * block until the synthesis is finished.
    196      *
    197      * Called on the synthesis thread.
    198      *
    199      * @param request The synthesis request.
    200      * @param callback The callback the the engine must use to make data available for
    201      *         playback or for writing to a file.
    202      */
    203     protected abstract void onSynthesizeText(SynthesisRequest request,
    204             SynthesisCallback callback);
    205 
    206     /**
    207      * Queries the service for a set of features supported for a given language.
    208      *
    209      * @param lang ISO-3 language code.
    210      * @param country ISO-3 country code. May be empty or null.
    211      * @param variant Language variant. May be empty or null.
    212      * @return A list of features supported for the given language.
    213      */
    214     protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) {
    215         return null;
    216     }
    217 
    218     private int getDefaultSpeechRate() {
    219         return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE);
    220     }
    221 
    222     private String[] getSettingsLocale() {
    223         final String locale = mEngineHelper.getLocalePrefForEngine(mPackageName);
    224         return TtsEngines.parseLocalePref(locale);
    225     }
    226 
    227     private int getSecureSettingInt(String name, int defaultValue) {
    228         return Settings.Secure.getInt(getContentResolver(), name, defaultValue);
    229     }
    230 
    231     /**
    232      * Synthesizer thread. This thread is used to run {@link SynthHandler}.
    233      */
    234     private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler {
    235 
    236         private boolean mFirstIdle = true;
    237 
    238         public SynthThread() {
    239             super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_DEFAULT);
    240         }
    241 
    242         @Override
    243         protected void onLooperPrepared() {
    244             getLooper().getQueue().addIdleHandler(this);
    245         }
    246 
    247         @Override
    248         public boolean queueIdle() {
    249             if (mFirstIdle) {
    250                 mFirstIdle = false;
    251             } else {
    252                 broadcastTtsQueueProcessingCompleted();
    253             }
    254             return true;
    255         }
    256 
    257         private void broadcastTtsQueueProcessingCompleted() {
    258             Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED);
    259             if (DBG) Log.d(TAG, "Broadcasting: " + i);
    260             sendBroadcast(i);
    261         }
    262     }
    263 
    264     private class SynthHandler extends Handler {
    265         private SpeechItem mCurrentSpeechItem = null;
    266 
    267         public SynthHandler(Looper looper) {
    268             super(looper);
    269         }
    270 
    271         private synchronized SpeechItem getCurrentSpeechItem() {
    272             return mCurrentSpeechItem;
    273         }
    274 
    275         private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) {
    276             SpeechItem old = mCurrentSpeechItem;
    277             mCurrentSpeechItem = speechItem;
    278             return old;
    279         }
    280 
    281         private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) {
    282             if (mCurrentSpeechItem != null &&
    283                     (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) {
    284                 SpeechItem current = mCurrentSpeechItem;
    285                 mCurrentSpeechItem = null;
    286                 return current;
    287             }
    288 
    289             return null;
    290         }
    291 
    292         public boolean isSpeaking() {
    293             return getCurrentSpeechItem() != null;
    294         }
    295 
    296         public void quit() {
    297             // Don't process any more speech items
    298             getLooper().quit();
    299             // Stop the current speech item
    300             SpeechItem current = setCurrentSpeechItem(null);
    301             if (current != null) {
    302                 current.stop();
    303             }
    304             // The AudioPlaybackHandler will be destroyed by the caller.
    305         }
    306 
    307         /**
    308          * Adds a speech item to the queue.
    309          *
    310          * Called on a service binder thread.
    311          */
    312         public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
    313             UtteranceProgressDispatcher utterenceProgress = null;
    314             if (speechItem instanceof UtteranceProgressDispatcher) {
    315                 utterenceProgress = (UtteranceProgressDispatcher) speechItem;
    316             }
    317 
    318             if (!speechItem.isValid()) {
    319                 if (utterenceProgress != null) {
    320                     utterenceProgress.dispatchOnError();
    321                 }
    322                 return TextToSpeech.ERROR;
    323             }
    324 
    325             if (queueMode == TextToSpeech.QUEUE_FLUSH) {
    326                 stopForApp(speechItem.getCallerIdentity());
    327             } else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
    328                 stopAll();
    329             }
    330             Runnable runnable = new Runnable() {
    331                 @Override
    332                 public void run() {
    333                     setCurrentSpeechItem(speechItem);
    334                     speechItem.play();
    335                     setCurrentSpeechItem(null);
    336                 }
    337             };
    338             Message msg = Message.obtain(this, runnable);
    339 
    340             // The obj is used to remove all callbacks from the given app in
    341             // stopForApp(String).
    342             //
    343             // Note that this string is interned, so the == comparison works.
    344             msg.obj = speechItem.getCallerIdentity();
    345             if (sendMessage(msg)) {
    346                 return TextToSpeech.SUCCESS;
    347             } else {
    348                 Log.w(TAG, "SynthThread has quit");
    349                 if (utterenceProgress != null) {
    350                     utterenceProgress.dispatchOnError();
    351                 }
    352                 return TextToSpeech.ERROR;
    353             }
    354         }
    355 
    356         /**
    357          * Stops all speech output and removes any utterances still in the queue for
    358          * the calling app.
    359          *
    360          * Called on a service binder thread.
    361          */
    362         public int stopForApp(Object callerIdentity) {
    363             if (callerIdentity == null) {
    364                 return TextToSpeech.ERROR;
    365             }
    366 
    367             removeCallbacksAndMessages(callerIdentity);
    368             // This stops writing data to the file / or publishing
    369             // items to the audio playback handler.
    370             //
    371             // Note that the current speech item must be removed only if it
    372             // belongs to the callingApp, else the item will be "orphaned" and
    373             // not stopped correctly if a stop request comes along for the item
    374             // from the app it belongs to.
    375             SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity);
    376             if (current != null) {
    377                 current.stop();
    378             }
    379 
    380             // Remove any enqueued audio too.
    381             mAudioPlaybackHandler.stopForApp(callerIdentity);
    382 
    383             return TextToSpeech.SUCCESS;
    384         }
    385 
    386         public int stopAll() {
    387             // Stop the current speech item unconditionally .
    388             SpeechItem current = setCurrentSpeechItem(null);
    389             if (current != null) {
    390                 current.stop();
    391             }
    392             // Remove all other items from the queue.
    393             removeCallbacksAndMessages(null);
    394             // Remove all pending playback as well.
    395             mAudioPlaybackHandler.stop();
    396 
    397             return TextToSpeech.SUCCESS;
    398         }
    399     }
    400 
    401     interface UtteranceProgressDispatcher {
    402         public void dispatchOnDone();
    403         public void dispatchOnStart();
    404         public void dispatchOnError();
    405     }
    406 
    407     /**
    408      * An item in the synth thread queue.
    409      */
    410     private abstract class SpeechItem {
    411         private final Object mCallerIdentity;
    412         protected final Bundle mParams;
    413         private final int mCallerUid;
    414         private final int mCallerPid;
    415         private boolean mStarted = false;
    416         private boolean mStopped = false;
    417 
    418         public SpeechItem(Object caller, int callerUid, int callerPid, Bundle params) {
    419             mCallerIdentity = caller;
    420             mParams = params;
    421             mCallerUid = callerUid;
    422             mCallerPid = callerPid;
    423         }
    424 
    425         public Object getCallerIdentity() {
    426             return mCallerIdentity;
    427         }
    428 
    429 
    430         public int getCallerUid() {
    431             return mCallerUid;
    432         }
    433 
    434         public int getCallerPid() {
    435             return mCallerPid;
    436         }
    437 
    438         /**
    439          * Checker whether the item is valid. If this method returns false, the item should not
    440          * be played.
    441          */
    442         public abstract boolean isValid();
    443 
    444         /**
    445          * Plays the speech item. Blocks until playback is finished.
    446          * Must not be called more than once.
    447          *
    448          * Only called on the synthesis thread.
    449          *
    450          * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}.
    451          */
    452         public int play() {
    453             synchronized (this) {
    454                 if (mStarted) {
    455                     throw new IllegalStateException("play() called twice");
    456                 }
    457                 mStarted = true;
    458             }
    459             return playImpl();
    460         }
    461 
    462         protected abstract int playImpl();
    463 
    464         /**
    465          * Stops the speech item.
    466          * Must not be called more than once.
    467          *
    468          * Can be called on multiple threads,  but not on the synthesis thread.
    469          */
    470         public void stop() {
    471             synchronized (this) {
    472                 if (mStopped) {
    473                     throw new IllegalStateException("stop() called twice");
    474                 }
    475                 mStopped = true;
    476             }
    477             stopImpl();
    478         }
    479 
    480         protected abstract void stopImpl();
    481 
    482         protected synchronized boolean isStopped() {
    483              return mStopped;
    484         }
    485     }
    486 
    487     /**
    488      * An item in the synth thread queue that process utterance.
    489      */
    490     private abstract class UtteranceSpeechItem extends SpeechItem
    491         implements UtteranceProgressDispatcher  {
    492 
    493         public UtteranceSpeechItem(Object caller, int callerUid, int callerPid, Bundle params) {
    494             super(caller, callerUid, callerPid, params);
    495         }
    496 
    497         @Override
    498         public void dispatchOnDone() {
    499             final String utteranceId = getUtteranceId();
    500             if (utteranceId != null) {
    501                 mCallbacks.dispatchOnDone(getCallerIdentity(), utteranceId);
    502             }
    503         }
    504 
    505         @Override
    506         public void dispatchOnStart() {
    507             final String utteranceId = getUtteranceId();
    508             if (utteranceId != null) {
    509                 mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId);
    510             }
    511         }
    512 
    513         @Override
    514         public void dispatchOnError() {
    515             final String utteranceId = getUtteranceId();
    516             if (utteranceId != null) {
    517                 mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId);
    518             }
    519         }
    520 
    521         public int getStreamType() {
    522             return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM);
    523         }
    524 
    525         public float getVolume() {
    526             return getFloatParam(Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME);
    527         }
    528 
    529         public float getPan() {
    530             return getFloatParam(Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN);
    531         }
    532 
    533         public String getUtteranceId() {
    534             return getStringParam(Engine.KEY_PARAM_UTTERANCE_ID, null);
    535         }
    536 
    537         protected String getStringParam(String key, String defaultValue) {
    538             return mParams == null ? defaultValue : mParams.getString(key, defaultValue);
    539         }
    540 
    541         protected int getIntParam(String key, int defaultValue) {
    542             return mParams == null ? defaultValue : mParams.getInt(key, defaultValue);
    543         }
    544 
    545         protected float getFloatParam(String key, float defaultValue) {
    546             return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue);
    547         }
    548 
    549     }
    550 
    551     class SynthesisSpeechItem extends UtteranceSpeechItem {
    552         // Never null.
    553         private final String mText;
    554         private final SynthesisRequest mSynthesisRequest;
    555         private final String[] mDefaultLocale;
    556         // Non null after synthesis has started, and all accesses
    557         // guarded by 'this'.
    558         private AbstractSynthesisCallback mSynthesisCallback;
    559         private final EventLogger mEventLogger;
    560         private final int mCallerUid;
    561 
    562         public SynthesisSpeechItem(Object callerIdentity, int callerUid, int callerPid,
    563                 Bundle params, String text) {
    564             super(callerIdentity, callerUid, callerPid, params);
    565             mText = text;
    566             mCallerUid = callerUid;
    567             mSynthesisRequest = new SynthesisRequest(mText, mParams);
    568             mDefaultLocale = getSettingsLocale();
    569             setRequestParams(mSynthesisRequest);
    570             mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid,
    571                     mPackageName);
    572         }
    573 
    574         public String getText() {
    575             return mText;
    576         }
    577 
    578         @Override
    579         public boolean isValid() {
    580             if (mText == null) {
    581                 Log.e(TAG, "null synthesis text");
    582                 return false;
    583             }
    584             if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) {
    585                 Log.w(TAG, "Text too long: " + mText.length() + " chars");
    586                 return false;
    587             }
    588             return true;
    589         }
    590 
    591         @Override
    592         protected int playImpl() {
    593             AbstractSynthesisCallback synthesisCallback;
    594             mEventLogger.onRequestProcessingStart();
    595             synchronized (this) {
    596                 // stop() might have been called before we enter this
    597                 // synchronized block.
    598                 if (isStopped()) {
    599                     return TextToSpeech.ERROR;
    600                 }
    601                 mSynthesisCallback = createSynthesisCallback();
    602                 synthesisCallback = mSynthesisCallback;
    603             }
    604             TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
    605             return synthesisCallback.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR;
    606         }
    607 
    608         protected AbstractSynthesisCallback createSynthesisCallback() {
    609             return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(),
    610                     mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger);
    611         }
    612 
    613         private void setRequestParams(SynthesisRequest request) {
    614             request.setLanguage(getLanguage(), getCountry(), getVariant());
    615             request.setSpeechRate(getSpeechRate());
    616             request.setCallerUid(mCallerUid);
    617             request.setPitch(getPitch());
    618         }
    619 
    620         @Override
    621         protected void stopImpl() {
    622             AbstractSynthesisCallback synthesisCallback;
    623             synchronized (this) {
    624                 synthesisCallback = mSynthesisCallback;
    625             }
    626             if (synthesisCallback != null) {
    627                 // If the synthesis callback is null, it implies that we haven't
    628                 // entered the synchronized(this) block in playImpl which in
    629                 // turn implies that synthesis would not have started.
    630                 synthesisCallback.stop();
    631                 TextToSpeechService.this.onStop();
    632             }
    633         }
    634 
    635         public String getLanguage() {
    636             return getStringParam(Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]);
    637         }
    638 
    639         private boolean hasLanguage() {
    640             return !TextUtils.isEmpty(getStringParam(Engine.KEY_PARAM_LANGUAGE, null));
    641         }
    642 
    643         private String getCountry() {
    644             if (!hasLanguage()) return mDefaultLocale[1];
    645             return getStringParam(Engine.KEY_PARAM_COUNTRY, "");
    646         }
    647 
    648         private String getVariant() {
    649             if (!hasLanguage()) return mDefaultLocale[2];
    650             return getStringParam(Engine.KEY_PARAM_VARIANT, "");
    651         }
    652 
    653         private int getSpeechRate() {
    654             return getIntParam(Engine.KEY_PARAM_RATE, getDefaultSpeechRate());
    655         }
    656 
    657         private int getPitch() {
    658             return getIntParam(Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH);
    659         }
    660     }
    661 
    662     private class SynthesisToFileOutputStreamSpeechItem extends SynthesisSpeechItem {
    663         private final FileOutputStream mFileOutputStream;
    664 
    665         public SynthesisToFileOutputStreamSpeechItem(Object callerIdentity, int callerUid,
    666                 int callerPid, Bundle params, String text, FileOutputStream fileOutputStream) {
    667             super(callerIdentity, callerUid, callerPid, params, text);
    668             mFileOutputStream = fileOutputStream;
    669         }
    670 
    671         @Override
    672         protected AbstractSynthesisCallback createSynthesisCallback() {
    673             return new FileSynthesisCallback(mFileOutputStream.getChannel());
    674         }
    675 
    676         @Override
    677         protected int playImpl() {
    678             dispatchOnStart();
    679             int status = super.playImpl();
    680             if (status == TextToSpeech.SUCCESS) {
    681                 dispatchOnDone();
    682             } else {
    683                 dispatchOnError();
    684             }
    685             try {
    686               mFileOutputStream.close();
    687             } catch(IOException e) {
    688               Log.w(TAG, "Failed to close output file", e);
    689             }
    690             return status;
    691         }
    692     }
    693 
    694     private class AudioSpeechItem extends UtteranceSpeechItem {
    695         private final AudioPlaybackQueueItem mItem;
    696         public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid,
    697                 Bundle params, Uri uri) {
    698             super(callerIdentity, callerUid, callerPid, params);
    699             mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(),
    700                     TextToSpeechService.this, uri, getStreamType());
    701         }
    702 
    703         @Override
    704         public boolean isValid() {
    705             return true;
    706         }
    707 
    708         @Override
    709         protected int playImpl() {
    710             mAudioPlaybackHandler.enqueue(mItem);
    711             return TextToSpeech.SUCCESS;
    712         }
    713 
    714         @Override
    715         protected void stopImpl() {
    716             // Do nothing.
    717         }
    718     }
    719 
    720     private class SilenceSpeechItem extends UtteranceSpeechItem {
    721         private final long mDuration;
    722 
    723         public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid,
    724                 Bundle params, long duration) {
    725             super(callerIdentity, callerUid, callerPid, params);
    726             mDuration = duration;
    727         }
    728 
    729         @Override
    730         public boolean isValid() {
    731             return true;
    732         }
    733 
    734         @Override
    735         protected int playImpl() {
    736             mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem(
    737                     this, getCallerIdentity(), mDuration));
    738             return TextToSpeech.SUCCESS;
    739         }
    740 
    741         @Override
    742         protected void stopImpl() {
    743             // Do nothing, handled by AudioPlaybackHandler#stopForApp
    744         }
    745     }
    746 
    747     private class LoadLanguageItem extends SpeechItem {
    748         private final String mLanguage;
    749         private final String mCountry;
    750         private final String mVariant;
    751 
    752         public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid,
    753                 Bundle params, String language, String country, String variant) {
    754             super(callerIdentity, callerUid, callerPid, params);
    755             mLanguage = language;
    756             mCountry = country;
    757             mVariant = variant;
    758         }
    759 
    760         @Override
    761         public boolean isValid() {
    762             return true;
    763         }
    764 
    765         @Override
    766         protected int playImpl() {
    767             int result = TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant);
    768             if (result == TextToSpeech.LANG_AVAILABLE ||
    769                     result == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
    770                     result == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
    771                 return TextToSpeech.SUCCESS;
    772             }
    773             return TextToSpeech.ERROR;
    774         }
    775 
    776         @Override
    777         protected void stopImpl() {
    778             // No-op
    779         }
    780     }
    781 
    782     @Override
    783     public IBinder onBind(Intent intent) {
    784         if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
    785             return mBinder;
    786         }
    787         return null;
    788     }
    789 
    790     /**
    791      * Binder returned from {@code #onBind(Intent)}. The methods in this class can be
    792      * called called from several different threads.
    793      */
    794     // NOTE: All calls that are passed in a calling app are interned so that
    795     // they can be used as message objects (which are tested for equality using ==).
    796     private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() {
    797         @Override
    798         public int speak(IBinder caller, String text, int queueMode, Bundle params) {
    799             if (!checkNonNull(caller, text, params)) {
    800                 return TextToSpeech.ERROR;
    801             }
    802 
    803             SpeechItem item = new SynthesisSpeechItem(caller,
    804                     Binder.getCallingUid(), Binder.getCallingPid(), params, text);
    805             return mSynthHandler.enqueueSpeechItem(queueMode, item);
    806         }
    807 
    808         @Override
    809         public int synthesizeToFileDescriptor(IBinder caller, String text, ParcelFileDescriptor
    810                 fileDescriptor, Bundle params) {
    811             if (!checkNonNull(caller, text, fileDescriptor, params)) {
    812                 return TextToSpeech.ERROR;
    813             }
    814 
    815             // In test env, ParcelFileDescriptor instance may be EXACTLY the same
    816             // one that is used by client. And it will be closed by a client, thus
    817             // preventing us from writing anything to it.
    818             final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd(
    819                     fileDescriptor.detachFd());
    820 
    821             SpeechItem item = new SynthesisToFileOutputStreamSpeechItem(caller,
    822                     Binder.getCallingUid(), Binder.getCallingPid(), params, text,
    823                     new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor));
    824             return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
    825         }
    826 
    827         @Override
    828         public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params) {
    829             if (!checkNonNull(caller, audioUri, params)) {
    830                 return TextToSpeech.ERROR;
    831             }
    832 
    833             SpeechItem item = new AudioSpeechItem(caller,
    834                     Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri);
    835             return mSynthHandler.enqueueSpeechItem(queueMode, item);
    836         }
    837 
    838         @Override
    839         public int playSilence(IBinder caller, long duration, int queueMode, Bundle params) {
    840             if (!checkNonNull(caller, params)) {
    841                 return TextToSpeech.ERROR;
    842             }
    843 
    844             SpeechItem item = new SilenceSpeechItem(caller,
    845                     Binder.getCallingUid(), Binder.getCallingPid(), params, duration);
    846             return mSynthHandler.enqueueSpeechItem(queueMode, item);
    847         }
    848 
    849         @Override
    850         public boolean isSpeaking() {
    851             return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking();
    852         }
    853 
    854         @Override
    855         public int stop(IBinder caller) {
    856             if (!checkNonNull(caller)) {
    857                 return TextToSpeech.ERROR;
    858             }
    859 
    860             return mSynthHandler.stopForApp(caller);
    861         }
    862 
    863         @Override
    864         public String[] getLanguage() {
    865             return onGetLanguage();
    866         }
    867 
    868         @Override
    869         public String[] getClientDefaultLanguage() {
    870             return getSettingsLocale();
    871         }
    872 
    873         /*
    874          * If defaults are enforced, then no language is "available" except
    875          * perhaps the default language selected by the user.
    876          */
    877         @Override
    878         public int isLanguageAvailable(String lang, String country, String variant) {
    879             if (!checkNonNull(lang)) {
    880                 return TextToSpeech.ERROR;
    881             }
    882 
    883             return onIsLanguageAvailable(lang, country, variant);
    884         }
    885 
    886         @Override
    887         public String[] getFeaturesForLanguage(String lang, String country, String variant) {
    888             Set<String> features = onGetFeaturesForLanguage(lang, country, variant);
    889             String[] featuresArray = null;
    890             if (features != null) {
    891                 featuresArray = new String[features.size()];
    892                 features.toArray(featuresArray);
    893             } else {
    894                 featuresArray = new String[0];
    895             }
    896             return featuresArray;
    897         }
    898 
    899         /*
    900          * There is no point loading a non default language if defaults
    901          * are enforced.
    902          */
    903         @Override
    904         public int loadLanguage(IBinder caller, String lang, String country, String variant) {
    905             if (!checkNonNull(lang)) {
    906                 return TextToSpeech.ERROR;
    907             }
    908             int retVal = onIsLanguageAvailable(lang, country, variant);
    909 
    910             if (retVal == TextToSpeech.LANG_AVAILABLE ||
    911                     retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
    912                     retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
    913 
    914                 SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(),
    915                     Binder.getCallingPid(), null, lang, country, variant);
    916 
    917                 if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) !=
    918                         TextToSpeech.SUCCESS) {
    919                     return TextToSpeech.ERROR;
    920                 }
    921             }
    922             return retVal;
    923         }
    924 
    925         @Override
    926         public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
    927             // Note that passing in a null callback is a valid use case.
    928             if (!checkNonNull(caller)) {
    929                 return;
    930             }
    931 
    932             mCallbacks.setCallback(caller, cb);
    933         }
    934 
    935         private String intern(String in) {
    936             // The input parameter will be non null.
    937             return in.intern();
    938         }
    939 
    940         private boolean checkNonNull(Object... args) {
    941             for (Object o : args) {
    942                 if (o == null) return false;
    943             }
    944             return true;
    945         }
    946     };
    947 
    948     private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
    949         private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback
    950                 = new HashMap<IBinder, ITextToSpeechCallback>();
    951 
    952         public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
    953             synchronized (mCallerToCallback) {
    954                 ITextToSpeechCallback old;
    955                 if (cb != null) {
    956                     register(cb, caller);
    957                     old = mCallerToCallback.put(caller, cb);
    958                 } else {
    959                     old = mCallerToCallback.remove(caller);
    960                 }
    961                 if (old != null && old != cb) {
    962                     unregister(old);
    963                 }
    964             }
    965         }
    966 
    967         public void dispatchOnDone(Object callerIdentity, String utteranceId) {
    968             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
    969             if (cb == null) return;
    970             try {
    971                 cb.onDone(utteranceId);
    972             } catch (RemoteException e) {
    973                 Log.e(TAG, "Callback onDone failed: " + e);
    974             }
    975         }
    976 
    977         public void dispatchOnStart(Object callerIdentity, String utteranceId) {
    978             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
    979             if (cb == null) return;
    980             try {
    981                 cb.onStart(utteranceId);
    982             } catch (RemoteException e) {
    983                 Log.e(TAG, "Callback onStart failed: " + e);
    984             }
    985 
    986         }
    987 
    988         public void dispatchOnError(Object callerIdentity, String utteranceId) {
    989             ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
    990             if (cb == null) return;
    991             try {
    992                 cb.onError(utteranceId);
    993             } catch (RemoteException e) {
    994                 Log.e(TAG, "Callback onError failed: " + e);
    995             }
    996         }
    997 
    998         @Override
    999         public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
   1000             IBinder caller = (IBinder) cookie;
   1001             synchronized (mCallerToCallback) {
   1002                 mCallerToCallback.remove(caller);
   1003             }
   1004             mSynthHandler.stopForApp(caller);
   1005         }
   1006 
   1007         @Override
   1008         public void kill() {
   1009             synchronized (mCallerToCallback) {
   1010                 mCallerToCallback.clear();
   1011                 super.kill();
   1012             }
   1013         }
   1014 
   1015         private ITextToSpeechCallback getCallbackFor(Object caller) {
   1016             ITextToSpeechCallback cb;
   1017             IBinder asBinder = (IBinder) caller;
   1018             synchronized (mCallerToCallback) {
   1019                 cb = mCallerToCallback.get(asBinder);
   1020             }
   1021 
   1022             return cb;
   1023         }
   1024 
   1025     }
   1026 
   1027 }
   1028