/*
* 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.app.Service;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioSystem;
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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
/**
* Abstract base class for TTS engine implementations. The following methods
* need to be implemented:
*
* - {@link #onIsLanguageAvailable}
* - {@link #onLoadLanguage}
* - {@link #onGetLanguage}
* - {@link #onSynthesizeText}
* - {@link #onStop}
*
* 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:
*
* - {@link #onGetVoices()}
* - {@link #onIsValidVoiceName(String)}
* - {@link #onLoadVoice(String)}
* - {@link #onGetDefaultVoiceNameFor(String, String, String)}
*
* 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.
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. Used for requests from V1
* clients ({@link android.speech.tts.TextToSpeech}). 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 onGetFeaturesForLanguage(String lang, String country, String variant) {
return null;
}
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 onGetVoices() {
// Enumerate all locales and check if they are available
ArrayList voices = new ArrayList();
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 features = onGetFeaturesForLanguage(locale.getISO3Language(),
locale.getISO3Country(), locale.getVariant());
voices.add(new Voice(locale.toLanguageTag(), 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 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;
public SynthHandler(Looper looper) {
super(looper);
}
private synchronized SpeechItem getCurrentSpeechItem() {
return mCurrentSpeechItem;
}
private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) {
SpeechItem old = mCurrentSpeechItem;
mCurrentSpeechItem = speechItem;
return old;
}
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 = setCurrentSpeechItem(null);
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() {
setCurrentSpeechItem(speechItem);
speechItem.play();
setCurrentSpeechItem(null);
}
};
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(Object callerIdentity) {
if (callerIdentity == null) {
return TextToSpeech.ERROR;
}
removeCallbacksAndMessages(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);
return TextToSpeech.SUCCESS;
}
public int stopAll() {
// Stop the current speech item unconditionally .
SpeechItem current = setCurrentSpeechItem(null);
if (current != null) {
current.stop();
}
// Remove all other items from the queue.
removeCallbacksAndMessages(null);
// Remove all pending playback as well.
mAudioPlaybackHandler.stop();
return TextToSpeech.SUCCESS;
}
}
interface UtteranceProgressDispatcher {
public void dispatchOnStop();
public void dispatchOnSuccess();
public void dispatchOnStart();
public void dispatchOnError(int errorCode);
}
/** 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 AudioSystem#AUDIO_SESSION_ALLOCATE}.
*/
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 = AudioSystem.AUDIO_SESSION_ALLOCATE;
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 createFromV1ParamsBundle(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,
AudioSystem.AUDIO_SESSION_ALLOCATE),
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;
}
}
/**
* 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);
}
}
@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);
}
}
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);
}
}
/**
* UtteranceSpeechItem for V1 API speech items. V1 API speech items keep
* synthesis parameters in a single Bundle passed as parameter. This class
* allow subclasses to access them conveniently.
*/
private abstract class SpeechItemV1 extends UtteranceSpeechItem {
protected final Bundle mParams;
protected final String mUtteranceId;
SpeechItemV1(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, Engine.DEFAULT_PITCH);
}
@Override
public String getUtteranceId() {
return mUtteranceId;
}
AudioOutputParams getAudioParams() {
return AudioOutputParams.createFromV1ParamsBundle(mParams, true);
}
}
class SynthesisSpeechItemV1 extends SpeechItemV1 {
// 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 EventLoggerV1 mEventLogger;
private final int mCallerUid;
public SynthesisSpeechItemV1(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 EventLoggerV1(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();
}
}
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 SynthesisToFileOutputStreamSpeechItemV1 extends SynthesisSpeechItemV1 {
private final FileOutputStream mFileOutputStream;
public SynthesisToFileOutputStreamSpeechItemV1(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, getCallerIdentity(), false);
}
@Override
protected void playImpl() {
dispatchOnStart();
super.playImpl();
try {
mFileOutputStream.close();
} catch(IOException e) {
Log.w(TAG, "Failed to close output file", e);
}
}
}
private class AudioSpeechItemV1 extends SpeechItemV1 {
private final AudioPlaybackQueueItem mItem;
public AudioSpeechItemV1(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.createFromV1ParamsBundle(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 SynthesisSpeechItemV1(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 SynthesisToFileOutputStreamSpeechItemV1(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 AudioSpeechItemV1(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 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 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 {
private final HashMap mCallerToCallback
= new HashMap();
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) {
ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
if (cb == null) return;
try {
cb.onStop(utteranceId);
} 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);
}
}
@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;
}
}
}