Add support for voices in TTS API.
Voices allow to expose multiple backends/voice packs for a single
Locale. This is an attempt to port this feature from V2 API.
Bug: 15834470
Change-Id: I0117de238cfcf028bcec5344b8d65c960b96b98c
diff --git a/api/current.txt b/api/current.txt
index fb598fd..4eea35d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -26609,6 +26609,7 @@
method public int getSpeechRate();
method public deprecated java.lang.String getText();
method public java.lang.String getVariant();
+ method public java.lang.String getVoiceName();
}
public class TextToSpeech {
@@ -26620,13 +26621,17 @@
method public int addSpeech(java.lang.CharSequence, java.lang.String, int);
method public int addSpeech(java.lang.String, java.lang.String);
method public int addSpeech(java.lang.CharSequence, java.lang.String);
- method public boolean areDefaultsEnforced();
+ method public deprecated boolean areDefaultsEnforced();
+ method public java.util.Set<java.util.Locale> getAvailableLanguages();
method public java.lang.String getDefaultEngine();
- method public java.util.Locale getDefaultLanguage();
+ method public deprecated java.util.Locale getDefaultLanguage();
+ method public android.speech.tts.Voice getDefaultVoice();
method public java.util.List<android.speech.tts.TextToSpeech.EngineInfo> getEngines();
- method public java.util.Set<java.lang.String> getFeatures(java.util.Locale);
- method public java.util.Locale getLanguage();
+ method public deprecated java.util.Set<java.lang.String> getFeatures(java.util.Locale);
+ method public deprecated java.util.Locale getLanguage();
method public static int getMaxSpeechInputLength();
+ method public android.speech.tts.Voice getVoice();
+ method public java.util.Set<android.speech.tts.Voice> getVoices();
method public int isLanguageAvailable(java.util.Locale);
method public boolean isSpeaking();
method public int playEarcon(java.lang.String, int, java.util.HashMap<java.lang.String, java.lang.String>, java.lang.String);
@@ -26639,6 +26644,7 @@
method public int setOnUtteranceProgressListener(android.speech.tts.UtteranceProgressListener);
method public int setPitch(float);
method public int setSpeechRate(float);
+ method public int setVoice(android.speech.tts.Voice);
method public void shutdown();
method public int speak(java.lang.CharSequence, int, java.util.HashMap<java.lang.String, java.lang.String>, java.lang.String);
method public deprecated int speak(java.lang.String, int, java.util.HashMap<java.lang.String, java.lang.String>);
@@ -26650,6 +26656,7 @@
field public static final int ERROR_INVALID_REQUEST = -8; // 0xfffffff8
field public static final int ERROR_NETWORK = -6; // 0xfffffffa
field public static final int ERROR_NETWORK_TIMEOUT = -7; // 0xfffffff9
+ field public static final int ERROR_NOT_INSTALLED_YET = -9; // 0xfffffff7
field public static final int ERROR_OUTPUT = -5; // 0xfffffffb
field public static final int ERROR_SERVICE = -4; // 0xfffffffc
field public static final int ERROR_SYNTHESIS = -3; // 0xfffffffd
@@ -26685,8 +26692,11 @@
field public static final deprecated java.lang.String EXTRA_VOICE_DATA_FILES_INFO = "dataFilesInfo";
field public static final deprecated java.lang.String EXTRA_VOICE_DATA_ROOT_DIRECTORY = "dataRoot";
field public static final java.lang.String INTENT_ACTION_TTS_SERVICE = "android.intent.action.TTS_SERVICE";
- field public static final java.lang.String KEY_FEATURE_EMBEDDED_SYNTHESIS = "embeddedTts";
- field public static final java.lang.String KEY_FEATURE_NETWORK_SYNTHESIS = "networkTts";
+ field public static final deprecated java.lang.String KEY_FEATURE_EMBEDDED_SYNTHESIS = "embeddedTts";
+ field public static final java.lang.String KEY_FEATURE_NETWORK_RETRIES_COUNT = "networkRetriesCount";
+ field public static final deprecated java.lang.String KEY_FEATURE_NETWORK_SYNTHESIS = "networkTts";
+ field public static final java.lang.String KEY_FEATURE_NETWORK_TIMEOUT_MS = "networkTimeoutMs";
+ field public static final java.lang.String KEY_FEATURE_NOT_INSTALLED = "notInstalled";
field public static final java.lang.String KEY_PARAM_PAN = "pan";
field public static final java.lang.String KEY_PARAM_SESSION_ID = "sessionId";
field public static final java.lang.String KEY_PARAM_STREAM = "streamType";
@@ -26712,11 +26722,15 @@
public abstract class TextToSpeechService extends android.app.Service {
ctor public TextToSpeechService();
+ method protected int isValidVoiceName(java.lang.String);
method public android.os.IBinder onBind(android.content.Intent);
+ method protected java.lang.String onGetDefaultVoiceNameFor(java.lang.String, java.lang.String, java.lang.String);
method protected java.util.Set<java.lang.String> onGetFeaturesForLanguage(java.lang.String, java.lang.String, java.lang.String);
method protected abstract java.lang.String[] onGetLanguage();
+ method protected java.util.List<android.speech.tts.Voice> onGetVoices();
method protected abstract int onIsLanguageAvailable(java.lang.String, java.lang.String, java.lang.String);
method protected abstract int onLoadLanguage(java.lang.String, java.lang.String, java.lang.String);
+ method protected int onLoadVoice(java.lang.String);
method protected abstract void onStop();
method protected abstract void onSynthesizeText(android.speech.tts.SynthesisRequest, android.speech.tts.SynthesisCallback);
}
@@ -26808,6 +26822,28 @@
method public abstract void onStart(java.lang.String);
}
+ public class Voice implements android.os.Parcelable {
+ ctor public Voice(java.lang.String, java.util.Locale, int, int, boolean, java.util.Set<java.lang.String>);
+ method public int describeContents();
+ method public java.util.Set<java.lang.String> getFeatures();
+ method public int getLatency();
+ method public java.util.Locale getLocale();
+ method public java.lang.String getName();
+ method public int getQuality();
+ method public boolean getRequiresNetworkConnection();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final int LATENCY_HIGH = 400; // 0x190
+ field public static final int LATENCY_LOW = 200; // 0xc8
+ field public static final int LATENCY_NORMAL = 300; // 0x12c
+ field public static final int LATENCY_VERY_HIGH = 500; // 0x1f4
+ field public static final int LATENCY_VERY_LOW = 100; // 0x64
+ field public static final int QUALITY_HIGH = 400; // 0x190
+ field public static final int QUALITY_LOW = 200; // 0xc8
+ field public static final int QUALITY_NORMAL = 300; // 0x12c
+ field public static final int QUALITY_VERY_HIGH = 500; // 0x1f4
+ field public static final int QUALITY_VERY_LOW = 100; // 0x64
+ }
+
}
package android.system {
diff --git a/core/java/android/speech/tts/ITextToSpeechService.aidl b/core/java/android/speech/tts/ITextToSpeechService.aidl
index 694f25a..4faa67f 100644
--- a/core/java/android/speech/tts/ITextToSpeechService.aidl
+++ b/core/java/android/speech/tts/ITextToSpeechService.aidl
@@ -20,6 +20,7 @@
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.speech.tts.ITextToSpeechCallback;
+import android.speech.tts.Voice;
/**
* Interface for TextToSpeech to talk to TextToSpeechService.
@@ -173,4 +174,37 @@
* @param cb The callback.
*/
void setCallback(in IBinder caller, ITextToSpeechCallback cb);
+
+ /**
+ * Get the array of available voices.
+ */
+ List<Voice> getVoices();
+
+ /**
+ * Notifies the engine that it should load a speech synthesis voice.
+ *
+ * @param caller a binder representing the identity of the calling
+ * TextToSpeech object.
+ * @param voiceName Unique voice of the name.
+ * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}.
+ */
+ int loadVoice(in IBinder caller, in String voiceName);
+
+ /**
+ * Return a name of the default voice for a given locale.
+ *
+ * This allows {@link TextToSpeech#getVoice} to return a sensible value after a client calls
+ * {@link TextToSpeech#setLanguage}.
+ *
+ * @param lang ISO 3-character language code.
+ * @param country ISO 3-character 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}.
+ */
+ String getDefaultVoiceNameFor(in String lang, in String country, in String variant);
}
diff --git a/core/java/android/speech/tts/SynthesisRequest.java b/core/java/android/speech/tts/SynthesisRequest.java
index eaacc06..d41aa67 100644
--- a/core/java/android/speech/tts/SynthesisRequest.java
+++ b/core/java/android/speech/tts/SynthesisRequest.java
@@ -18,12 +18,15 @@
import android.os.Bundle;
/**
- * Contains data required by engines to synthesize speech. This data is :
+ * Contains data required by engines to synthesize speech. This data is:
* <ul>
* <li>The text to synthesize</li>
* <li>The synthesis locale, represented as a language, country and a variant.
* The language is an ISO 639-3 letter language code, and the country is an
* ISO 3166 alpha 3 code. The variant is not specified.</li>
+ * <li>The name of the voice requested for this synthesis. May be empty if
+ * the client uses {@link TextToSpeech#setLanguage} instead of
+ * {@link TextToSpeech#setVoice}</li>
* <li>The synthesis speech rate, with 100 being the normal, and
* higher values representing higher speech rates.</li>
* <li>The voice pitch, with 100 being the default pitch.</li>
@@ -36,6 +39,7 @@
public final class SynthesisRequest {
private final CharSequence mText;
private final Bundle mParams;
+ private String mVoiceName;
private String mLanguage;
private String mCountry;
private String mVariant;
@@ -72,6 +76,13 @@
}
/**
+ * Gets the name of the voice to use.
+ */
+ public String getVoiceName() {
+ return mVoiceName;
+ }
+
+ /**
* Gets the ISO 3-letter language code for the language to use.
*/
public String getLanguage() {
@@ -130,6 +141,13 @@
}
/**
+ * Sets the voice name for the request.
+ */
+ void setVoiceName(String voiceName) {
+ mVoiceName = voiceName;
+ }
+
+ /**
* Sets the speech rate.
*/
void setSpeechRate(int speechRate) {
diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java
index e1c1767b..ac9044a 100644
--- a/core/java/android/speech/tts/TextToSpeech.java
+++ b/core/java/android/speech/tts/TextToSpeech.java
@@ -36,6 +36,7 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -44,6 +45,7 @@
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
+import java.util.TreeSet;
/**
*
@@ -104,6 +106,12 @@
public static final int ERROR_INVALID_REQUEST = -8;
/**
+ * Denotes a failure caused by an unfinished download of the voice data.
+ * @see Engine#KEY_FEATURE_NOT_INSTALLED
+ */
+ public static final int ERROR_NOT_INSTALLED_YET = -9;
+
+ /**
* Queue mode where all entries in the playback queue (media to be played
* and text to be synthesized) are dropped and replaced by the new entry.
* Queues are flushed with respect to a given calling app. Entries in the queue
@@ -478,6 +486,11 @@
/**
* @hide
*/
+ public static final String KEY_PARAM_VOICE_NAME = "voiceName";
+
+ /**
+ * @hide
+ */
public static final String KEY_PARAM_LANGUAGE = "language";
/**
@@ -550,7 +563,13 @@
* @see TextToSpeech#speak(String, int, java.util.HashMap)
* @see TextToSpeech#synthesizeToFile(String, java.util.HashMap, String)
* @see TextToSpeech#getFeatures(java.util.Locale)
+ *
+ * @deprecated Starting from API level 20, to select network synthesis, call
+ * ({@link TextToSpeech#getVoices()}, find a suitable network voice
+ * ({@link Voice#getRequiresNetworkConnection()}) and pass it
+ * to {@link TextToSpeech#setVoice(Voice)}).
*/
+ @Deprecated
public static final String KEY_FEATURE_NETWORK_SYNTHESIS = "networkTts";
/**
@@ -562,7 +581,13 @@
* @see TextToSpeech#speak(String, int, java.util.HashMap)
* @see TextToSpeech#synthesizeToFile(String, java.util.HashMap, String)
* @see TextToSpeech#getFeatures(java.util.Locale)
+
+ * @deprecated Starting from API level 20, to select embedded synthesis, call
+ * ({@link TextToSpeech#getVoices()}, find a suitable embedded voice
+ * ({@link Voice#getRequiresNetworkConnection()}) and pass it
+ * to {@link TextToSpeech#setVoice(Voice)}).
*/
+ @Deprecated
public static final String KEY_FEATURE_EMBEDDED_SYNTHESIS = "embeddedTts";
/**
@@ -575,6 +600,43 @@
* @see TextToSpeech#playEarcon(String, int, HashMap)
*/
public static final String KEY_PARAM_SESSION_ID = "sessionId";
+
+ /**
+ * Feature key that indicates that the voice may need to download additional data to be fully
+ * functional. The download will be triggered by calling
+ * {@link TextToSpeech#setVoice(Voice)} or {@link TextToSpeech#setLanguage(Locale)}.
+ * Until download is complete, each synthesis request will either report
+ * {@link TextToSpeech#ERROR_NOT_INSTALLED_YET} error, or use a different voice to synthesize
+ * the request. This feature should NOT be used as a key of a request parameter.
+ *
+ * @see TextToSpeech#getFeatures(java.util.Locale)
+ * @see Voice#getFeatures()
+ */
+ public static final String KEY_FEATURE_NOT_INSTALLED = "notInstalled";
+
+ /**
+ * Feature key that indicate that a network timeout can be set for the request. If set and
+ * supported as per {@link TextToSpeech#getFeatures(Locale)} or {@link Voice#getFeatures()},
+ * it can be used as request parameter to set the maximum allowed time for a single
+ * request attempt, in milliseconds, before synthesis fails. When used as a key of
+ * a request parameter, its value should be a string with an integer value.
+ *
+ * @see TextToSpeech#getFeatures(java.util.Locale)
+ * @see Voice#getFeatures()
+ */
+ public static final String KEY_FEATURE_NETWORK_TIMEOUT_MS = "networkTimeoutMs";
+
+ /**
+ * Feature key that indicates that network request retries count can be set for the request.
+ * If set and supported as per {@link TextToSpeech#getFeatures(Locale)} or
+ * {@link Voice#getFeatures()}, it can be used as a request parameter to set the
+ * number of network request retries that are attempted in case of failure. When used as
+ * a key of a request parameter, its value should be a string with an integer value.
+ *
+ * @see TextToSpeech#getFeatures(java.util.Locale)
+ * @see Voice#getFeatures()
+ */
+ public static final String KEY_FEATURE_NETWORK_RETRIES_COUNT = "networkRetriesCount";
}
private final Context mContext;
@@ -596,7 +658,6 @@
private final Map<CharSequence, Uri> mUtterances;
private final Bundle mParams = new Bundle();
private final TtsEngines mEnginesHelper;
- private final String mPackageName;
private volatile String mCurrentEngine = null;
/**
@@ -648,11 +709,6 @@
mUtteranceProgressListener = null;
mEnginesHelper = new TtsEngines(mContext);
- if (packageName != null) {
- mPackageName = packageName;
- } else {
- mPackageName = mContext.getPackageName();
- }
initTts();
}
@@ -1186,12 +1242,16 @@
* {@link TextToSpeech#speak(String, int, java.util.HashMap)} and
* {@link TextToSpeech#synthesizeToFile(String, java.util.HashMap, String)}.
*
- * Features are boolean flags, and their values in the synthesis parameters
- * must be behave as per {@link Boolean#parseBoolean(String)}.
+ * Features values are strings and their values must meet restrictions described in their
+ * documentation.
*
* @param locale The locale to query features for.
* @return Set instance. May return {@code null} on error.
+ * @deprecated As of API level 20, please use voices. In order to query features of the voice,
+ * call {@link #getVoices()} to retrieve the list of available voices and
+ * {@link Voice#getFeatures()} to retrieve the set of features.
*/
+ @Deprecated
public Set<String> getFeatures(final Locale locale) {
return runAction(new Action<Set<String>>() {
@Override
@@ -1308,9 +1368,15 @@
* Returns a Locale instance describing the language currently being used as the default
* Text-to-speech language.
*
+ * The locale object returned by this method is NOT a valid one. It has identical form to the
+ * one in {@link #getLanguage()}. Please refer to {@link #getLanguage()} for more information.
+ *
* @return language, country (if any) and variant (if any) used by the client stored in a
* Locale instance, or {@code null} on error.
+ * @deprecated As of API Level 20, use <code>getDefaultVoice().getLocale()</code> ({@link
+ * #getDefaultVoice()})
*/
+ @Deprecated
public Locale getDefaultLanguage() {
return runAction(new Action<Locale>() {
@Override
@@ -1329,6 +1395,9 @@
* will be used. Use {@link #isLanguageAvailable(Locale)} to check the level of support
* before choosing the language to use for the next utterances.
*
+ * This method sets the current voice to the default one for the given Locale;
+ * {@link #getVoice()} can be used to retrieve it.
+ *
* @param loc The locale describing the language to be used.
*
* @return Code indicating the support status for the locale. See {@link #LANG_AVAILABLE},
@@ -1359,12 +1428,12 @@
String variant = loc.getVariant();
- // Check if the language, country, variant are available, and cache
- // the available parts.
- // Note that the language is not actually set here, instead it is cached so it
- // will be associated with all upcoming utterances.
+ // As of API level 20, setLanguage is implemented using setVoice.
+ // (which, in the default implementation, will call loadLanguage on the service
+ // interface).
- int result = service.loadLanguage(getCallerIdentity(), language, country, variant);
+ // Sanitize locale using isLanguageAvailable.
+ int result = service.isLanguageAvailable( language, country, variant);
if (result >= LANG_AVAILABLE){
if (result < LANG_COUNTRY_VAR_AVAILABLE) {
variant = "";
@@ -1372,6 +1441,20 @@
country = "";
}
}
+ // Get the default voice for the locale.
+ String voiceName = service.getDefaultVoiceNameFor(language, country, variant);
+ if (TextUtils.isEmpty(voiceName)) {
+ Log.w(TAG, "Couldn't find the default voice for " + language + "/" +
+ country + "/" + variant);
+ return LANG_NOT_SUPPORTED;
+ }
+
+ // Load it.
+ if (service.loadVoice(getCallerIdentity(), voiceName) == TextToSpeech.ERROR) {
+ return LANG_NOT_SUPPORTED;
+ }
+
+ mParams.putString(Engine.KEY_PARAM_VOICE_NAME, voiceName);
mParams.putString(Engine.KEY_PARAM_LANGUAGE, language);
mParams.putString(Engine.KEY_PARAM_COUNTRY, country);
mParams.putString(Engine.KEY_PARAM_VARIANT, variant);
@@ -1393,9 +1476,21 @@
* used for the synthesis requests sent from this client. That is the last language set
* by a {@link TextToSpeech#setLanguage} call on this instance.
*
+ * If a voice is set (by {@link #setVoice(Voice)}), getLanguage will return the language of
+ * the currently set voice.
+ *
+ * Please note that the Locale object returned by this method is NOT a valid Locale object. Its
+ * language field contains a three-letter ISO 639-2/T code (where a proper Locale would use
+ * a two-letter ISO 639-1 code), and the country field contains a three-letter ISO 3166 country
+ * code (where a proper Locale would use a two-letter ISO 3166-1 code).
+ *
* @return language, country (if any) and variant (if any) used by the client stored in a
* Locale instance, or {@code null} on error.
+ *
+ * @deprecated As of API level 20, please use <code>getVoice().getLocale()</code>
+ * ({@link #getVoice()}).
*/
+ @Deprecated
public Locale getLanguage() {
return runAction(new Action<Locale>() {
@Override
@@ -1411,6 +1506,178 @@
}
/**
+ * Query the engine about the set of available languages.
+ */
+ public Set<Locale> getAvailableLanguages() {
+ return runAction(new Action<Set<Locale>>() {
+ @Override
+ public Set<Locale> run(ITextToSpeechService service) throws RemoteException {
+ List<Voice> voices = service.getVoices();
+ if (voices != null) {
+ return new TreeSet<Locale>();
+ }
+ TreeSet<Locale> locales = new TreeSet<Locale>();
+ for (Voice voice : voices) {
+ locales.add(voice.getLocale());
+ }
+ return locales;
+ }
+ }, null, "getAvailableLanguages");
+ }
+
+ /**
+ * Query the engine about the set of available voices.
+ *
+ * Each TTS Engine can expose multiple voices for each locale, each with a different set of
+ * features.
+ *
+ * @see #setVoice(Voice)
+ * @see Voice
+ */
+ public Set<Voice> getVoices() {
+ return runAction(new Action<Set<Voice>>() {
+ @Override
+ public Set<Voice> run(ITextToSpeechService service) throws RemoteException {
+ List<Voice> voices = service.getVoices();
+ return (voices != null) ? new TreeSet<Voice>(voices) : new TreeSet<Voice>();
+ }
+ }, null, "getVoices");
+ }
+
+ /**
+ * Sets the text-to-speech voice.
+ *
+ * @param voice One of objects returned by {@link #getVoices()}.
+ *
+ * @return {@link #ERROR} or {@link #SUCCESS}.
+ *
+ * @see #getVoices
+ * @see Voice
+ */
+ public int setVoice(final Voice voice) {
+ return runAction(new Action<Integer>() {
+ @Override
+ public Integer run(ITextToSpeechService service) throws RemoteException {
+ int result = service.loadVoice(getCallerIdentity(), voice.getName());
+ if (result == SUCCESS) {
+ mParams.putString(Engine.KEY_PARAM_VOICE_NAME, voice.getName());
+
+ // Set the language/country/variant, so #getLanguage will return the voice
+ // locale when called.
+ String language = "";
+ try {
+ language = voice.getLocale().getISO3Language();
+ } catch (MissingResourceException e) {
+ Log.w(TAG, "Couldn't retrieve ISO 639-2/T language code for locale: " +
+ voice.getLocale(), e);
+ }
+
+ String country = "";
+ try {
+ country = voice.getLocale().getISO3Country();
+ } catch (MissingResourceException e) {
+ Log.w(TAG, "Couldn't retrieve ISO 3166 country code for locale: " +
+ voice.getLocale(), e);
+ }
+ mParams.putString(Engine.KEY_PARAM_LANGUAGE, language);
+ mParams.putString(Engine.KEY_PARAM_COUNTRY, country);
+ mParams.putString(Engine.KEY_PARAM_VARIANT, voice.getLocale().getVariant());
+ }
+ return result;
+ }
+ }, LANG_NOT_SUPPORTED, "setVoice");
+ }
+
+ /**
+ * Returns a Voice instance describing the voice currently being used for synthesis
+ * requests sent to the TextToSpeech engine.
+ *
+ * @return Voice instance used by the client, or {@code null} if not set or on error.
+ *
+ * @see #getVoices
+ * @see #setVoice
+ * @see Voice
+ */
+ public Voice getVoice() {
+ return runAction(new Action<Voice>() {
+ @Override
+ public Voice run(ITextToSpeechService service) throws RemoteException {
+ String voiceName = mParams.getString(Engine.KEY_PARAM_VOICE_NAME, "");
+ if (TextUtils.isEmpty(voiceName)) {
+ return null;
+ }
+ List<Voice> voices = service.getVoices();
+ if (voices == null) {
+ return null;
+ }
+ for (Voice voice : voices) {
+ if (voice.getName().equals(voiceName)) {
+ return voice;
+ }
+ }
+ return null;
+ }
+ }, null, "getVoice");
+ }
+
+ /**
+ * Returns a Voice instance that's the default voice for the default Text-to-speech language.
+ * @return The default voice instance for the default language, or {@code null} if not set or
+ * on error.
+ */
+ public Voice getDefaultVoice() {
+ return runAction(new Action<Voice>() {
+ @Override
+ public Voice run(ITextToSpeechService service) throws RemoteException {
+
+ String[] defaultLanguage = service.getClientDefaultLanguage();
+
+ if (defaultLanguage == null || defaultLanguage.length == 0) {
+ Log.e(TAG, "service.getClientDefaultLanguage() returned empty array");
+ return null;
+ }
+ String language = defaultLanguage[0];
+ String country = (defaultLanguage.length > 1) ? defaultLanguage[1] : "";
+ String variant = (defaultLanguage.length > 2) ? defaultLanguage[2] : "";
+
+ // Sanitize the locale using isLanguageAvailable.
+ int result = service.isLanguageAvailable(language, country, variant);
+ if (result >= LANG_AVAILABLE){
+ if (result < LANG_COUNTRY_VAR_AVAILABLE) {
+ variant = "";
+ if (result < LANG_COUNTRY_AVAILABLE) {
+ country = "";
+ }
+ }
+ } else {
+ // The default language is not supported.
+ return null;
+ }
+
+ // Get the default voice name
+ String voiceName = service.getDefaultVoiceNameFor(language, country, variant);
+ if (TextUtils.isEmpty(voiceName)) {
+ return null;
+ }
+
+ // Find it
+ List<Voice> voices = service.getVoices();
+ if (voices == null) {
+ return null;
+ }
+ for (Voice voice : voices) {
+ if (voice.getName().equals(voiceName)) {
+ return voice;
+ }
+ }
+ return null;
+ }
+ }, null, "getDefaultVoice");
+ }
+
+
+
+ /**
* Checks if the specified language as represented by the Locale is available and supported.
*
* @param loc The Locale describing the language to be used.
@@ -1538,6 +1805,8 @@
// Copy feature strings defined by the framework.
copyStringParam(bundle, params, Engine.KEY_FEATURE_NETWORK_SYNTHESIS);
copyStringParam(bundle, params, Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
+ copyIntParam(bundle, params, Engine.KEY_FEATURE_NETWORK_TIMEOUT_MS);
+ copyIntParam(bundle, params, Engine.KEY_FEATURE_NETWORK_RETRIES_COUNT);
// Copy over all parameters that start with the name of the
// engine that we are currently connected to. The engine is
@@ -1653,6 +1922,7 @@
* by the calling application. As of the Ice cream sandwich release,
* user settings never forcibly override the app's settings.
*/
+ @Deprecated
public boolean areDefaultsEnforced() {
return false;
}
diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java
index 017be93..ecfb8e0 100644
--- a/core/java/android/speech/tts/TextToSpeechService.java
+++ b/core/java/android/speech/tts/TextToSpeechService.java
@@ -39,6 +39,7 @@
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;
@@ -49,7 +50,7 @@
/**
* Abstract base class for TTS engine implementations. The following methods
- * need to be implemented for V1 API ({@link TextToSpeech}) implementation.
+ * need to be implemented:
* <ul>
* <li>{@link #onIsLanguageAvailable}</li>
* <li>{@link #onLoadLanguage}</li>
@@ -76,6 +77,29 @@
*
* {@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 #isValidVoiceName(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 20 {@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 {
@@ -228,6 +252,160 @@
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.
+ */
+ protected 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());
+ 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.
+ */
+ protected 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 (isValidVoiceName(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}.
+ */
+ protected 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}.
+ */
+ protected int isValidVoiceName(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);
}
@@ -736,7 +914,11 @@
}
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());
@@ -770,6 +952,10 @@
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 {
@@ -896,6 +1082,35 @@
}
}
+ /**
+ * 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())) {
@@ -1042,6 +1257,44 @@
}
@Override
+ public List<Voice> getVoices() {
+ return onGetVoices();
+ }
+
+ @Override
+ public int loadVoice(IBinder caller, String voiceName) {
+ if (!checkNonNull(voiceName)) {
+ return TextToSpeech.ERROR;
+ }
+ int retVal = isValidVoiceName(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)) {
diff --git a/core/java/android/speech/tts/TtsEngines.java b/core/java/android/speech/tts/TtsEngines.java
index 7474efe..df6c010 100644
--- a/core/java/android/speech/tts/TtsEngines.java
+++ b/core/java/android/speech/tts/TtsEngines.java
@@ -427,6 +427,36 @@
}
/**
+ * This method tries its best to return a valid {@link Locale} object from the TTS-specific
+ * Locale input (returned by {@link TextToSpeech#getLanguage}
+ * and {@link TextToSpeech#getDefaultLanguage}). A TTS Locale language field contains
+ * a three-letter ISO 639-2/T code (where a proper Locale would use a two-letter ISO 639-1
+ * code), and the country field contains a three-letter ISO 3166 country code (where a proper
+ * Locale would use a two-letter ISO 3166-1 code).
+ *
+ * This method tries to convert three-letter language and country codes into their two-letter
+ * equivalents. If it fails to do so, it keeps the value from the TTS locale.
+ */
+ public static Locale normalizeTTSLocale(Locale ttsLocale) {
+ String language = ttsLocale.getLanguage();
+ if (!TextUtils.isEmpty(language)) {
+ String normalizedLanguage = sNormalizeLanguage.get(language);
+ if (normalizedLanguage != null) {
+ language = normalizedLanguage;
+ }
+ }
+
+ String country = ttsLocale.getCountry();
+ if (!TextUtils.isEmpty(country)) {
+ String normalizedCountry= sNormalizeCountry.get(country);
+ if (normalizedCountry != null) {
+ country = normalizedCountry;
+ }
+ }
+ return new Locale(language, country, ttsLocale.getVariant());
+ }
+
+ /**
* Return the old-style string form of the locale. It consists of 3 letter codes:
* <ul>
* <li>"ISO 639-2/T language code" if the locale has no country entry</li>
diff --git a/core/java/android/speech/tts/Voice.aidl b/core/java/android/speech/tts/Voice.aidl
new file mode 100644
index 0000000..ca51ff2
--- /dev/null
+++ b/core/java/android/speech/tts/Voice.aidl
@@ -0,0 +1,20 @@
+/*
+**
+** Copyright 2014, 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;
+
+parcelable Voice;
\ No newline at end of file
diff --git a/core/java/android/speech/tts/Voice.java b/core/java/android/speech/tts/Voice.java
new file mode 100644
index 0000000..a97141c
--- /dev/null
+++ b/core/java/android/speech/tts/Voice.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2014 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.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Characteristics and features of a Text-To-Speech Voice. Each TTS Engine can expose
+ * multiple voices for each locale, with different set of features.
+ */
+public class Voice implements Parcelable {
+ /** Very low, but still intelligible quality of speech synthesis */
+ public static final int QUALITY_VERY_LOW = 100;
+
+ /** Low, not human-like quality of speech synthesis */
+ public static final int QUALITY_LOW = 200;
+
+ /** Normal quality of speech synthesis */
+ public static final int QUALITY_NORMAL = 300;
+
+ /** High, human-like quality of speech synthesis */
+ public static final int QUALITY_HIGH = 400;
+
+ /** Very high, almost human-indistinguishable quality of speech synthesis */
+ public static final int QUALITY_VERY_HIGH = 500;
+
+ /** Very low expected synthesizer latency (< 20ms) */
+ public static final int LATENCY_VERY_LOW = 100;
+
+ /** Low expected synthesizer latency (~20ms) */
+ public static final int LATENCY_LOW = 200;
+
+ /** Normal expected synthesizer latency (~50ms) */
+ public static final int LATENCY_NORMAL = 300;
+
+ /** Network based expected synthesizer latency (~200ms) */
+ public static final int LATENCY_HIGH = 400;
+
+ /** Very slow network based expected synthesizer latency (> 200ms) */
+ public static final int LATENCY_VERY_HIGH = 500;
+
+ private final String mName;
+ private final Locale mLocale;
+ private final int mQuality;
+ private final int mLatency;
+ private final boolean mRequiresNetworkConnection;
+ private final Set<String> mFeatures;
+
+ public Voice(String name,
+ Locale locale,
+ int quality,
+ int latency,
+ boolean requiresNetworkConnection,
+ Set<String> features) {
+ this.mName = name;
+ this.mLocale = locale;
+ this.mQuality = quality;
+ this.mLatency = latency;
+ this.mRequiresNetworkConnection = requiresNetworkConnection;
+ this.mFeatures = features;
+ }
+
+ private Voice(Parcel in) {
+ this.mName = in.readString();
+ this.mLocale = (Locale)in.readSerializable();
+ this.mQuality = in.readInt();
+ this.mLatency = in.readInt();
+ this.mRequiresNetworkConnection = (in.readByte() == 1);
+ this.mFeatures = new HashSet<String>();
+ Collections.addAll(this.mFeatures, in.readStringArray());
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mName);
+ dest.writeSerializable(mLocale);
+ dest.writeInt(mQuality);
+ dest.writeInt(mLatency);
+ dest.writeByte((byte) (mRequiresNetworkConnection ? 1 : 0));
+ dest.writeStringList(new ArrayList<String>(mFeatures));
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @hide
+ */
+ public static final Parcelable.Creator<Voice> CREATOR = new Parcelable.Creator<Voice>() {
+ @Override
+ public Voice createFromParcel(Parcel in) {
+ return new Voice(in);
+ }
+
+ @Override
+ public Voice[] newArray(int size) {
+ return new Voice[size];
+ }
+ };
+
+
+ /**
+ * @return The voice's locale
+ */
+ public Locale getLocale() {
+ return mLocale;
+ }
+
+ /**
+ * @return The voice's quality (higher is better)
+ * @see #QUALITY_VERY_HIGH
+ * @see #QUALITY_HIGH
+ * @see #QUALITY_NORMAL
+ * @see #QUALITY_LOW
+ * @see #QUALITY_VERY_LOW
+ */
+ public int getQuality() {
+ return mQuality;
+ }
+
+ /**
+ * @return The voice's latency (lower is better)
+ * @see #LATENCY_VERY_LOW
+ * @see #LATENCY_LOW
+ * @see #LATENCY_NORMAL
+ * @see #LATENCY_HIGH
+ * @see #LATENCY_VERY_HIGH
+ */
+ public int getLatency() {
+ return mLatency;
+ }
+
+ /**
+ * @return Does the Voice require a network connection to work.
+ */
+ public boolean getRequiresNetworkConnection() {
+ return mRequiresNetworkConnection;
+ }
+
+ /**
+ * @return Unique voice name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the set of features it supports for a given voice.
+ * Features can either be framework defined, e.g.
+ * {@link TextToSpeech.Engine#KEY_FEATURE_NETWORK_TIMEOUT_MS} or engine specific.
+ * Engine specific keys must be prefixed by the name of the engine they
+ * are intended for. These keys can be used as parameters to
+ * {@link TextToSpeech#speak(String, int, java.util.HashMap)} and
+ * {@link TextToSpeech#synthesizeToFile(String, java.util.HashMap, String)}.
+ *
+ * Features values are strings and their values must met restrictions described in their
+ * documentation.
+ *
+ * @return Set instance. May return {@code null} on error.
+ */
+ public Set<String> getFeatures() {
+ return mFeatures;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder(64);
+ return builder.append("Voice[Name: ").append(mName)
+ .append(", locale: ").append(mLocale)
+ .append(", quality: ").append(mQuality)
+ .append(", latency: ").append(mLatency)
+ .append(", requiresNetwork: ").append(mRequiresNetworkConnection)
+ .append(", features: ").append(mFeatures.toString())
+ .append("]").toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mFeatures == null) ? 0 : mFeatures.hashCode());
+ result = prime * result + mLatency;
+ result = prime * result + ((mLocale == null) ? 0 : mLocale.hashCode());
+ result = prime * result + ((mName == null) ? 0 : mName.hashCode());
+ result = prime * result + mQuality;
+ result = prime * result + (mRequiresNetworkConnection ? 1231 : 1237);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Voice other = (Voice) obj;
+ if (mFeatures == null) {
+ if (other.mFeatures != null) {
+ return false;
+ }
+ } else if (!mFeatures.equals(other.mFeatures)) {
+ return false;
+ }
+ if (mLatency != other.mLatency) {
+ return false;
+ }
+ if (mLocale == null) {
+ if (other.mLocale != null) {
+ return false;
+ }
+ } else if (!mLocale.equals(other.mLocale)) {
+ return false;
+ }
+ if (mName == null) {
+ if (other.mName != null) {
+ return false;
+ }
+ } else if (!mName.equals(other.mName)) {
+ return false;
+ }
+ if (mQuality != other.mQuality) {
+ return false;
+ }
+ if (mRequiresNetworkConnection != other.mRequiresNetworkConnection) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/tests/TtsTests/src/com/android/speech/tts/TtsEnginesTests.java b/tests/TtsTests/src/com/android/speech/tts/TtsEnginesTests.java
index 45e5216..3fbc44b 100644
--- a/tests/TtsTests/src/com/android/speech/tts/TtsEnginesTests.java
+++ b/tests/TtsTests/src/com/android/speech/tts/TtsEnginesTests.java
@@ -40,6 +40,19 @@
TtsEngines.toOldLocaleStringFormat(new Locale("foo")));
}
+ public void testNormalizeLocale() {
+ assertEquals(Locale.UK,
+ TtsEngines.normalizeTTSLocale(new Locale("eng", "gbr")));
+ assertEquals(Locale.UK,
+ TtsEngines.normalizeTTSLocale(new Locale("eng", "GBR")));
+ assertEquals(Locale.GERMANY,
+ TtsEngines.normalizeTTSLocale(new Locale("deu", "deu")));
+ assertEquals(Locale.GERMAN,
+ TtsEngines.normalizeTTSLocale(new Locale("deu")));
+ assertEquals(new Locale("yyy", "DE"),
+ TtsEngines.normalizeTTSLocale(new Locale("yyy", "DE")));
+ }
+
public void testGetLocalePrefForEngine() {
assertEquals(new Locale("en", "US"),
mTtsHelper.getLocalePrefForEngine("foo","foo:en-US"));