diff options
author | Przemyslaw Szczepaniak <pszczepaniak@google.com> | 2014-07-01 17:04:25 +0100 |
---|---|---|
committer | Przemyslaw Szczepaniak <pszczepaniak@google.com> | 2014-07-04 11:28:06 +0100 |
commit | ad6df74ada7c478257425b746588f22eeec199a6 (patch) | |
tree | fa5675220d901f86dc3b5c264fefe45bffb6637c | |
parent | 96aacd2a2c04f5feeb58e025cacc3c83fc902339 (diff) |
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
-rw-r--r-- | api/current.txt | 48 | ||||
-rw-r--r-- | core/java/android/speech/tts/ITextToSpeechService.aidl | 34 | ||||
-rw-r--r-- | core/java/android/speech/tts/SynthesisRequest.java | 20 | ||||
-rw-r--r-- | core/java/android/speech/tts/TextToSpeech.java | 296 | ||||
-rw-r--r-- | core/java/android/speech/tts/TextToSpeechService.java | 255 | ||||
-rw-r--r-- | core/java/android/speech/tts/TtsEngines.java | 30 | ||||
-rw-r--r-- | core/java/android/speech/tts/Voice.aidl | 20 | ||||
-rw-r--r-- | core/java/android/speech/tts/Voice.java | 263 | ||||
-rw-r--r-- | tests/TtsTests/src/com/android/speech/tts/TtsEnginesTests.java | 13 |
9 files changed, 958 insertions, 21 deletions
diff --git a/api/current.txt b/api/current.txt index fb598fd193e6..4eea35d0193f 100644 --- a/api/current.txt +++ b/api/current.txt @@ -26609,6 +26609,7 @@ package android.speech.tts { 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 @@ package android.speech.tts { 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 @@ package android.speech.tts { 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 @@ package android.speech.tts { 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 @@ package android.speech.tts { 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 @@ package android.speech.tts { 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 @@ package android.speech.tts { 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 694f25af6cc8..4faa67fa1e81 100644 --- a/core/java/android/speech/tts/ITextToSpeechService.aidl +++ b/core/java/android/speech/tts/ITextToSpeechService.aidl @@ -20,6 +20,7 @@ import android.net.Uri; 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 @@ interface ITextToSpeechService { * @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 eaacc06e40b2..d41aa678e7d4 100644 --- a/core/java/android/speech/tts/SynthesisRequest.java +++ b/core/java/android/speech/tts/SynthesisRequest.java @@ -18,12 +18,15 @@ package android.speech.tts; 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 @@ import android.os.Bundle; 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 @@ public final class SynthesisRequest { } /** + * 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 @@ public final class SynthesisRequest { } /** + * 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 e1c1767b79b6..ac9044a213dd 100644 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -36,6 +36,7 @@ import android.util.Log; 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.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; +import java.util.TreeSet; /** * @@ -104,6 +106,12 @@ public class TextToSpeech { 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 @@ public class TextToSpeech { /** * @hide */ + public static final String KEY_PARAM_VOICE_NAME = "voiceName"; + + /** + * @hide + */ public static final String KEY_PARAM_LANGUAGE = "language"; /** @@ -550,7 +563,13 @@ public class TextToSpeech { * @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 @@ public class TextToSpeech { * @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 @@ public class TextToSpeech { * @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 @@ public class TextToSpeech { 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 @@ public class TextToSpeech { mUtteranceProgressListener = null; mEnginesHelper = new TtsEngines(mContext); - if (packageName != null) { - mPackageName = packageName; - } else { - mPackageName = mContext.getPackageName(); - } initTts(); } @@ -1186,12 +1242,16 @@ public class TextToSpeech { * {@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 @@ public class TextToSpeech { * 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 @@ public class TextToSpeech { * 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 @@ public class TextToSpeech { 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 @@ public class TextToSpeech { 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 @@ public class TextToSpeech { * 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 @@ public class TextToSpeech { } /** + * 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 @@ public class TextToSpeech { // 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 @@ public class TextToSpeech { * 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 017be9356288..ecfb8e079dc5 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -39,6 +39,7 @@ 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; @@ -49,7 +50,7 @@ import java.util.Set; /** * 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 @@ import java.util.Set; * * {@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 @@ public abstract class TextToSpeechService extends Service { 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 @@ public abstract class TextToSpeechService extends Service { } 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 abstract class TextToSpeechService extends Service { 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 @@ public abstract class TextToSpeechService extends Service { } } + /** + * 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 @@ public abstract class TextToSpeechService extends Service { } @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 7474efec0a46..df6c0102a677 100644 --- a/core/java/android/speech/tts/TtsEngines.java +++ b/core/java/android/speech/tts/TtsEngines.java @@ -427,6 +427,36 @@ public class TtsEngines { } /** + * 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 000000000000..ca51ff24624b --- /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 000000000000..a97141c51b04 --- /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 45e521657a57..3fbc44bde0dd 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 @@ public class TtsEnginesTests extends InstrumentationTestCase { 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")); |