Merge "Expose "default tts locale" to the TTS V2 API."
diff --git a/api/current.txt b/api/current.txt
index 504d63d..a2d62b0 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -26522,6 +26522,7 @@
}
public static final class TextToSpeechClient.EngineStatus {
+ method public java.util.Locale getDefaultLocale();
method public java.lang.String getEnginePackage();
method public java.util.List<android.speech.tts.VoiceInfo> getVoices();
}
diff --git a/core/java/android/speech/tts/RequestConfigHelper.java b/core/java/android/speech/tts/RequestConfigHelper.java
index 3b5490b..bc65280 100644
--- a/core/java/android/speech/tts/RequestConfigHelper.java
+++ b/core/java/android/speech/tts/RequestConfigHelper.java
@@ -46,7 +46,8 @@
/**
* Score positively voices that exactly match the given locale
- * @param locale Reference locale. If null, the default locale will be used.
+ * @param locale Reference locale. If null, the system default locale for the
+ * current user will be used ({@link Locale#getDefault()}).
*/
public ExactLocaleMatcher(Locale locale) {
if (locale == null) {
@@ -70,7 +71,8 @@
/**
* Score positively voices with similar locale.
- * @param locale Reference locale. If null, default will be used.
+ * @param locale Reference locale. If null, the system default locale for the
+ * current user will be used ({@link Locale#getDefault()}).
*/
public LanguageMatcher(Locale locale) {
if (locale == null) {
@@ -164,10 +166,10 @@
}
/**
- * Get highest quality voice for the default locale.
+ * Get highest quality voice for the TTS default locale.
*
* Call {@link #highestQuality(EngineStatus, boolean, VoiceScorer)} with
- * {@link LanguageMatcher} set to device default locale.
+ * {@link LanguageMatcher} set to the {@link EngineStatus#getDefaultLocale()}.
*
* @param engineStatus
* Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call.
@@ -179,7 +181,7 @@
public static RequestConfig highestQuality(EngineStatus engineStatus,
boolean hasToBeEmbedded) {
return highestQuality(engineStatus, hasToBeEmbedded,
- new LanguageMatcher(Locale.getDefault()));
+ new LanguageMatcher(engineStatus.getDefaultLocale()));
}
}
diff --git a/core/java/android/speech/tts/SynthesisRequestV2.java b/core/java/android/speech/tts/SynthesisRequestV2.java
index a42aa16..938458c9 100644
--- a/core/java/android/speech/tts/SynthesisRequestV2.java
+++ b/core/java/android/speech/tts/SynthesisRequestV2.java
@@ -167,9 +167,7 @@
}
};
- /**
- * @hide
- */
+ /** @hide */
@Override
public int describeContents() {
return 0;
diff --git a/core/java/android/speech/tts/TextToSpeechClient.java b/core/java/android/speech/tts/TextToSpeechClient.java
index 0c0be83..f726743 100644
--- a/core/java/android/speech/tts/TextToSpeechClient.java
+++ b/core/java/android/speech/tts/TextToSpeechClient.java
@@ -42,6 +42,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -357,9 +358,13 @@
/** Name of the TTS engine package */
private final String mPackageName;
- private EngineStatus(String packageName, List<VoiceInfo> voices) {
+ /** Engine default locale */
+ private final Locale mDefaultLocale;
+
+ private EngineStatus(String packageName, List<VoiceInfo> voices, Locale defaultLocale) {
this.mVoices = Collections.unmodifiableList(voices);
this.mPackageName = packageName;
+ this.mDefaultLocale = defaultLocale;
}
/**
@@ -375,6 +380,16 @@
public String getEnginePackage() {
return mPackageName;
}
+
+ /**
+ * Get the default locale to use for TTS with this TTS engine.
+ * Unless the user changed the TTS settings for this engine, the value returned should be
+ * the same as the system default locale for the current user
+ * ({@link Locale#getDefault()}).
+ */
+ public Locale getDefaultLocale() {
+ return mDefaultLocale;
+ }
}
/** Unique synthesis request identifier. */
@@ -638,7 +653,9 @@
return null;
}
- return new EngineStatus(mServiceConnection.getEngineName(), voices);
+ return new EngineStatus(mServiceConnection.getEngineName(), voices,
+ mEnginesHelper.getLocalePrefForEngine(
+ mServiceConnection.getEngineName()));
}
private class Connection implements ServiceConnection {
@@ -696,7 +713,9 @@
public void onVoicesInfoChange(List<VoiceInfo> voicesInfo) {
synchronized (mLock) {
mEngineStatus = new EngineStatus(mServiceConnection.getEngineName(),
- voicesInfo);
+ voicesInfo,
+ mEnginesHelper.getLocalePrefForEngine(
+ mServiceConnection.getEngineName()));
mMainHandler.obtainMessage(InternalHandler.WHAT_ENGINE_STATUS_CHANGED,
mEngineStatus).sendToTarget();
}
diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java
index 14a4024..20f3ad7 100644
--- a/core/java/android/speech/tts/TextToSpeechService.java
+++ b/core/java/android/speech/tts/TextToSpeechService.java
@@ -460,8 +460,8 @@
}
private String[] getSettingsLocale() {
- final String locale = mEngineHelper.getLocalePrefForEngine(mPackageName);
- return TtsEngines.parseLocalePref(locale);
+ final Locale locale = mEngineHelper.getLocalePrefForEngine(mPackageName);
+ return TtsEngines.toOldLocaleStringFormat(locale);
}
private int getSecureSettingInt(String name, int defaultValue) {
diff --git a/core/java/android/speech/tts/TtsEngines.java b/core/java/android/speech/tts/TtsEngines.java
index 9b929a3..b4c2824 100644
--- a/core/java/android/speech/tts/TtsEngines.java
+++ b/core/java/android/speech/tts/TtsEngines.java
@@ -28,6 +28,7 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+
import static android.provider.Settings.Secure.getString;
import android.provider.Settings;
@@ -42,8 +43,10 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.MissingResourceException;
/**
@@ -53,16 +56,52 @@
* Comments in this class the use the shorthand "system engines" for engines that
* are a part of the system image.
*
+ * This class is thread-safe/
+ *
* @hide
*/
public class TtsEngines {
private static final String TAG = "TtsEngines";
private static final boolean DBG = false;
- private static final String LOCALE_DELIMITER = "-";
+ /** Locale delimiter used by the old-style 3 char locale string format (like "eng-usa") */
+ private static final String LOCALE_DELIMITER_OLD = "-";
+
+ /** Locale delimiter used by the new-style locale string format (Locale.toString() results,
+ * like "en_US") */
+ private static final String LOCALE_DELIMITER_NEW = "_";
private final Context mContext;
+ /** Mapping of various language strings to the normalized Locale form */
+ private static final Map<String, String> sNormalizeLanguage;
+
+ /** Mapping of various country strings to the normalized Locale form */
+ private static final Map<String, String> sNormalizeCountry;
+
+ // Populate the sNormalize* maps
+ static {
+ HashMap<String, String> normalizeLanguage = new HashMap<String, String>();
+ for (String language : Locale.getISOLanguages()) {
+ try {
+ normalizeLanguage.put(new Locale(language).getISO3Language(), language);
+ } catch (MissingResourceException e) {
+ continue;
+ }
+ }
+ sNormalizeLanguage = Collections.unmodifiableMap(normalizeLanguage);
+
+ HashMap<String, String> normalizeCountry = new HashMap<String, String>();
+ for (String country : Locale.getISOCountries()) {
+ try {
+ normalizeCountry.put(new Locale("", country).getISO3Country(), country);
+ } catch (MissingResourceException e) {
+ continue;
+ }
+ }
+ sNormalizeCountry = Collections.unmodifiableMap(normalizeCountry);
+ }
+
public TtsEngines(Context ctx) {
mContext = ctx;
}
@@ -282,139 +321,139 @@
}
/**
- * Returns the locale string for a given TTS engine. Attempts to read the
+ * Returns the default locale for a given TTS engine. Attempts to read the
* value from {@link Settings.Secure#TTS_DEFAULT_LOCALE}, failing which the
- * old style value from {@link Settings.Secure#TTS_DEFAULT_LANG} is read. If
- * both these values are empty, the default phone locale is returned.
+ * default phone locale is returned.
*
* @param engineName the engine to return the locale for.
- * @return the locale string preference for this engine. Will be non null
- * and non empty.
+ * @return the locale preference for this engine. Will be non null.
*/
- public String getLocalePrefForEngine(String engineName) {
- String locale = parseEnginePrefFromList(
- getString(mContext.getContentResolver(), Settings.Secure.TTS_DEFAULT_LOCALE),
- engineName);
-
- if (TextUtils.isEmpty(locale)) {
- // The new style setting is unset, attempt to return the old style setting.
- locale = getV1Locale();
- }
-
- if (DBG) Log.d(TAG, "getLocalePrefForEngine(" + engineName + ")= " + locale);
-
- return locale;
+ public Locale getLocalePrefForEngine(String engineName) {
+ return getLocalePrefForEngine(engineName,
+ getString(mContext.getContentResolver(), Settings.Secure.TTS_DEFAULT_LOCALE));
}
/**
+ * Returns the default locale for a given TTS engine from given settings string. */
+ public Locale getLocalePrefForEngine(String engineName, String prefValue) {
+ String localeString = parseEnginePrefFromList(
+ prefValue,
+ engineName);
+
+ if (TextUtils.isEmpty(localeString)) {
+ // The new style setting is unset, attempt to return the old style setting.
+ return Locale.getDefault();
+ }
+
+ Locale result = parseLocaleString(localeString);
+ if (result == null) {
+ Log.w(TAG, "Failed to parse locale " + localeString + ", returning en_US instead");
+ result = Locale.US;
+ }
+
+ if (DBG) Log.d(TAG, "getLocalePrefForEngine(" + engineName + ")= " + result);
+
+ return result;
+ }
+
+
+ /**
* True if a given TTS engine uses the default phone locale as a default locale. Attempts to
- * read the value from {@link Settings.Secure#TTS_DEFAULT_LOCALE}, failing which the
- * old style value from {@link Settings.Secure#TTS_DEFAULT_LANG} is read. If
- * both these values are empty, this methods returns true.
+ * read the value from {@link Settings.Secure#TTS_DEFAULT_LOCALE}. If
+ * its value is empty, this methods returns true.
*
* @param engineName the engine to return the locale for.
*/
public boolean isLocaleSetToDefaultForEngine(String engineName) {
- return (TextUtils.isEmpty(parseEnginePrefFromList(
+ return TextUtils.isEmpty(parseEnginePrefFromList(
getString(mContext.getContentResolver(), Settings.Secure.TTS_DEFAULT_LOCALE),
- engineName)) &&
- TextUtils.isEmpty(
- Settings.Secure.getString(mContext.getContentResolver(),
- Settings.Secure.TTS_DEFAULT_LANG)));
+ engineName));
}
-
/**
- * Parses a locale preference value delimited by {@link #LOCALE_DELIMITER}.
- * Varies from {@link String#split} in that it will always return an array
- * of length 3 with non null values.
+ * Parses a locale encoded as a string, and tries its best to return a valid {@link Locale}
+ * object, even if the input string is encoded using the old-style 3 character format e.g.
+ * "deu-deu". At the end, we test if the resulting locale can return ISO3 language and
+ * country codes ({@link Locale#getISO3Language()} and {@link Locale#getISO3Country()}),
+ * if it fails to do so, we return null.
*/
- public static String[] parseLocalePref(String pref) {
- String[] returnVal = new String[] { "", "", ""};
- if (!TextUtils.isEmpty(pref)) {
- String[] split = pref.split(LOCALE_DELIMITER);
- System.arraycopy(split, 0, returnVal, 0, split.length);
+ public Locale parseLocaleString(String localeString) {
+ String language = "", country = "", variant = "";
+ if (!TextUtils.isEmpty(localeString)) {
+ String[] split = localeString.split(
+ "[" + LOCALE_DELIMITER_OLD + LOCALE_DELIMITER_NEW + "]");
+ language = split[0].toLowerCase();
+ if (split.length == 0) {
+ Log.w(TAG, "Failed to convert " + localeString + " to a valid Locale object. Only" +
+ " separators");
+ return null;
+ }
+ if (split.length > 3) {
+ Log.w(TAG, "Failed to convert " + localeString + " to a valid Locale object. Too" +
+ " many separators");
+ return null;
+ }
+ if (split.length >= 2) {
+ country = split[1].toUpperCase();
+ }
+ if (split.length >= 3) {
+ variant = split[2];
+ }
+
}
- if (DBG) Log.d(TAG, "parseLocalePref(" + returnVal[0] + "," + returnVal[1] +
- "," + returnVal[2] +")");
+ String normalizedLanguage = sNormalizeLanguage.get(language);
+ if (normalizedLanguage != null) {
+ language = normalizedLanguage;
+ }
- return returnVal;
+ String normalizedCountry= sNormalizeCountry.get(country);
+ if (normalizedCountry != null) {
+ country = normalizedCountry;
+ }
+
+ if (DBG) Log.d(TAG, "parseLocalePref(" + language + "," + country +
+ "," + variant +")");
+
+ Locale result = new Locale(language, country, variant);
+ try {
+ result.getISO3Language();
+ result.getISO3Country();
+ return result;
+ } catch(MissingResourceException e) {
+ Log.w(TAG, "Failed to convert " + localeString + " to a valid Locale object.");
+ return null;
+ }
}
/**
- * @return the old style locale string constructed from
- * {@link Settings.Secure#TTS_DEFAULT_LANG},
- * {@link Settings.Secure#TTS_DEFAULT_COUNTRY} and
- * {@link Settings.Secure#TTS_DEFAULT_VARIANT}. If no such locale is set,
- * then return the default phone locale.
- */
- private String getV1Locale() {
- final ContentResolver cr = mContext.getContentResolver();
-
- final String lang = Settings.Secure.getString(cr, Settings.Secure.TTS_DEFAULT_LANG);
- final String country = Settings.Secure.getString(cr, Settings.Secure.TTS_DEFAULT_COUNTRY);
- final String variant = Settings.Secure.getString(cr, Settings.Secure.TTS_DEFAULT_VARIANT);
-
- if (TextUtils.isEmpty(lang)) {
- return getDefaultLocale();
- }
-
- String v1Locale = lang;
- if (!TextUtils.isEmpty(country)) {
- v1Locale += LOCALE_DELIMITER + country;
- } else {
- return v1Locale;
- }
-
- if (!TextUtils.isEmpty(variant)) {
- v1Locale += LOCALE_DELIMITER + variant;
- }
-
- return v1Locale;
- }
-
- /**
- * Return the default device locale in form of 3 letter codes delimited by
- * {@link #LOCALE_DELIMITER}:
+ * Return the old-style string form of the locale. It consists of 3 letter codes:
* <ul>
- * <li> "ISO 639-2/T language code" if locale have no country entry</li>
- * <li> "ISO 639-2/T language code{@link #LOCALE_DELIMITER}ISO 3166 country code "
- * if locale have no variant entry</li>
- * <li> "ISO 639-2/T language code{@link #LOCALE_DELIMITER}ISO 3166 country code
- * {@link #LOCALE_DELIMITER} variant" if locale have variant entry</li>
+ * <li>"ISO 639-2/T language code" if the locale has no country entry</li>
+ * <li> "ISO 639-2/T language code{@link #LOCALE_DELIMITER}ISO 3166 country code"
+ * if the locale has no variant entry</li>
+ * <li> "ISO 639-2/T language code{@link #LOCALE_DELIMITER}ISO 3166 country
+ * code{@link #LOCALE_DELIMITER}variant" if the locale has a variant entry</li>
* </ul>
+ * If we fail to generate those codes using {@link Locale#getISO3Country()} and
+ * {@link Locale#getISO3Language()}, then we return new String[]{"eng","USA",""};
*/
- public String getDefaultLocale() {
- final Locale locale = Locale.getDefault();
-
+ static public String[] toOldLocaleStringFormat(Locale locale) {
+ String[] ret = new String[]{"","",""};
try {
// Note that the default locale might have an empty variant
// or language, and we take care that the construction is
// the same as {@link #getV1Locale} i.e no trailing delimiters
// or spaces.
- String defaultLocale = locale.getISO3Language();
- if (TextUtils.isEmpty(defaultLocale)) {
- Log.w(TAG, "Default locale is empty.");
- return "";
- }
+ ret[0] = locale.getISO3Language();
+ ret[1] = locale.getISO3Country();
+ ret[2] = locale.getVariant();
- if (!TextUtils.isEmpty(locale.getISO3Country())) {
- defaultLocale += LOCALE_DELIMITER + locale.getISO3Country();
- } else {
- // Do not allow locales of the form lang--variant with
- // an empty country.
- return defaultLocale;
- }
- if (!TextUtils.isEmpty(locale.getVariant())) {
- defaultLocale += LOCALE_DELIMITER + locale.getVariant();
- }
-
- return defaultLocale;
+ return ret;
} catch (MissingResourceException e) {
// Default locale does not have a ISO 3166 and/or ISO 639-2/T codes. Return the
// default "eng-usa" (that would be the result of Locale.getDefault() == Locale.US).
- return "eng-usa";
+ return new String[]{"eng","USA",""};
}
}
@@ -443,16 +482,21 @@
return null;
}
- public synchronized void updateLocalePrefForEngine(String name, String newLocale) {
+ /**
+ * Serialize the locale to a string and store it as a default locale for the given engine. If
+ * the passed locale is null, an empty string will be serialized; that empty string, when
+ * read back, will evaluate to {@line Locale#getDefault()}.
+ */
+ public synchronized void updateLocalePrefForEngine(String engineName, Locale newLocale) {
final String prefList = Settings.Secure.getString(mContext.getContentResolver(),
Settings.Secure.TTS_DEFAULT_LOCALE);
if (DBG) {
- Log.d(TAG, "updateLocalePrefForEngine(" + name + ", " + newLocale +
+ Log.d(TAG, "updateLocalePrefForEngine(" + engineName + ", " + newLocale +
"), originally: " + prefList);
}
final String newPrefList = updateValueInCommaSeparatedList(prefList,
- name, newLocale);
+ engineName, (newLocale != null) ? newLocale.toString() : "");
if (DBG) Log.d(TAG, "updateLocalePrefForEngine(), writing: " + newPrefList.toString());
diff --git a/tests/TtsTests/src/com/android/speech/tts/TtsEnginesTests.java b/tests/TtsTests/src/com/android/speech/tts/TtsEnginesTests.java
new file mode 100644
index 0000000..45e5216
--- /dev/null
+++ b/tests/TtsTests/src/com/android/speech/tts/TtsEnginesTests.java
@@ -0,0 +1,64 @@
+package com.android.speech.tts;
+
+import android.speech.tts.TtsEngines;
+import android.test.InstrumentationTestCase;
+
+import java.util.Locale;
+
+public class TtsEnginesTests extends InstrumentationTestCase {
+ private TtsEngines mTtsHelper;
+
+ @Override
+ public void setUp() {
+ mTtsHelper = new TtsEngines(getInstrumentation().getContext());
+ }
+
+ public void testParseLocaleString() {
+ assertEquals(new Locale("en", "US"), mTtsHelper.parseLocaleString("eng-usa"));
+ assertEquals(new Locale("en", "US"), mTtsHelper.parseLocaleString("eng-USA"));
+ assertEquals(new Locale("en", "US"), mTtsHelper.parseLocaleString("en-US"));
+ assertEquals(new Locale("en", "US"), mTtsHelper.parseLocaleString("en_us"));
+ assertEquals(new Locale("en", "US"), mTtsHelper.parseLocaleString("eng_US"));
+ assertEquals(new Locale("en", "US", "foobar"),
+ mTtsHelper.parseLocaleString("eng_US-foobar"));
+ assertEquals(new Locale("en", "", "foobar"), mTtsHelper.parseLocaleString("eng__foobar"));
+ assertNull(mTtsHelper.parseLocaleString("cc_xx_barbar"));
+ assertNull(mTtsHelper.parseLocaleString("cc--barbar"));
+
+ assertEquals(new Locale("en"), mTtsHelper.parseLocaleString("eng"));
+ assertEquals(new Locale("en","US","var"), mTtsHelper.parseLocaleString("eng-USA-var"));
+ }
+
+ public void testToOldLocaleStringFormat() {
+ assertArraysEqual(new String[]{"deu", "DEU", ""},
+ TtsEngines.toOldLocaleStringFormat(new Locale("de", "DE")));
+ assertArraysEqual(new String[]{"deu", "", ""},
+ TtsEngines.toOldLocaleStringFormat(new Locale("de")));
+ assertArraysEqual(new String[]{"eng", "", ""},
+ TtsEngines.toOldLocaleStringFormat(new Locale("en")));
+ assertArraysEqual(new String[]{"eng", "USA", ""},
+ TtsEngines.toOldLocaleStringFormat(new Locale("foo")));
+ }
+
+ public void testGetLocalePrefForEngine() {
+ assertEquals(new Locale("en", "US"),
+ mTtsHelper.getLocalePrefForEngine("foo","foo:en-US"));
+ assertEquals(new Locale("en", "US"),
+ mTtsHelper.getLocalePrefForEngine("foo","foo:eng-usa"));
+ assertEquals(new Locale("en", "US"),
+ mTtsHelper.getLocalePrefForEngine("foo","foo:eng_USA"));
+ assertEquals(new Locale("de", "DE"),
+ mTtsHelper.getLocalePrefForEngine("foo","foo:deu-deu"));
+ assertEquals(Locale.getDefault(),
+ mTtsHelper.getLocalePrefForEngine("foo","foo:,bar:xx"));
+ assertEquals(Locale.getDefault(),
+ mTtsHelper.getLocalePrefForEngine("other","foo:,bar:xx"));
+ }
+
+ private void assertArraysEqual(String[] expected, String[] actual) {
+ assertEquals("array length", expected.length, actual.length);
+ for (int i = 0; i < expected.length; i++) {
+ assertEquals("index " + i, expected[i], actual[i]);
+ }
+ }
+}
\ No newline at end of file