/*
* Copyright (C) 2015 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 com.android.utils;
import com.android.talkback.SpeechController;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.provider.Settings.Secure;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.Engine;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
import android.text.TextUtils;
import android.util.Log;
import com.android.utils.compat.provider.SettingsCompatUtils.SecureCompatUtils;
import com.android.utils.compat.speech.tts.TextToSpeechCompatUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Wrapper for {@link TextToSpeech} that handles fail-over when a specific
* engine does not work.
* <p>
* Does <strong>NOT</strong> implement queuing! Every call to {@link #speak}
* flushes the global speech queue.
* <p>
* This wrapper handles the following:
* <ul>
* <li>Fail-over from a failing TTS to a working one
* <li>Splitting utterances into <4k character chunks
* <li>Switching to the system TTS when media is unmounted
* <li>Utterance-specific pitch and rate changes
* <li>Pitch and rate changes relative to the user preference
* </ul>
*/
@SuppressWarnings("deprecation")
public class FailoverTextToSpeech {
private static final String TAG = "FailoverTextToSpeech";
/** The package name for the Google TTS engine. */
private static final String PACKAGE_GOOGLE_TTS = "com.google.android.tts";
/** Number of times a TTS engine can fail before switching. */
private static final int MAX_TTS_FAILURES = 3;
/** Maximum number of TTS error messages to print to the log. */
private static final int MAX_LOG_MESSAGES = 10;
/**
* Constant to flush speech globally.
* The constant corresponds to the non-public API {@link TextToSpeech#QUEUE_DESTROY}.
* To avoid a bug, we always need to use {@link TextToSpeech#QUEUE_FLUSH} before using
* {@link #SPEECH_FLUSH_ALL} -- on Android version M only.
**/
private static final int SPEECH_FLUSH_ALL = 2;
/**
* {@link BroadcastReceiver} for determining changes in the media state used
* for switching the TTS engine.
*/
private final MediaMountStateMonitor mMediaStateMonitor = new MediaMountStateMonitor();
/** A list of installed TTS engines. */
private final LinkedList<String> mInstalledTtsEngines = new LinkedList<>();
private final Context mContext;
private final ContentResolver mResolver;
/** The TTS engine. */
private TextToSpeech mTts;
/** The engine loaded into the current TTS. */
private String mTtsEngine;
/** The number of time the current TTS has failed consecutively. */
private int mTtsFailures;
/** The package name of the preferred TTS engine. */
private String mDefaultTtsEngine;
/** The package name of the system TTS engine. */
private String mSystemTtsEngine;
/** A temporary TTS used for switching engines. */
private TextToSpeech mTempTts;
/** The engine loading into the temporary TTS. */
private String mTempTtsEngine;
/** The rate adjustment specified in {@link android.provider.Settings}. */
private float mDefaultRate;
/** The pitch adjustment specified in {@link android.provider.Settings}. */
private float mDefaultPitch;
/** The most recent rate sent to {@link TextToSpeech#setSpeechRate}. */
private float mCurrentRate = 1.0f;
/** The most recent pitch sent to {@link TextToSpeech#setSpeechRate}. */
private float mCurrentPitch = 1.0f;
private List<FailoverTtsListener> mListeners = new ArrayList<>();
public FailoverTextToSpeech(Context context) {
mContext = context;
mContext.registerReceiver(mMediaStateMonitor, mMediaStateMonitor.getFilter());
final Uri defaultSynth = Secure.getUriFor(Secure.TTS_DEFAULT_SYNTH);
final Uri defaultPitch = Secure.getUriFor(Secure.TTS_DEFAULT_PITCH);
final Uri defaultRate = Secure.getUriFor(Secure.TTS_DEFAULT_RATE);
mResolver = context.getContentResolver();
mResolver.registerContentObserver(defaultSynth, false, mSynthObserver);
mResolver.registerContentObserver(defaultPitch, false, mPitchObserver);
mResolver.registerContentObserver(defaultRate, false, mRateObserver);
registerGoogleTtsFixCallbacks();
updateDefaultPitch();
updateDefaultRate();
// Updating the default engine reloads the list of installed engines and
// the system engine. This also loads the default engine.
updateDefaultEngine();
}
/**
* Add a new listener for changes in speaking state.
*
* @param listener The listener to add.
*/
public void addListener(FailoverTtsListener listener) {
mListeners.add(listener);
}
/**
* Whether the text-to-speech engine is ready to speak.
*
* @return {@code true} if calling {@link #speak} is expected to succeed.
*/
public boolean isReady() {
return (mTts != null);
}
/**
* Returns the label for the current text-to-speech engine.
*
* @return The localized name of the current engine.
*/
public CharSequence getEngineLabel() {
return TextToSpeechUtils.getLabelForEngine(mContext, mTtsEngine);
}
/**
* Returns the {@link TextToSpeech} instance that is currently being used as the engine.
*
* @return The engine instance.
*/
@SuppressWarnings("UnusedDeclaration") // Used by analytics
public TextToSpeech getEngineInstance() {
return mTts;
}
/**
* Speak the specified text.
*
* @param text The text to speak.
* @param pitch The pitch adjustment, in the range [0 ... 1].
* @param rate The rate adjustment, in the range [0 ... 1].
* @param params The parameters to pass to the text-to-speech engine.
*/
public void speak(CharSequence text, float pitch, float rate, HashMap<String, String> params,
int stream, float volume) {
// Handle empty text immediately.
if (TextUtils.isEmpty(text)) {
mHandler.onUtteranceCompleted(params.get(Engine.KEY_PARAM_UTTERANCE_ID));
return;
}
int result;
Exception failureException = null;
try {
result = trySpeak(text, pitch, rate, params, stream, volume);
} catch (Exception e) {
failureException = e;
result = TextToSpeech.ERROR;
}
if (result == TextToSpeech.ERROR) {
attemptTtsFailover(mTtsEngine);
}
if ((result != TextToSpeech.SUCCESS)
&& params.containsKey(Engine.KEY_PARAM_UTTERANCE_ID)) {
if (failureException != null) {
if(LogUtils.LOG_LEVEL <= Log.WARN) {
Log.w(TAG, "Failed to speak " + text + " due to an exception");
}
failureException.printStackTrace();
} else {
if (LogUtils.LOG_LEVEL <= Log.WARN) {
Log.w(TAG, "Failed to speak " + text);
}
}
mHandler.onUtteranceCompleted(params.get(Engine.KEY_PARAM_UTTERANCE_ID));
}
}
/**
* Stops speech from all applications. No utterance callbacks will be sent.
*/
public void stopAll() {
try {
ensureQueueFlush();
mTts.speak("", SPEECH_FLUSH_ALL, null);
} catch (Exception e) {
// Don't care, we're not speaking.
}
}
/**
* Stops all speech that originated from TalkBack. No utterance callbacks will be sent.
*/
public void stopFromTalkBack() {
try {
mTts.speak("", TextToSpeech.QUEUE_FLUSH, null);
} catch (Exception e) {
// Don't care, we're not speaking.
}
}
/**
* Unregisters receivers, observers, and shuts down the text-to-speech
* engine. No calls should be made to this object after calling this method.
*/
public void shutdown() {
mContext.unregisterReceiver(mMediaStateMonitor);
unregisterGoogleTtsFixCallbacks();
mResolver.unregisterContentObserver(mSynthObserver);
mResolver.unregisterContentObserver(mPitchObserver);
mResolver.unregisterContentObserver(mRateObserver);
TextToSpeechUtils.attemptTtsShutdown(mTts);
mTts = null;
TextToSpeechUtils.attemptTtsShutdown(mTempTts);
mTempTts = null;
}
/**
* Attempts to speak the specified text.
*
* @param text to speak, must be under 3999 chars.
* @param pitch to speak text in.
* @param rate to speak text in.
* @param params to the TTS.
* @return The result of speaking the specified text.
*/
@SuppressWarnings("unused")
private int trySpeak(CharSequence text, float pitch, float rate, HashMap<String, String> params,
int stream, float volume) {
if (mTts == null) {
return TextToSpeech.ERROR;
}
float effectivePitch = (pitch * mDefaultPitch);
float effectiveRate = (rate * mDefaultRate);
int result;
String utteranceId = params.get(Engine.KEY_PARAM_UTTERANCE_ID);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) {
result = speakApi21(text, params, utteranceId, effectivePitch, effectiveRate, stream,
volume);
} else {
// Set the pitch and rate only if necessary, since that is slow.
if ((mCurrentPitch != effectivePitch) || (mCurrentRate != effectiveRate)) {
mTts.stop();
mTts.setPitch(effectivePitch);
mTts.setSpeechRate(effectiveRate);
}
result = mTts.speak(text.toString(), SPEECH_FLUSH_ALL, params);
}
mCurrentPitch = effectivePitch;
mCurrentRate = effectiveRate;
if (result != TextToSpeech.SUCCESS) {
ensureSupportedLocale();
}
if (LogUtils.LOG_LEVEL < Log.DEBUG) {
Log.d(TAG, "Speak call for " + utteranceId + " returned " + result);
}
return result;
}
@TargetApi(21)
private int speakApi21(CharSequence text, HashMap<String, String> params, String utteranceId,
float pitch, float rate, int stream, float volume) {
Bundle bundle = new Bundle();
if (params != null) {
for (String key : params.keySet()) {
bundle.putString(key, params.get(key));
}
}
bundle.putInt(SpeechController.SpeechParam.PITCH, (int)(pitch*100));
bundle.putInt(SpeechController.SpeechParam.RATE, (int)(rate*100));
bundle.putInt(Engine.KEY_PARAM_STREAM, stream);
bundle.putFloat(SpeechController.SpeechParam.VOLUME, volume);
ensureQueueFlush();
return mTts.speak(text, SPEECH_FLUSH_ALL, bundle, utteranceId);
}
/**
* Flushes the TextToSpeech queue for fast speech queueing, needed only on Android M.
* See bug BUG
*/
@TargetApi(21)
private void ensureQueueFlush() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
mTts.speak("", TextToSpeech.QUEUE_FLUSH, null, null);
}
}
/**
* Try to switch the TTS engine.
*
* @param engine The package name of the desired TTS engine
*/
private void setTtsEngine(String engine, boolean resetFailures) {
if (resetFailures) {
mTtsFailures = 0;
}
// Always try to stop the current engine before switching.
TextToSpeechUtils.attemptTtsShutdown(mTts);
if (mTempTts != null) {
LogUtils.log(SpeechController.class, Log.ERROR,
"Can't start TTS engine %s while still loading previous engine", engine);
return;
}
LogUtils.logWithLimit(SpeechController.class, Log.INFO, mTtsFailures, MAX_LOG_MESSAGES,
"Switching to TTS engine: %s", engine);
mTempTtsEngine = engine;
mTempTts = new TextToSpeech(mContext, mTtsChangeListener, engine);
}
/**
* Assumes the current engine has failed and attempts to start the next
* available engine.
*
* @param failedEngine The package name of the engine to switch from.
*/
private void attemptTtsFailover(String failedEngine) {
LogUtils.logWithLimit(SpeechController.class, Log.ERROR, mTtsFailures, MAX_LOG_MESSAGES,
"Attempting TTS failover from %s", failedEngine);
mTtsFailures++;
// If there is only one installed engine, or if the current engine
// hasn't failed enough times, just restart the current engine.
if ((mInstalledTtsEngines.size() <= 1) || (mTtsFailures < MAX_TTS_FAILURES)) {
setTtsEngine(failedEngine, false);
return;
}
// Move the engine to the back of the list.
if (failedEngine != null) {
mInstalledTtsEngines.remove(failedEngine);
mInstalledTtsEngines.addLast(failedEngine);
}
// Try to use the first available TTS engine.
final String nextEngine = mInstalledTtsEngines.getFirst();
setTtsEngine(nextEngine, true);
}
/**
* Handles TTS engine initialization.
*
* @param status The status returned by the TTS engine.
*/
@SuppressWarnings("deprecation")
private void handleTtsInitialized(int status) {
if (mTempTts == null) {
LogUtils.log(this, Log.ERROR, "Attempted to initialize TTS more than once!");
return;
}
final TextToSpeech tempTts = mTempTts;
final String tempTtsEngine = mTempTtsEngine;
mTempTts = null;
mTempTtsEngine = null;
if (status != TextToSpeech.SUCCESS) {
attemptTtsFailover(tempTtsEngine);
return;
}
final boolean isSwitchingEngines = (mTts != null);
if (isSwitchingEngines) {
TextToSpeechUtils.attemptTtsShutdown(mTts);
}
mTts = tempTts;
mTts.setOnUtteranceCompletedListener(mTtsListener);
if (tempTtsEngine == null) {
mTtsEngine = TextToSpeechCompatUtils.getCurrentEngine(mTts);
} else {
mTtsEngine = tempTtsEngine;
}
updateDefaultLocale();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) {
setAudioAttributesApi21();
}
LogUtils.log(SpeechController.class, Log.INFO, "Switched to TTS engine: %s", tempTtsEngine);
for (FailoverTtsListener mListener : mListeners) {
mListener.onTtsInitialized(isSwitchingEngines);
}
}
@TargetApi(21)
private void setAudioAttributesApi21() {
mTts.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
.build());
}
/**
* Method that's called by TTS whenever an utterance is completed. Do common
* tasks and execute any UtteranceCompleteActions associate with this
* utterance index (or an earlier index, in case one was accidentally
* dropped).
*
* @param utteranceId The utteranceId from the onUtteranceCompleted callback
* - we expect this to consist of UTTERANCE_ID_PREFIX followed by
* the utterance index.
* @param success {@code true} if the utterance was spoken successfully.
*/
private void handleUtteranceCompleted(String utteranceId, boolean success) {
if (success) {
mTtsFailures = 0;
}
for (FailoverTtsListener mListener : mListeners) {
mListener.onUtteranceCompleted(utteranceId, success);
}
}
/**
* Handles media state changes.
*
* @param action The current media state.
*/
private void handleMediaStateChanged(String action) {
if (Intent.ACTION_MEDIA_UNMOUNTED.equals(action)) {
if (!TextUtils.equals(mSystemTtsEngine, mTtsEngine)) {
// Temporarily switch to the system TTS engine.
LogUtils.log(this, Log.VERBOSE, "Saw media unmount");
setTtsEngine(mSystemTtsEngine, true);
}
}
if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
if (!TextUtils.equals(mDefaultTtsEngine, mTtsEngine)) {
// Try to switch back to the default engine.
LogUtils.log(this, Log.VERBOSE, "Saw media mount");
setTtsEngine(mDefaultTtsEngine, true);
}
}
}
public void updateDefaultEngine() {
final ContentResolver resolver = mContext.getContentResolver();
// Always refresh the list of available engines, since the user may have
// installed a new TTS and then switched to it.
mInstalledTtsEngines.clear();
mSystemTtsEngine = TextToSpeechUtils.reloadInstalledTtsEngines(
mContext.getPackageManager(), mInstalledTtsEngines);
// This may be null if the user hasn't specified an engine.
mDefaultTtsEngine = Secure.getString(resolver, Secure.TTS_DEFAULT_SYNTH);
// Switch engines when the system default changes and it's not the current engine.
if (mTtsEngine == null || !mTtsEngine.equals(mDefaultTtsEngine)) {
if (mInstalledTtsEngines.contains(mDefaultTtsEngine)) {
// Can load the default engine.
setTtsEngine(mDefaultTtsEngine, true);
} else if (!mInstalledTtsEngines.isEmpty()) {
// We'll take whatever TTS we can get.
setTtsEngine(mInstalledTtsEngines.get(0), true);
}
}
}
/**
* Loads the default pitch adjustment from {@link Secure#TTS_DEFAULT_PITCH}.
* This will take effect during the next call to {@link #trySpeak}.
*/
private void updateDefaultPitch() {
mDefaultPitch = (Secure.getInt(mResolver, Secure.TTS_DEFAULT_PITCH, 100) / 100.0f);
}
/**
* Loads the default rate adjustment from {@link Secure#TTS_DEFAULT_RATE}.
* This will take effect during the next call to {@link #trySpeak}.
*/
private void updateDefaultRate() {
mDefaultRate = (Secure.getInt(mResolver, Secure.TTS_DEFAULT_RATE, 100) / 100.0f);
}
/** Whether we need to always force locale changes through TTS. */
private static final boolean FORCE_TTS_LOCALE_CHANGES =
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2);
/**
* Preferred locale for fallback language.
*/
private static final Locale PREFERRED_FALLBACK_LOCALE = Locale.US;
/**
* The system's default locale.
*/
private Locale mSystemLocale = Locale.getDefault();
/**
* The current engine's default locale. This will be {@code null} if the
* user never specified a preference.
*/
private Locale mDefaultLocale = null;
/**
* Whether we're using a fallback locale because the TTS attempted to use an
* unsupported locale.
*/
private boolean mUsingFallbackLocale;
/**
* Whether we've ever explicitly set the locale using
* {@link TextToSpeech#setLanguage}. If so, we'll need to work around a TTS
* bug and manually update the TTS locale every time the user changes
* locale-related settings.
*/
private boolean mHasSetLocale;
/**
* Helper method that ensures the text-to-speech engine works even when the
* user is using the Google TTS and has the system set to a non-embedded
* language.
* <p>
* This method should be called whenever the TTS engine is
* loaded, the system locale changes, or the default TTS locale changes.
*/
private void ensureSupportedLocale() {
if (needsFallbackLocale()) {
attemptSetFallbackLanguage();
} else if (mUsingFallbackLocale || mHasSetLocale || FORCE_TTS_LOCALE_CHANGES) {
// We might need to restore the system locale. Or, if we've ever
// explicitly set the locale, we'll need to work around a bug where
// there's no way to tell the TTS engine to use whatever it thinks
// the default language should be.
attemptRestorePreferredLocale();
}
}
/**
* Returns whether we need to attempt to use a fallback language.
*/
private boolean needsFallbackLocale() {
// If the user isn't using Google TTS, or if they set a preferred
// locale, we do not need to check locale support.
if (!PACKAGE_GOOGLE_TTS.equals(mTtsEngine) || (mDefaultLocale != null)) {
return false;
}
if (mTts == null) {
return false;
}
// Otherwise, the TTS engine will attempt to use the system locale which
// may not be supported. If the locale is embedded or advertised as
// available, we're fine.
final Set<String> features = mTts.getFeatures(mSystemLocale);
return !(((features != null)
&& features.contains(Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS))
|| !isNotAvailableStatus(mTts.isLanguageAvailable(mSystemLocale)));
}
/**
* Attempts to obtain and set a fallback TTS locale.
*/
private void attemptSetFallbackLanguage() {
final Locale fallbackLocale = getBestAvailableLocale();
if (fallbackLocale == null) {
LogUtils.log(this, Log.ERROR, "Failed to find fallback locale");
return;
}
if (mTts == null) {
LogUtils.log(this, Log.ERROR, "mTts null when setting fallback locale.");
return;
}
final int status = mTts.setLanguage(fallbackLocale);
if (isNotAvailableStatus(status)) {
LogUtils.log(this, Log.ERROR, "Failed to set fallback locale to %s", fallbackLocale);
return;
}
LogUtils.log(this, Log.VERBOSE, "Set fallback locale to %s", fallbackLocale);
mUsingFallbackLocale = true;
mHasSetLocale = true;
}
/**
* Attempts to obtain a supported TTS locale with preference given to
* {@link #PREFERRED_FALLBACK_LOCALE}. The resulting locale may not be
* optimal for the user, but it will likely be enough to understand what's
* on the screen.
*/
private Locale getBestAvailableLocale() {
if (mTts == null) {
return null;
}
// Always attempt to use the preferred locale first.
if (mTts.isLanguageAvailable(PREFERRED_FALLBACK_LOCALE) >= 0) {
return PREFERRED_FALLBACK_LOCALE;
}
// Since there's no way to query available languages from an engine,
// we'll need to check every locale supported by the device.
Locale bestLocale = null;
int bestScore = -1;
final Locale[] locales = Locale.getAvailableLocales();
for (Locale locale : locales) {
final int status = mTts.isLanguageAvailable(locale);
if (isNotAvailableStatus(status)) {
continue;
}
final int score = compareLocales(mSystemLocale, locale);
if (score > bestScore) {
bestLocale = locale;
bestScore = score;
}
}
return bestLocale;
}
/**
* Attempts to restore the user's preferred TTS locale, if set. Otherwise
* attempts to restore the system locale.
*/
private void attemptRestorePreferredLocale() {
if (mTts == null) {
return;
}
final Locale preferredLocale = (mDefaultLocale != null ? mDefaultLocale : mSystemLocale);
final int status = mTts.setLanguage(preferredLocale);
if (isNotAvailableStatus(status)) {
LogUtils.log(this, Log.ERROR, "Failed to restore TTS locale to %s", preferredLocale);
return;
}
LogUtils.log(this, Log.INFO, "Restored TTS locale to %s", preferredLocale);
mUsingFallbackLocale = false;
mHasSetLocale = true;
}
/**
* Handles updating the default locale.
*/
private void updateDefaultLocale() {
final String defaultLocale = TextToSpeechUtils.getDefaultLocaleForEngine(
mResolver, mTtsEngine);
mDefaultLocale = (!TextUtils.isEmpty(defaultLocale)) ? new Locale(defaultLocale) : null;
// The default locale changed, which may mean we can restore the user's
// preferred locale.
ensureSupportedLocale();
}
/**
* Handles updating the system locale.
*/
private void onConfigurationChanged(Configuration newConfig) {
final Locale newLocale = newConfig.locale;
if (newLocale.equals(mSystemLocale)) {
return;
}
mSystemLocale = newLocale;
// The system locale changed, which may mean we need to override the
// current TTS locale.
ensureSupportedLocale();
}
/**
* Registers the configuration change callback.
*/
private void registerGoogleTtsFixCallbacks() {
final Uri defaultLocaleUri = Secure.getUriFor(SecureCompatUtils.TTS_DEFAULT_LOCALE);
mResolver.registerContentObserver(defaultLocaleUri, false, mLocaleObserver);
mContext.registerComponentCallbacks(mComponentCallbacks);
}
/**
* Unregisters the configuration change callback.
*/
private void unregisterGoogleTtsFixCallbacks() {
mResolver.unregisterContentObserver(mLocaleObserver);
mContext.unregisterComponentCallbacks(mComponentCallbacks);
}
/**
* Compares a locale against a primary locale. Returns higher values for
* closer matches. A return value of 3 indicates that the locale is an exact
* match for the primary locale's language, country, and variant.
*
* @param primary The primary locale for comparison.
* @param other The other locale to compare against the primary locale.
* @return A value indicating how well the other locale matches the primary
* locale. Higher is better.
*/
private static int compareLocales(Locale primary, Locale other) {
final String lang = primary.getLanguage();
if ((lang == null) || !lang.equals(other.getLanguage())) {
return 0;
}
final String country = primary.getCountry();
if ((country == null) || !country.equals(other.getCountry())) {
return 1;
}
final String variant = primary.getVariant();
if ((variant == null) || !variant.equals(other.getVariant())) {
return 2;
}
return 3;
}
/**
* Returns {@code true} if the specified status indicates that the language
* is available.
*
* @param status A language availability code, as returned from
* {@link TextToSpeech#isLanguageAvailable}.
* @return {@code true} if the status indicates that the language is
* available.
*/
private static boolean isNotAvailableStatus(int status) {
return (status != TextToSpeech.LANG_AVAILABLE)
&& (status != TextToSpeech.LANG_COUNTRY_AVAILABLE)
&& (status != TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE);
}
private final FailoverTextToSpeech.SpeechHandler mHandler = new SpeechHandler(this);
/**
* Handles changes to the default TTS engine.
*/
private final ContentObserver mSynthObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
updateDefaultEngine();
}
};
private final ContentObserver mPitchObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
updateDefaultPitch();
}
};
private final ContentObserver mRateObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
updateDefaultRate();
}
};
/**
* Callbacks used to observe changes to the TTS locale.
*/
private final ContentObserver mLocaleObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
updateDefaultLocale();
}
};
/** Hands utterance completed processing to the main thread. */
private final OnUtteranceCompletedListener mTtsListener = new OnUtteranceCompletedListener() {
@Override
public void onUtteranceCompleted(String utteranceId) {
LogUtils.log(this, Log.DEBUG, "Received completion for \"%s\"", utteranceId);
mHandler.onUtteranceCompleted(utteranceId);
}
};
/**
* When changing TTS engines, switches the active TTS engine when the new
* engine is initialized.
*/
private final OnInitListener mTtsChangeListener = new OnInitListener() {
@Override
public void onInit(int status) {
mHandler.onTtsInitialized(status);
}
};
/**
* Callbacks used to observe configuration changes.
*/
private final ComponentCallbacks mComponentCallbacks = new ComponentCallbacks() {
@Override
public void onLowMemory() {
// Do nothing.
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
FailoverTextToSpeech.this.onConfigurationChanged(newConfig);
}
};
/**
* {@link BroadcastReceiver} for detecting media mount and unmount.
*/
private class MediaMountStateMonitor extends BroadcastReceiver {
private final IntentFilter mMediaIntentFilter;
public MediaMountStateMonitor() {
mMediaIntentFilter = new IntentFilter();
mMediaIntentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
mMediaIntentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
mMediaIntentFilter.addDataScheme("file");
}
public IntentFilter getFilter() {
return mMediaIntentFilter;
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
mHandler.onMediaStateChanged(action);
}
}
/** Handler used to return to the main thread from the TTS thread. */
private static class SpeechHandler extends WeakReferenceHandler<FailoverTextToSpeech> {
/** Hand-off engine initialized. */
private static final int MSG_INITIALIZED = 1;
/** Hand-off utterance completed. */
private static final int MSG_UTTERANCE_COMPLETED = 2;
/** Hand-off media state changes. */
private static final int MSG_MEDIA_STATE_CHANGED = 3;
public SpeechHandler(FailoverTextToSpeech parent) {
super(parent);
}
@Override
public void handleMessage(Message msg, FailoverTextToSpeech parent) {
switch (msg.what) {
case MSG_INITIALIZED:
parent.handleTtsInitialized(msg.arg1);
break;
case MSG_UTTERANCE_COMPLETED:
parent.handleUtteranceCompleted((String) msg.obj, true);
break;
case MSG_MEDIA_STATE_CHANGED:
parent.handleMediaStateChanged((String) msg.obj);
}
}
public void onTtsInitialized(int status) {
obtainMessage(MSG_INITIALIZED, status, 0).sendToTarget();
}
public void onUtteranceCompleted(String utteranceId) {
obtainMessage(MSG_UTTERANCE_COMPLETED, utteranceId).sendToTarget();
}
public void onMediaStateChanged(String action) {
obtainMessage(MSG_MEDIA_STATE_CHANGED, action).sendToTarget();
}
}
/**
* Listener for TTS events.
*/
public interface FailoverTtsListener {
/*
* Called after the class has initialized with a tts engine.
*/
public void onTtsInitialized(boolean wasSwitchingEngines);
/*
* Called after an utterance has completed speaking.
*/
public void onUtteranceCompleted(String utteranceId, boolean success);
}
}