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