content: /*
   * Copyright (C) 2011 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License"); you may not
   * use this file except in compliance with the License. You may obtain a copy of
   * the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   * License for the specific language governing permissions and limitations under
   * the License.
   */
  package android.speech.tts;
  
  import android.annotation.NonNull;
  import android.app.Service;
  import android.content.Intent;
  import android.media.AudioAttributes;
  import android.media.AudioManager;
  import android.net.Uri;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.HandlerThread;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.Message;
  import android.os.MessageQueue;
  import android.os.ParcelFileDescriptor;
  import android.os.RemoteCallbackList;
  import android.os.RemoteException;
  import android.provider.Settings;
  import android.speech.tts.TextToSpeech.Engine;
  import android.text.TextUtils;
  import android.util.Log;
  
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Locale;
  import java.util.MissingResourceException;
  import java.util.Set;
  
  
  /**
   * Abstract base class for TTS engine implementations. The following methods
   * need to be implemented:
   * <ul>
   * <li>{@link #onIsLanguageAvailable}</li>
   * <li>{@link #onLoadLanguage}</li>
   * <li>{@link #onGetLanguage}</li>
   * <li>{@link #onSynthesizeText}</li>
   * <li>{@link #onStop}</li>
   * </ul>
   * The first three deal primarily with language management, and are used to
   * query the engine for it's support for a given language and indicate to it
   * that requests in a given language are imminent.
   *
   * {@link #onSynthesizeText} is central to the engine implementation. The
   * implementation should synthesize text as per the request parameters and
   * return synthesized data via the supplied callback. This class and its helpers
   * will then consume that data, which might mean queuing it for playback or writing
   * it to a file or similar. All calls to this method will be on a single thread,
   * which will be different from the main thread of the service. Synthesis must be
   * synchronous which means the engine must NOT hold on to the callback or call any
   * methods on it after the method returns.
   *
   * {@link #onStop} tells the engine that it should stop
   * all ongoing synthesis, if any. Any pending data from the current synthesis
   * will be discarded.
   *
   * {@link #onGetLanguage} is not required as of JELLYBEAN_MR2 (API 18) and later, it is only
   * called on earlier versions of Android.
   *
   * API Level 20 adds support for Voice objects. Voices are an abstraction that allow the TTS
   * service to expose multiple backends for a single locale. Each one of them can have a different
   * features set. In order to fully take advantage of voices, an engine should implement
   * the following methods:
   * <ul>
   * <li>{@link #onGetVoices()}</li>
   * <li>{@link #onIsValidVoiceName(String)}</li>
   * <li>{@link #onLoadVoice(String)}</li>
   * <li>{@link #onGetDefaultVoiceNameFor(String, String, String)}</li>
   * </ul>
   * The first three methods are siblings of the {@link #onGetLanguage},
   * {@link #onIsLanguageAvailable} and {@link #onLoadLanguage} methods. The last one,
   * {@link #onGetDefaultVoiceNameFor(String, String, String)} is a link between locale and voice
   * based methods. Since API level 21 {@link TextToSpeech#setLanguage} is implemented by
   * calling {@link TextToSpeech#setVoice} with the voice returned by
   * {@link #onGetDefaultVoiceNameFor(String, String, String)}.
   *
   * If the client uses a voice instead of a locale, {@link SynthesisRequest} will contain the
   * requested voice name.
   *
   * The default implementations of Voice-related methods implement them using the
   * pre-existing locale-based implementation.
   */
  public abstract class TextToSpeechService extends Service {
  
      private static final boolean DBG = false;
      private static final String TAG = "TextToSpeechService";
  
      private static final String SYNTH_THREAD_NAME = "SynthThread";
  
      private SynthHandler mSynthHandler;
      // A thread and it's associated handler for playing back any audio
      // associated with this TTS engine. Will handle all requests except synthesis
      // to file requests, which occur on the synthesis thread.
      @NonNull private AudioPlaybackHandler mAudioPlaybackHandler;
      private TtsEngines mEngineHelper;
  
      private CallbackMap mCallbacks;
      private String mPackageName;
  
      private final Object mVoicesInfoLock = new Object();
  
      @Override
      public void onCreate() {
          if (DBG) Log.d(TAG, "onCreate()");
          super.onCreate();
  
          SynthThread synthThread = new SynthThread();
          synthThread.start();
          mSynthHandler = new SynthHandler(synthThread.getLooper());
  
          mAudioPlaybackHandler = new AudioPlaybackHandler();
          mAudioPlaybackHandler.start();
  
          mEngineHelper = new TtsEngines(this);
  
          mCallbacks = new CallbackMap();
  
          mPackageName = getApplicationInfo().packageName;
  
          String[] defaultLocale = getSettingsLocale();
  
          // Load default language
          onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]);
      }
  
      @Override
      public void onDestroy() {
          if (DBG) Log.d(TAG, "onDestroy()");
  
          // Tell the synthesizer to stop
          mSynthHandler.quit();
          // Tell the audio playback thread to stop.
          mAudioPlaybackHandler.quit();
          // Unregister all callbacks.
          mCallbacks.kill();
  
          super.onDestroy();
      }
  
      /**
       * Checks whether the engine supports a given language.
       *
       * Can be called on multiple threads.
       *
       * Its return values HAVE to be consistent with onLoadLanguage.
       *
       * @param lang ISO-3 language code.
       * @param country ISO-3 country code. May be empty or null.
       * @param variant Language variant. May be empty or null.
       * @return Code indicating the support status for the locale.
       *         One of {@link TextToSpeech#LANG_AVAILABLE},
       *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
       *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
       *         {@link TextToSpeech#LANG_MISSING_DATA}
       *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
       */
      protected abstract int onIsLanguageAvailable(String lang, String country, String variant);
  
      /**
       * Returns the language, country and variant currently being used by the TTS engine.
       *
       * This method will be called only on Android 4.2 and before (API <= 17). In later versions
       * this method is not called by the Android TTS framework.
       *
       * Can be called on multiple threads.
       *
       * @return A 3-element array, containing language (ISO 3-letter code),
       *         country (ISO 3-letter code) and variant used by the engine.
       *         The country and variant may be {@code ""}. If country is empty, then variant must
       *         be empty too.
       * @see Locale#getISO3Language()
       * @see Locale#getISO3Country()
       * @see Locale#getVariant()
       */
      protected abstract String[] onGetLanguage();
  
      /**
       * Notifies the engine that it should load a speech synthesis language. There is no guarantee
       * that this method is always called before the language is used for synthesis. It is merely
       * a hint to the engine that it will probably get some synthesis requests for this language
       * at some point in the future.
       *
       * Can be called on multiple threads.
       * In <= Android 4.2 (<= API 17) can be called on main and service binder threads.
       * In > Android 4.2 (> API 17) can be called on main and synthesis threads.
       *
       * @param lang ISO-3 language code.
       * @param country ISO-3 country code. May be empty or null.
       * @param variant Language variant. May be empty or null.
       * @return Code indicating the support status for the locale.
       *         One of {@link TextToSpeech#LANG_AVAILABLE},
       *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
       *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
       *         {@link TextToSpeech#LANG_MISSING_DATA}
       *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
       */
      protected abstract int onLoadLanguage(String lang, String country, String variant);
  
      /**
       * Notifies the service that it should stop any in-progress speech synthesis.
       * This method can be called even if no speech synthesis is currently in progress.
       *
       * Can be called on multiple threads, but not on the synthesis thread.
       */
      protected abstract void onStop();
  
      /**
       * Tells the service to synthesize speech from the given text. This method should block until
       * the synthesis is finished. Called on the synthesis thread.
       *
       * @param request The synthesis request.
       * @param callback The callback that the engine must use to make data available for playback or
       *     for writing to a file.
       */
      protected abstract void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback);
  
      /**
       * Queries the service for a set of features supported for a given language.
       *
       * Can be called on multiple threads.
       *
       * @param lang ISO-3 language code.
       * @param country ISO-3 country code. May be empty or null.
       * @param variant Language variant. May be empty or null.
       * @return A list of features supported for the given language.
       */
      protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) {
          return new HashSet<String>();
      }
  
      private int getExpectedLanguageAvailableStatus(Locale locale) {
          int expectedStatus = TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE;
          if (locale.getVariant().isEmpty()) {
              if (locale.getCountry().isEmpty()) {
                  expectedStatus = TextToSpeech.LANG_AVAILABLE;
              } else {
                  expectedStatus = TextToSpeech.LANG_COUNTRY_AVAILABLE;
              }
          }
          return expectedStatus;
      }
  
      /**
       * Queries the service for a set of supported voices.
       *
       * Can be called on multiple threads.
       *
       * The default implementation tries to enumerate all available locales, pass them to
       * {@link #onIsLanguageAvailable(String, String, String)} and create Voice instances (using
       * the locale's BCP-47 language tag as the voice name) for the ones that are supported.
       * Note, that this implementation is suitable only for engines that don't have multiple voices
       * for a single locale. Also, this implementation won't work with Locales not listed in the
       * set returned by the {@link Locale#getAvailableLocales()} method.
       *
       * @return A list of voices supported.
       */
      public List<Voice> onGetVoices() {
          // Enumerate all locales and check if they are available
          ArrayList<Voice> voices = new ArrayList<Voice>();
          for (Locale locale : Locale.getAvailableLocales()) {
              int expectedStatus = getExpectedLanguageAvailableStatus(locale);
              try {
                  int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
                          locale.getISO3Country(), locale.getVariant());
                  if (localeStatus != expectedStatus) {
                      continue;
                  }
              } catch (MissingResourceException e) {
                  // Ignore locale without iso 3 codes
                  continue;
              }
              Set<String> features = onGetFeaturesForLanguage(locale.getISO3Language(),
                      locale.getISO3Country(), locale.getVariant());
              String voiceName = onGetDefaultVoiceNameFor(locale.getISO3Language(),
                      locale.getISO3Country(), locale.getVariant());
              voices.add(new Voice(voiceName, locale, Voice.QUALITY_NORMAL,
                      Voice.LATENCY_NORMAL, false, features));
          }
          return voices;
      }
  
      /**
       * Return a name of the default voice for a given locale.
       *
       * This method provides a mapping between locales and available voices. This method is
       * used in {@link TextToSpeech#setLanguage}, which calls this method and then calls
       * {@link TextToSpeech#setVoice} with the voice returned by this method.
       *
       * Also, it's used by {@link TextToSpeech#getDefaultVoice()} to find a default voice for
       * the default locale.
       *
       * @param lang ISO-3 language code.
       * @param country ISO-3 country code. May be empty or null.
       * @param variant Language variant. May be empty or null.
  
       * @return A name of the default voice for a given locale.
       */
      public String onGetDefaultVoiceNameFor(String lang, String country, String variant) {
          int localeStatus = onIsLanguageAvailable(lang, country, variant);
          Locale iso3Locale = null;
          switch (localeStatus) {
              case TextToSpeech.LANG_AVAILABLE:
                  iso3Locale = new Locale(lang);
                  break;
              case TextToSpeech.LANG_COUNTRY_AVAILABLE:
                  iso3Locale = new Locale(lang, country);
                  break;
              case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE:
                  iso3Locale = new Locale(lang, country, variant);
                  break;
              default:
                  return null;
          }
          Locale properLocale = TtsEngines.normalizeTTSLocale(iso3Locale);
          String voiceName = properLocale.toLanguageTag();
          if (onIsValidVoiceName(voiceName) == TextToSpeech.SUCCESS) {
              return voiceName;
          } else {
              return null;
          }
      }
  
      /**
       * Notifies the engine that it should load a speech synthesis voice. There is no guarantee
       * that this method is always called before the voice is used for synthesis. It is merely
       * a hint to the engine that it will probably get some synthesis requests for this voice
       * at some point in the future.
       *
       * Will be called only on synthesis thread.
       *
       * The default implementation creates a Locale from the voice name (by interpreting the name as
       * a BCP-47 tag for the locale), and passes it to
       * {@link #onLoadLanguage(String, String, String)}.
       *
       * @param voiceName Name of the voice.
       * @return {@link TextToSpeech#ERROR} or {@link TextToSpeech#SUCCESS}.
       */
      public int onLoadVoice(String voiceName) {
          Locale locale = Locale.forLanguageTag(voiceName);
          if (locale == null) {
              return TextToSpeech.ERROR;
          }
          int expectedStatus = getExpectedLanguageAvailableStatus(locale);
          try {
              int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
                      locale.getISO3Country(), locale.getVariant());
              if (localeStatus != expectedStatus) {
                  return TextToSpeech.ERROR;
              }
              onLoadLanguage(locale.getISO3Language(),
                      locale.getISO3Country(), locale.getVariant());
              return TextToSpeech.SUCCESS;
          } catch (MissingResourceException e) {
              return TextToSpeech.ERROR;
          }
      }
  
      /**
       * Checks whether the engine supports a voice with a given name.
       *
       * Can be called on multiple threads.
       *
       * The default implementation treats the voice name as a language tag, creating a Locale from
       * the voice name, and passes it to {@link #onIsLanguageAvailable(String, String, String)}.
       *
       * @param voiceName Name of the voice.
       * @return {@link TextToSpeech#ERROR} or {@link TextToSpeech#SUCCESS}.
       */
      public int onIsValidVoiceName(String voiceName) {
          Locale locale = Locale.forLanguageTag(voiceName);
          if (locale == null) {
              return TextToSpeech.ERROR;
          }
          int expectedStatus = getExpectedLanguageAvailableStatus(locale);
          try {
              int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
                      locale.getISO3Country(), locale.getVariant());
              if (localeStatus != expectedStatus) {
                  return TextToSpeech.ERROR;
              }
              return TextToSpeech.SUCCESS;
          } catch (MissingResourceException e) {
              return TextToSpeech.ERROR;
          }
      }
  
      private int getDefaultSpeechRate() {
          return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE);
      }
  
      private int getDefaultPitch() {
          return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_PITCH, Engine.DEFAULT_PITCH);
      }
  
      private String[] getSettingsLocale() {
          final Locale locale = mEngineHelper.getLocalePrefForEngine(mPackageName);
          return TtsEngines.toOldLocaleStringFormat(locale);
      }
  
      private int getSecureSettingInt(String name, int defaultValue) {
          return Settings.Secure.getInt(getContentResolver(), name, defaultValue);
      }
  
      /**
       * Synthesizer thread. This thread is used to run {@link SynthHandler}.
       */
      private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler {
  
          private boolean mFirstIdle = true;
  
          public SynthThread() {
              super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_DEFAULT);
          }
  
          @Override
          protected void onLooperPrepared() {
              getLooper().getQueue().addIdleHandler(this);
          }
  
          @Override
          public boolean queueIdle() {
              if (mFirstIdle) {
                  mFirstIdle = false;
              } else {
                  broadcastTtsQueueProcessingCompleted();
              }
              return true;
          }
  
          private void broadcastTtsQueueProcessingCompleted() {
              Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED);
              if (DBG) Log.d(TAG, "Broadcasting: " + i);
              sendBroadcast(i);
          }
      }
  
      private class SynthHandler extends Handler {
          private SpeechItem mCurrentSpeechItem = null;
  
          // When a message with QUEUE_FLUSH arrives we add the caller identity to the List and when a
          // message with QUEUE_DESTROY arrives we increment mFlushAll. Then a message is added to the
          // handler queue that removes the caller identify from the list and decrements the mFlushAll
          // counter. This is so that when a message is processed and the caller identity is in the
          // list or mFlushAll is not zero, we know that the message should be flushed.
          // It's important that mFlushedObjects is a List and not a Set, and that mFlushAll is an
          // int and not a bool. This is because when multiple messages arrive with QUEUE_FLUSH or
          // QUEUE_DESTROY, we want to keep flushing messages until we arrive at the last QUEUE_FLUSH
          // or QUEUE_DESTROY message.
          private List<Object> mFlushedObjects = new ArrayList<>();
          private int mFlushAll = 0;
  
          public SynthHandler(Looper looper) {
              super(looper);
          }
  
          private void startFlushingSpeechItems(Object callerIdentity) {
              synchronized (mFlushedObjects) {
                  if (callerIdentity == null) {
                      mFlushAll += 1;
                  } else {
                      mFlushedObjects.add(callerIdentity);
                  }
              }
          }
          private void endFlushingSpeechItems(Object callerIdentity) {
              synchronized (mFlushedObjects) {
                  if (callerIdentity == null) {
                      mFlushAll -= 1;
                  } else {
                      mFlushedObjects.remove(callerIdentity);
                  }
              }
          }
          private boolean isFlushed(SpeechItem speechItem) {
              synchronized (mFlushedObjects) {
                  return mFlushAll > 0 || mFlushedObjects.contains(speechItem.getCallerIdentity());
              }
          }
  
          private synchronized SpeechItem getCurrentSpeechItem() {
              return mCurrentSpeechItem;
          }
  
          private synchronized boolean setCurrentSpeechItem(SpeechItem speechItem) {
              // Do not set as current if the item has already been flushed. The check is
              // intentionally put inside this synchronized method. Specifically, the following
              // racy sequence between this method and stopForApp() needs to be avoided.
              //        (this method)          (stopForApp)
              //     1. isFlushed
              //     2.                        startFlushingSpeechItems
              //     3.                        maybeRemoveCurrentSpeechItem
              //     4. set mCurrentSpeechItem
              // If it happens, stop() is never called on the item. The guard by synchornized(this)
              // ensures that the step 3 cannot interrupt between 1 and 4.
              if (speechItem != null && isFlushed(speechItem)) {
                  return false;
              }
              mCurrentSpeechItem = speechItem;
              return true;
          }
  
          private synchronized SpeechItem removeCurrentSpeechItem() {
              SpeechItem current = mCurrentSpeechItem;
              mCurrentSpeechItem = null;
              return current;
          }
  
          private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) {
              if (mCurrentSpeechItem != null &&
                      (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) {
                  SpeechItem current = mCurrentSpeechItem;
                  mCurrentSpeechItem = null;
                  return current;
              }
  
              return null;
          }
  
          public boolean isSpeaking() {
              return getCurrentSpeechItem() != null;
          }
  
          public void quit() {
              // Don't process any more speech items
              getLooper().quit();
              // Stop the current speech item
              SpeechItem current = removeCurrentSpeechItem();
              if (current != null) {
                  current.stop();
              }
              // The AudioPlaybackHandler will be destroyed by the caller.
          }
  
          /**
           * Adds a speech item to the queue.
           *
           * Called on a service binder thread.
           */
          public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
              UtteranceProgressDispatcher utterenceProgress = null;
              if (speechItem instanceof UtteranceProgressDispatcher) {
                  utterenceProgress = (UtteranceProgressDispatcher) speechItem;
              }
  
              if (!speechItem.isValid()) {
                  if (utterenceProgress != null) {
                      utterenceProgress.dispatchOnError(
                              TextToSpeech.ERROR_INVALID_REQUEST);
                  }
                  return TextToSpeech.ERROR;
              }
  
              if (queueMode == TextToSpeech.QUEUE_FLUSH) {
                  stopForApp(speechItem.getCallerIdentity());
              } else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
                  stopAll();
              }
              Runnable runnable = new Runnable() {
                  @Override
                  public void run() {
                      if (setCurrentSpeechItem(speechItem)) {
                          speechItem.play();
                          removeCurrentSpeechItem();
                      } else {
                          // The item is alreadly flushed. Stopping.
                          speechItem.stop();
                      }
                  }
              };
              Message msg = Message.obtain(this, runnable);
  
              // The obj is used to remove all callbacks from the given app in
              // stopForApp(String).
              //
              // Note that this string is interned, so the == comparison works.
              msg.obj = speechItem.getCallerIdentity();
  
              if (sendMessage(msg)) {
                  return TextToSpeech.SUCCESS;
              } else {
                  Log.w(TAG, "SynthThread has quit");
                  if (utterenceProgress != null) {
                      utterenceProgress.dispatchOnError(TextToSpeech.ERROR_SERVICE);
                  }
                  return TextToSpeech.ERROR;
              }
          }
  
          /**
           * Stops all speech output and removes any utterances still in the queue for
           * the calling app.
           *
           * Called on a service binder thread.
           */
          public int stopForApp(final Object callerIdentity) {
              if (callerIdentity == null) {
                  return TextToSpeech.ERROR;
              }
  
              // Flush pending messages from callerIdentity.
              // See setCurrentSpeechItem on a subtlety around a race condition.
              startFlushingSpeechItems(callerIdentity);
  
              // This stops writing data to the file / or publishing
              // items to the audio playback handler.
              //
              // Note that the current speech item must be removed only if it
              // belongs to the callingApp, else the item will be "orphaned" and
              // not stopped correctly if a stop request comes along for the item
              // from the app it belongs to.
              SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity);
              if (current != null) {
                  current.stop();
              }
  
              // Remove any enqueued audio too.
              mAudioPlaybackHandler.stopForApp(callerIdentity);
  
              // Stop flushing pending messages
              Runnable runnable = new Runnable() {
                  @Override
                  public void run() {
                      endFlushingSpeechItems(callerIdentity);
                  }
              };
              sendMessage(Message.obtain(this, runnable));
              return TextToSpeech.SUCCESS;
          }
  
          public int stopAll() {
              // Order to flush pending messages
              startFlushingSpeechItems(null);
  
              // Stop the current speech item unconditionally .
              SpeechItem current = removeCurrentSpeechItem();
              if (current != null) {
                  current.stop();
              }
              // Remove all pending playback as well.
              mAudioPlaybackHandler.stop();
  
              // Message to stop flushing pending messages
              Runnable runnable = new Runnable() {
                  @Override
                  public void run() {
                      endFlushingSpeechItems(null);
                  }
              };
              sendMessage(Message.obtain(this, runnable));
  
  
              return TextToSpeech.SUCCESS;
          }
      }
  
      interface UtteranceProgressDispatcher {
          void dispatchOnStop();
  
          void dispatchOnSuccess();
  
          void dispatchOnStart();
  
          void dispatchOnError(int errorCode);
  
          void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount);
  
          void dispatchOnAudioAvailable(byte[] audio);
  
          public void dispatchOnRangeStart(int start, int end, int frame);
      }
  
      /** Set of parameters affecting audio output. */
      static class AudioOutputParams {
          /**
           * Audio session identifier. May be used to associate audio playback with one of the
           * {@link android.media.audiofx.AudioEffect} objects. If not specified by client,
           * it should be equal to {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
           */
          public final int mSessionId;
  
          /**
           * Volume, in the range [0.0f, 1.0f]. The default value is
           * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f).
           */
          public final float mVolume;
  
          /**
           * Left/right position of the audio, in the range [-1.0f, 1.0f].
           * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f).
           */
          public final float mPan;
  
  
          /**
           * Audio attributes, set by {@link TextToSpeech#setAudioAttributes}
           * or created from the value of {@link TextToSpeech.Engine#KEY_PARAM_STREAM}.
           */
          public final AudioAttributes mAudioAttributes;
  
          /** Create AudioOutputParams with default values */
          AudioOutputParams() {
              mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
              mVolume = Engine.DEFAULT_VOLUME;
              mPan = Engine.DEFAULT_PAN;
              mAudioAttributes = null;
          }
  
          AudioOutputParams(int sessionId, float volume, float pan,
                  AudioAttributes audioAttributes) {
              mSessionId = sessionId;
              mVolume = volume;
              mPan = pan;
              mAudioAttributes = audioAttributes;
          }
  
          /** Create AudioOutputParams from A {@link SynthesisRequest#getParams()} bundle */
          static AudioOutputParams createFromParamsBundle(Bundle paramsBundle, boolean isSpeech) {
              if (paramsBundle == null) {
                  return new AudioOutputParams();
              }
  
              AudioAttributes audioAttributes =
                      (AudioAttributes) paramsBundle.getParcelable(
                              Engine.KEY_PARAM_AUDIO_ATTRIBUTES);
              if (audioAttributes == null) {
                  int streamType = paramsBundle.getInt(
                          Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM);
                  audioAttributes = (new AudioAttributes.Builder())
                          .setLegacyStreamType(streamType)
                          .setContentType((isSpeech ?
                                  AudioAttributes.CONTENT_TYPE_SPEECH :
                                  AudioAttributes.CONTENT_TYPE_SONIFICATION))
                          .build();
              }
  
              return new AudioOutputParams(
                      paramsBundle.getInt(
                              Engine.KEY_PARAM_SESSION_ID,
                              AudioManager.AUDIO_SESSION_ID_GENERATE),
                      paramsBundle.getFloat(
                              Engine.KEY_PARAM_VOLUME,
                              Engine.DEFAULT_VOLUME),
                      paramsBundle.getFloat(
                              Engine.KEY_PARAM_PAN,
                              Engine.DEFAULT_PAN),
                      audioAttributes);
          }
      }
  
  
      /**
       * An item in the synth thread queue.
       */
      private abstract class SpeechItem {
          private final Object mCallerIdentity;
          private final int mCallerUid;
          private final int mCallerPid;
          private boolean mStarted = false;
          private boolean mStopped = false;
  
          public SpeechItem(Object caller, int callerUid, int callerPid) {
              mCallerIdentity = caller;
              mCallerUid = callerUid;
              mCallerPid = callerPid;
          }
  
          public Object getCallerIdentity() {
              return mCallerIdentity;
          }
  
          public int getCallerUid() {
              return mCallerUid;
          }
  
          public int getCallerPid() {
              return mCallerPid;
          }
  
          /**
           * Checker whether the item is valid. If this method returns false, the item should not
           * be played.
           */
          public abstract boolean isValid();
  
          /**
           * Plays the speech item. Blocks until playback is finished.
           * Must not be called more than once.
           *
           * Only called on the synthesis thread.
           */
          public void play() {
              synchronized (this) {
                  if (mStarted) {
                      throw new IllegalStateException("play() called twice");
                  }
                  mStarted = true;
              }
              playImpl();
          }
  
          protected abstract void playImpl();
  
          /**
           * Stops the speech item.
           * Must not be called more than once.
           *
           * Can be called on multiple threads,  but not on the synthesis thread.
           */
          public void stop() {
              synchronized (this) {
                  if (mStopped) {
                      throw new IllegalStateException("stop() called twice");
                  }
                  mStopped = true;
              }
              stopImpl();
          }
  
          protected abstract void stopImpl();
  
          protected synchronized boolean isStopped() {
               return mStopped;
          }
  
          protected synchronized boolean isStarted() {
              return mStarted;
         }
      }
  
      /**
       * An item in the synth thread queue that process utterance (and call back to client about
       * progress).
       */
      private abstract class UtteranceSpeechItem extends SpeechItem
          implements UtteranceProgressDispatcher  {
  
          public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) {
              super(caller, callerUid, callerPid);
          }
  
          @Override
          public void dispatchOnSuccess() {
              final String utteranceId = getUtteranceId();
              if (utteranceId != null) {
                  mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId);
              }
          }
  
          @Override
          public void dispatchOnStop() {
              final String utteranceId = getUtteranceId();
              if (utteranceId != null) {
                  mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted());
              }
          }
  
          @Override
          public void dispatchOnStart() {
              final String utteranceId = getUtteranceId();
              if (utteranceId != null) {
                  mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId);
              }
          }
  
          @Override
          public void dispatchOnError(int errorCode) {
              final String utteranceId = getUtteranceId();
              if (utteranceId != null) {
                  mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode);
              }
          }
  
          @Override
          public void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount) {
              final String utteranceId = getUtteranceId();
              if (utteranceId != null) {
                  mCallbacks.dispatchOnBeginSynthesis(getCallerIdentity(), utteranceId, sampleRateInHz, audioFormat, channelCount);
              }
          }
  
          @Override
          public void dispatchOnAudioAvailable(byte[] audio) {
              final String utteranceId = getUtteranceId();
              if (utteranceId != null) {
                  mCallbacks.dispatchOnAudioAvailable(getCallerIdentity(), utteranceId, audio);
              }
          }
  
          @Override
          public void dispatchOnRangeStart(int start, int end, int frame) {
              final String utteranceId = getUtteranceId();
              if (utteranceId != null) {
                  mCallbacks.dispatchOnRangeStart(
                          getCallerIdentity(), utteranceId, start, end, frame);
              }
          }
  
          abstract public String getUtteranceId();
  
          String getStringParam(Bundle params, String key, String defaultValue) {
              return params == null ? defaultValue : params.getString(key, defaultValue);
          }
  
          int getIntParam(Bundle params, String key, int defaultValue) {
              return params == null ? defaultValue : params.getInt(key, defaultValue);
          }
  
          float getFloatParam(Bundle params, String key, float defaultValue) {
              return params == null ? defaultValue : params.getFloat(key, defaultValue);
          }
      }
  
      /**
       * Synthesis parameters are kept in a single Bundle passed as parameter. This class allow
       * subclasses to access them conveniently.
       */
      private abstract class UtteranceSpeechItemWithParams extends UtteranceSpeechItem {
          protected final Bundle mParams;
          protected final String mUtteranceId;
  
          UtteranceSpeechItemWithParams(
                  Object callerIdentity,
                  int callerUid,
                  int callerPid,
                  Bundle params,
                  String utteranceId) {
              super(callerIdentity, callerUid, callerPid);
              mParams = params;
              mUtteranceId = utteranceId;
          }
  
          boolean hasLanguage() {
              return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null));
          }
  
          int getSpeechRate() {
              return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate());
          }
  
          int getPitch() {
              return getIntParam(mParams, Engine.KEY_PARAM_PITCH, getDefaultPitch());
          }
  
          @Override
          public String getUtteranceId() {
              return mUtteranceId;
          }
  
          AudioOutputParams getAudioParams() {
              return AudioOutputParams.createFromParamsBundle(mParams, true);
          }
      }
  
      class SynthesisSpeechItem extends UtteranceSpeechItemWithParams {
          // Never null.
          private final CharSequence mText;
          private final SynthesisRequest mSynthesisRequest;
          private final String[] mDefaultLocale;
          // Non null after synthesis has started, and all accesses
          // guarded by 'this'.
          private AbstractSynthesisCallback mSynthesisCallback;
          private final EventLogger mEventLogger;
          private final int mCallerUid;
  
          public SynthesisSpeechItem(
                  Object callerIdentity,
                  int callerUid,
                  int callerPid,
                  Bundle params,
                  String utteranceId,
                  CharSequence text) {
              super(callerIdentity, callerUid, callerPid, params, utteranceId);
              mText = text;
              mCallerUid = callerUid;
              mSynthesisRequest = new SynthesisRequest(mText, mParams);
              mDefaultLocale = getSettingsLocale();
              setRequestParams(mSynthesisRequest);
              mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid, mPackageName);
          }
  
          public CharSequence getText() {
              return mText;
          }
  
          @Override
          public boolean isValid() {
              if (mText == null) {
                  Log.e(TAG, "null synthesis text");
                  return false;
              }
              if (mText.length() > TextToSpeech.getMaxSpeechInputLength()) {
                  Log.w(TAG, "Text too long: " + mText.length() + " chars");
                  return false;
              }
              return true;
          }
  
          @Override
          protected void playImpl() {
              AbstractSynthesisCallback synthesisCallback;
              mEventLogger.onRequestProcessingStart();
              synchronized (this) {
                  // stop() might have been called before we enter this
                  // synchronized block.
                  if (isStopped()) {
                      return;
                  }
                  mSynthesisCallback = createSynthesisCallback();
                  synthesisCallback = mSynthesisCallback;
              }
  
              TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
  
              // Fix for case where client called .start() & .error(), but did not called .done()
              if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) {
                  synthesisCallback.done();
              }
          }
  
          protected AbstractSynthesisCallback createSynthesisCallback() {
              return new PlaybackSynthesisCallback(getAudioParams(),
                      mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false);
          }
  
          private void setRequestParams(SynthesisRequest request) {
              String voiceName = getVoiceName();
              request.setLanguage(getLanguage(), getCountry(), getVariant());
              if (!TextUtils.isEmpty(voiceName)) {
                  request.setVoiceName(getVoiceName());
              }
              request.setSpeechRate(getSpeechRate());
              request.setCallerUid(mCallerUid);
              request.setPitch(getPitch());
          }
  
          @Override
          protected void stopImpl() {
              AbstractSynthesisCallback synthesisCallback;
              synchronized (this) {
                  synthesisCallback = mSynthesisCallback;
              }
              if (synthesisCallback != null) {
                  // If the synthesis callback is null, it implies that we haven't
                  // entered the synchronized(this) block in playImpl which in
                  // turn implies that synthesis would not have started.
                  synthesisCallback.stop();
                  TextToSpeechService.this.onStop();
              } else {
                  dispatchOnStop();
              }
          }
  
          private String getCountry() {
              if (!hasLanguage()) return mDefaultLocale[1];
              return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, "");
          }
  
          private String getVariant() {
              if (!hasLanguage()) return mDefaultLocale[2];
              return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, "");
          }
  
          public String getLanguage() {
              return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]);
          }
  
          public String getVoiceName() {
              return getStringParam(mParams, Engine.KEY_PARAM_VOICE_NAME, "");
          }
      }
  
      private class SynthesisToFileOutputStreamSpeechItem extends SynthesisSpeechItem {
          private final FileOutputStream mFileOutputStream;
  
          public SynthesisToFileOutputStreamSpeechItem(
                  Object callerIdentity,
                  int callerUid,
                  int callerPid,
                  Bundle params,
                  String utteranceId,
                  CharSequence text,
                  FileOutputStream fileOutputStream) {
              super(callerIdentity, callerUid, callerPid, params, utteranceId, text);
              mFileOutputStream = fileOutputStream;
          }
  
          @Override
          protected AbstractSynthesisCallback createSynthesisCallback() {
              return new FileSynthesisCallback(mFileOutputStream.getChannel(), this, false);
          }
  
          @Override
          protected void playImpl() {
              super.playImpl();
              try {
                mFileOutputStream.close();
              } catch(IOException e) {
                Log.w(TAG, "Failed to close output file", e);
              }
          }
      }
  
      private class AudioSpeechItem extends UtteranceSpeechItemWithParams {
          private final AudioPlaybackQueueItem mItem;
  
          public AudioSpeechItem(
                  Object callerIdentity,
                  int callerUid,
                  int callerPid,
                  Bundle params,
                  String utteranceId,
                  Uri uri) {
              super(callerIdentity, callerUid, callerPid, params, utteranceId);
              mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(),
                      TextToSpeechService.this, uri, getAudioParams());
          }
  
          @Override
          public boolean isValid() {
              return true;
          }
  
          @Override
          protected void playImpl() {
              mAudioPlaybackHandler.enqueue(mItem);
          }
  
          @Override
          protected void stopImpl() {
              // Do nothing.
          }
  
          @Override
          public String getUtteranceId() {
              return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null);
          }
  
          @Override
          AudioOutputParams getAudioParams() {
              return AudioOutputParams.createFromParamsBundle(mParams, false);
          }
      }
  
      private class SilenceSpeechItem extends UtteranceSpeechItem {
          private final long mDuration;
          private final String mUtteranceId;
  
          public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid,
                  String utteranceId, long duration) {
              super(callerIdentity, callerUid, callerPid);
              mUtteranceId = utteranceId;
              mDuration = duration;
          }
  
          @Override
          public boolean isValid() {
              return true;
          }
  
          @Override
          protected void playImpl() {
              mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem(
                      this, getCallerIdentity(), mDuration));
          }
  
          @Override
          protected void stopImpl() {
  
          }
  
          @Override
          public String getUtteranceId() {
              return mUtteranceId;
          }
      }
  
      /**
       * Call {@link TextToSpeechService#onLoadLanguage} on synth thread.
       */
      private class LoadLanguageItem extends SpeechItem {
          private final String mLanguage;
          private final String mCountry;
          private final String mVariant;
  
          public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid,
                  String language, String country, String variant) {
              super(callerIdentity, callerUid, callerPid);
              mLanguage = language;
              mCountry = country;
              mVariant = variant;
          }
  
          @Override
          public boolean isValid() {
              return true;
          }
  
          @Override
          protected void playImpl() {
              TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant);
          }
  
          @Override
          protected void stopImpl() {
              // No-op
          }
      }
  
      /**
       * Call {@link TextToSpeechService#onLoadLanguage} on synth thread.
       */
      private class LoadVoiceItem extends SpeechItem {
          private final String mVoiceName;
  
          public LoadVoiceItem(Object callerIdentity, int callerUid, int callerPid,
                  String voiceName) {
              super(callerIdentity, callerUid, callerPid);
              mVoiceName = voiceName;
          }
  
          @Override
          public boolean isValid() {
              return true;
          }
  
          @Override
          protected void playImpl() {
              TextToSpeechService.this.onLoadVoice(mVoiceName);
          }
  
          @Override
          protected void stopImpl() {
              // No-op
          }
      }
  
  
      @Override
      public IBinder onBind(Intent intent) {
          if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
              return mBinder;
          }
          return null;
      }
  
      /**
       * Binder returned from {@code #onBind(Intent)}. The methods in this class can be called called
       * from several different threads.
       */
      // NOTE: All calls that are passed in a calling app are interned so that
      // they can be used as message objects (which are tested for equality using ==).
      private final ITextToSpeechService.Stub mBinder =
              new ITextToSpeechService.Stub() {
                  @Override
                  public int speak(
                          IBinder caller,
                          CharSequence text,
                          int queueMode,
                          Bundle params,
                          String utteranceId) {
                      if (!checkNonNull(caller, text, params)) {
                          return TextToSpeech.ERROR;
                      }
  
                      SpeechItem item =
                              new SynthesisSpeechItem(
                                      caller,
                                      Binder.getCallingUid(),
                                      Binder.getCallingPid(),
                                      params,
                                      utteranceId,
                                      text);
                      return mSynthHandler.enqueueSpeechItem(queueMode, item);
                  }
  
                  @Override
                  public int synthesizeToFileDescriptor(
                          IBinder caller,
                          CharSequence text,
                          ParcelFileDescriptor fileDescriptor,
                          Bundle params,
                          String utteranceId) {
                      if (!checkNonNull(caller, text, fileDescriptor, params)) {
                          return TextToSpeech.ERROR;
                      }
  
                      // In test env, ParcelFileDescriptor instance may be EXACTLY the same
                      // one that is used by client. And it will be closed by a client, thus
                      // preventing us from writing anything to it.
                      final ParcelFileDescriptor sameFileDescriptor =
                              ParcelFileDescriptor.adoptFd(fileDescriptor.detachFd());
  
                      SpeechItem item =
                              new SynthesisToFileOutputStreamSpeechItem(
                                      caller,
                                      Binder.getCallingUid(),
                                      Binder.getCallingPid(),
                                      params,
                                      utteranceId,
                                      text,
                                      new ParcelFileDescriptor.AutoCloseOutputStream(
                                              sameFileDescriptor));
                      return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
                  }
  
                  @Override
                  public int playAudio(
                          IBinder caller,
                          Uri audioUri,
                          int queueMode,
                          Bundle params,
                          String utteranceId) {
                      if (!checkNonNull(caller, audioUri, params)) {
                          return TextToSpeech.ERROR;
                      }
  
                      SpeechItem item =
                              new AudioSpeechItem(
                                      caller,
                                      Binder.getCallingUid(),
                                      Binder.getCallingPid(),
                                      params,
                                      utteranceId,
                                      audioUri);
                      return mSynthHandler.enqueueSpeechItem(queueMode, item);
                  }
  
                  @Override
                  public int playSilence(
                          IBinder caller, long duration, int queueMode, String utteranceId) {
                      if (!checkNonNull(caller)) {
                          return TextToSpeech.ERROR;
                      }
  
                      SpeechItem item =
                              new SilenceSpeechItem(
                                      caller,
                                      Binder.getCallingUid(),
                                      Binder.getCallingPid(),
                                      utteranceId,
                                      duration);
                      return mSynthHandler.enqueueSpeechItem(queueMode, item);
                  }
  
                  @Override
                  public boolean isSpeaking() {
                      return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking();
                  }
  
                  @Override
                  public int stop(IBinder caller) {
                      if (!checkNonNull(caller)) {
                          return TextToSpeech.ERROR;
                      }
  
                      return mSynthHandler.stopForApp(caller);
                  }
  
                  @Override
                  public String[] getLanguage() {
                      return onGetLanguage();
                  }
  
                  @Override
                  public String[] getClientDefaultLanguage() {
                      return getSettingsLocale();
                  }
  
                  /*
                   * If defaults are enforced, then no language is "available" except
                   * perhaps the default language selected by the user.
                   */
                  @Override
                  public int isLanguageAvailable(String lang, String country, String variant) {
                      if (!checkNonNull(lang)) {
                          return TextToSpeech.ERROR;
                      }
  
                      return onIsLanguageAvailable(lang, country, variant);
                  }
  
                  @Override
                  public String[] getFeaturesForLanguage(
                          String lang, String country, String variant) {
                      Set<String> features = onGetFeaturesForLanguage(lang, country, variant);
                      String[] featuresArray = null;
                      if (features != null) {
                          featuresArray = new String[features.size()];
                          features.toArray(featuresArray);
                      } else {
                          featuresArray = new String[0];
                      }
                      return featuresArray;
                  }
  
                  /*
                   * There is no point loading a non default language if defaults
                   * are enforced.
                   */
                  @Override
                  public int loadLanguage(
                          IBinder caller, String lang, String country, String variant) {
                      if (!checkNonNull(lang)) {
                          return TextToSpeech.ERROR;
                      }
                      int retVal = onIsLanguageAvailable(lang, country, variant);
  
                      if (retVal == TextToSpeech.LANG_AVAILABLE
                              || retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE
                              || retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
  
                          SpeechItem item =
                                  new LoadLanguageItem(
                                          caller,
                                          Binder.getCallingUid(),
                                          Binder.getCallingPid(),
                                          lang,
                                          country,
                                          variant);
  
                          if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item)
                                  != TextToSpeech.SUCCESS) {
                              return TextToSpeech.ERROR;
                          }
                      }
                      return retVal;
                  }
  
                  @Override
                  public List<Voice> getVoices() {
                      return onGetVoices();
                  }
  
                  @Override
                  public int loadVoice(IBinder caller, String voiceName) {
                      if (!checkNonNull(voiceName)) {
                          return TextToSpeech.ERROR;
                      }
                      int retVal = onIsValidVoiceName(voiceName);
  
                      if (retVal == TextToSpeech.SUCCESS) {
                          SpeechItem item =
                                  new LoadVoiceItem(
                                          caller,
                                          Binder.getCallingUid(),
                                          Binder.getCallingPid(),
                                          voiceName);
                          if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item)
                                  != TextToSpeech.SUCCESS) {
                              return TextToSpeech.ERROR;
                          }
                      }
                      return retVal;
                  }
  
                  public String getDefaultVoiceNameFor(String lang, String country, String variant) {
                      if (!checkNonNull(lang)) {
                          return null;
                      }
                      int retVal = onIsLanguageAvailable(lang, country, variant);
  
                      if (retVal == TextToSpeech.LANG_AVAILABLE
                              || retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE
                              || retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
                          return onGetDefaultVoiceNameFor(lang, country, variant);
                      } else {
                          return null;
                      }
                  }
  
                  @Override
                  public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
                      // Note that passing in a null callback is a valid use case.
                      if (!checkNonNull(caller)) {
                          return;
                      }
  
                      mCallbacks.setCallback(caller, cb);
                  }
  
                  private String intern(String in) {
                      // The input parameter will be non null.
                      return in.intern();
                  }
  
                  private boolean checkNonNull(Object... args) {
                      for (Object o : args) {
                          if (o == null) return false;
                      }
                      return true;
                  }
              };
  
      private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
          private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback
                  = new HashMap<IBinder, ITextToSpeechCallback>();
  
          public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
              synchronized (mCallerToCallback) {
                  ITextToSpeechCallback old;
                  if (cb != null) {
                      register(cb, caller);
                      old = mCallerToCallback.put(caller, cb);
                  } else {
                      old = mCallerToCallback.remove(caller);
                  }
                  if (old != null && old != cb) {
                      unregister(old);
                  }
              }
          }
  
          public void dispatchOnStop(Object callerIdentity, String utteranceId, boolean started) {
              ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
              if (cb == null) return;
              try {
                  cb.onStop(utteranceId, started);
              } catch (RemoteException e) {
                  Log.e(TAG, "Callback onStop failed: " + e);
              }
          }
  
          public void dispatchOnSuccess(Object callerIdentity, String utteranceId) {
              ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
              if (cb == null) return;
              try {
                  cb.onSuccess(utteranceId);
              } catch (RemoteException e) {
                  Log.e(TAG, "Callback onDone failed: " + e);
              }
          }
  
          public void dispatchOnStart(Object callerIdentity, String utteranceId) {
              ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
              if (cb == null) return;
              try {
                  cb.onStart(utteranceId);
              } catch (RemoteException e) {
                  Log.e(TAG, "Callback onStart failed: " + e);
              }
          }
  
          public void dispatchOnError(Object callerIdentity, String utteranceId,
                  int errorCode) {
              ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
              if (cb == null) return;
              try {
                  cb.onError(utteranceId, errorCode);
              } catch (RemoteException e) {
                  Log.e(TAG, "Callback onError failed: " + e);
              }
          }
  
          public void dispatchOnBeginSynthesis(Object callerIdentity, String utteranceId, int sampleRateInHz, int audioFormat, int channelCount) {
              ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
              if (cb == null) return;
              try {
                  cb.onBeginSynthesis(utteranceId, sampleRateInHz, audioFormat, channelCount);
              } catch (RemoteException e) {
                  Log.e(TAG, "Callback dispatchOnBeginSynthesis(String, int, int, int) failed: " + e);
              }
          }
  
          public void dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer) {
              ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
              if (cb == null) return;
              try {
                  cb.onAudioAvailable(utteranceId, buffer);
              } catch (RemoteException e) {
                  Log.e(TAG, "Callback dispatchOnAudioAvailable(String, byte[]) failed: " + e);
              }
          }
  
          public void dispatchOnRangeStart(
                  Object callerIdentity, String utteranceId, int start, int end, int frame) {
              ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
              if (cb == null) return;
              try {
                  cb.onRangeStart(utteranceId, start, end, frame);
              } catch (RemoteException e) {
                  Log.e(TAG, "Callback dispatchOnRangeStart(String, int, int, int) failed: " + e);
              }
          }
  
          @Override
          public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
              IBinder caller = (IBinder) cookie;
              synchronized (mCallerToCallback) {
                  mCallerToCallback.remove(caller);
              }
              mSynthHandler.stopForApp(caller);
          }
  
          @Override
          public void kill() {
              synchronized (mCallerToCallback) {
                  mCallerToCallback.clear();
                  super.kill();
              }
          }
  
          private ITextToSpeechCallback getCallbackFor(Object caller) {
              ITextToSpeechCallback cb;
              IBinder asBinder = (IBinder) caller;
              synchronized (mCallerToCallback) {
                  cb = mCallerToCallback.get(asBinder);
              }
  
              return cb;
          }
      }
  }
  