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