Merge "Framework changes to support new TTS settings features."
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 554afd2..03cba3d 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2815,6 +2815,16 @@
         public static final String TTS_DEFAULT_VARIANT = "tts_default_variant";
 
         /**
+         * Stores the default tts locales on a per engine basis. Stored as
+         * a comma seperated list of values, each value being of the form
+         * {@code engine_name:locale} for example,
+         * {@code com.foo.ttsengine:eng-USA,com.bar.ttsengine:esp-ESP}.
+         *
+         * @hide
+         */
+        public static final String TTS_DEFAULT_LOCALE = "tts_default_locale";
+
+        /**
          * Space delimited list of plugin packages that are enabled.
          */
         public static final String TTS_ENABLED_PLUGINS = "tts_enabled_plugins";
diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java
index b4e8ab4..a08ba2a 100644
--- a/core/java/android/speech/tts/TextToSpeechService.java
+++ b/core/java/android/speech/tts/TextToSpeechService.java
@@ -65,6 +65,7 @@
  *
  * {@link #onStop} tells the engine that it should stop all ongoing synthesis, if
  * any. Any pending data from the current synthesis will be discarded.
+ *
  */
 // TODO: Add a link to the sample TTS engine once it's done.
 public abstract class TextToSpeechService extends Service {
@@ -80,6 +81,7 @@
     // associated with this TTS engine. Will handle all requests except synthesis
     // to file requests, which occur on the synthesis thread.
     private AudioPlaybackHandler mAudioPlaybackHandler;
+    private TtsEngines mEngineHelper;
 
     private CallbackMap mCallbacks;
     private String mPackageName;
@@ -96,12 +98,15 @@
         mAudioPlaybackHandler = new AudioPlaybackHandler();
         mAudioPlaybackHandler.start();
 
+        mEngineHelper = new TtsEngines(this);
+
         mCallbacks = new CallbackMap();
 
         mPackageName = getApplicationInfo().packageName;
 
+        String[] defaultLocale = getSettingsLocale();
         // Load default language
-        onLoadLanguage(getDefaultLanguage(), getDefaultCountry(), getDefaultVariant());
+        onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]);
     }
 
     @Override
@@ -195,30 +200,15 @@
         return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE);
     }
 
-    private String getDefaultLanguage() {
-        return getSecureSettingString(Settings.Secure.TTS_DEFAULT_LANG,
-                Locale.getDefault().getISO3Language());
-    }
-
-    private String getDefaultCountry() {
-        return getSecureSettingString(Settings.Secure.TTS_DEFAULT_COUNTRY,
-                Locale.getDefault().getISO3Country());
-    }
-
-    private String getDefaultVariant() {
-        return getSecureSettingString(Settings.Secure.TTS_DEFAULT_VARIANT,
-                Locale.getDefault().getVariant());
+    private String[] getSettingsLocale() {
+        final String locale = mEngineHelper.getLocalePrefForEngine(mPackageName);
+        return TtsEngines.parseLocalePref(locale);
     }
 
     private int getSecureSettingInt(String name, int defaultValue) {
         return Settings.Secure.getInt(getContentResolver(), name, defaultValue);
     }
 
-    private String getSecureSettingString(String name, String defaultValue) {
-        String value = Settings.Secure.getString(getContentResolver(), name);
-        return value != null ? value : defaultValue;
-    }
-
     /**
      * Synthesizer thread. This thread is used to run {@link SynthHandler}.
      */
@@ -458,6 +448,7 @@
     class SynthesisSpeechItem extends SpeechItem {
         private final String mText;
         private final SynthesisRequest mSynthesisRequest;
+        private final String[] mDefaultLocale;
         // Non null after synthesis has started, and all accesses
         // guarded by 'this'.
         private AbstractSynthesisCallback mSynthesisCallback;
@@ -467,6 +458,7 @@
             super(callingApp, params);
             mText = text;
             mSynthesisRequest = new SynthesisRequest(mText, mParams);
+            mDefaultLocale = getSettingsLocale();
             setRequestParams(mSynthesisRequest);
             mEventLogger = new EventLogger(mSynthesisRequest, getCallingApp(), mPackageName);
         }
@@ -523,7 +515,7 @@
         }
 
         public String getLanguage() {
-            return getStringParam(Engine.KEY_PARAM_LANGUAGE, getDefaultLanguage());
+            return getStringParam(Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]);
         }
 
         private boolean hasLanguage() {
@@ -531,12 +523,12 @@
         }
 
         private String getCountry() {
-            if (!hasLanguage()) return getDefaultCountry();
+            if (!hasLanguage()) return mDefaultLocale[1];
             return getStringParam(Engine.KEY_PARAM_COUNTRY, "");
         }
 
         private String getVariant() {
-            if (!hasLanguage()) return getDefaultVariant();
+            if (!hasLanguage()) return mDefaultLocale[2];
             return getStringParam(Engine.KEY_PARAM_VARIANT, "");
         }
 
diff --git a/core/java/android/speech/tts/TtsEngines.java b/core/java/android/speech/tts/TtsEngines.java
index 5f0cb74..bb72bea 100644
--- a/core/java/android/speech/tts/TtsEngines.java
+++ b/core/java/android/speech/tts/TtsEngines.java
@@ -17,6 +17,7 @@
 
 import org.xmlpull.v1.XmlPullParserException;
 
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
@@ -27,6 +28,8 @@
 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;
 import android.speech.tts.TextToSpeech.Engine;
 import android.speech.tts.TextToSpeech.EngineInfo;
@@ -40,6 +43,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * Support class for querying the list of available engines
@@ -52,6 +56,9 @@
  */
 public class TtsEngines {
     private static final String TAG = "TtsEngines";
+    private static final boolean DBG = false;
+
+    private static final String LOCALE_DELIMITER = "-";
 
     private final Context mContext;
 
@@ -65,7 +72,7 @@
      *         the highest ranked engine is returned as per {@link EngineInfoComparator}.
      */
     public String getDefaultEngine() {
-        String engine = Settings.Secure.getString(mContext.getContentResolver(),
+        String engine = getString(mContext.getContentResolver(),
                 Settings.Secure.TTS_DEFAULT_SYNTH);
         return isEngineInstalled(engine) ? engine : getHighestRankedEngineName();
     }
@@ -129,12 +136,6 @@
         return engines;
     }
 
-    // TODO: Used only by the settings app. Remove once
-    // the settings UI change has been finalized.
-    public boolean isEngineEnabled(String engine) {
-        return isEngineInstalled(engine);
-    }
-
     private boolean isSystemEngine(ServiceInfo info) {
         final ApplicationInfo appInfo = info.applicationInfo;
         return appInfo != null && (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
@@ -182,7 +183,7 @@
      * The name of the XML tag that text to speech engines must use to
      * declare their meta data.
      *
-     * {@link com.android.internal.R.styleable.TextToSpeechEngine}
+     * {@link com.android.internal.R.styleable#TextToSpeechEngine}
      */
     private static final String XML_TAG_NAME = "tts-engine";
 
@@ -279,4 +280,175 @@
         }
     }
 
+    /**
+     * Returns the locale string 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.
+     *
+     * @param engineName the engine to return the locale for.
+     * @return the locale string preference for this engine. Will be non null
+     *         and non empty.
+     */
+    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;
+    }
+
+    /**
+     * 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.
+     */
+    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);
+        }
+
+        if (DBG) Log.d(TAG, "parseLocalePref(" + returnVal[0] + "," + returnVal[1] +
+                "," + returnVal[2] +")");
+
+        return returnVal;
+    }
+
+    /**
+     * @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;
+        }
+        if (!TextUtils.isEmpty(variant)) {
+            v1Locale += LOCALE_DELIMITER + variant;
+        }
+
+        return v1Locale;
+    }
+
+    private String getDefaultLocale() {
+        final Locale locale = Locale.getDefault();
+
+        return locale.getISO3Language() + LOCALE_DELIMITER + locale.getISO3Country() +
+                LOCALE_DELIMITER + locale.getVariant();
+    }
+
+    /**
+     * Parses a comma separated list of engine locale preferences. The list is of the
+     * form {@code "engine_name_1:locale_1,engine_name_2:locale2"} and so on and
+     * so forth. Returns null if the list is empty, malformed or if there is no engine
+     * specific preference in the list.
+     */
+    private static String parseEnginePrefFromList(String prefValue, String engineName) {
+        if (TextUtils.isEmpty(prefValue)) {
+            return null;
+        }
+
+        String[] prefValues = prefValue.split(",");
+
+        for (String value : prefValues) {
+            final int delimiter = value.indexOf(':');
+            if (delimiter > 0) {
+                if (engineName.equals(value.substring(0, delimiter))) {
+                    return value.substring(delimiter + 1);
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public synchronized void updateLocalePrefForEngine(String name, String newLocale) {
+        final String prefList = Settings.Secure.getString(mContext.getContentResolver(),
+                Settings.Secure.TTS_DEFAULT_LOCALE);
+        if (DBG) {
+            Log.d(TAG, "updateLocalePrefForEngine(" + name + ", " + newLocale +
+                    "), originally: " + prefList);
+        }
+
+        final String newPrefList = updateValueInCommaSeparatedList(prefList,
+                name, newLocale);
+
+        if (DBG) Log.d(TAG, "updateLocalePrefForEngine(), writing: " + newPrefList.toString());
+
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.TTS_DEFAULT_LOCALE, newPrefList.toString());
+    }
+
+    /**
+     * Updates the value for a given key in a comma separated list of key value pairs,
+     * each of which are delimited by a colon. If no value exists for the given key,
+     * the kay value pair are appended to the end of the list.
+     */
+    private String updateValueInCommaSeparatedList(String list, String key,
+            String newValue) {
+        StringBuilder newPrefList = new StringBuilder();
+        if (TextUtils.isEmpty(list)) {
+            // If empty, create a new list with a single entry.
+            newPrefList.append(key).append(':').append(newValue);
+        } else {
+            String[] prefValues = list.split(",");
+            // Whether this is the first iteration in the loop.
+            boolean first = true;
+            // Whether we found the given key.
+            boolean found = false;
+            for (String value : prefValues) {
+                final int delimiter = value.indexOf(':');
+                if (delimiter > 0) {
+                    if (key.equals(value.substring(0, delimiter))) {
+                        if (first) {
+                            first = false;
+                        } else {
+                            newPrefList.append(',');
+                        }
+                        found = true;
+                        newPrefList.append(key).append(':').append(newValue);
+                    } else {
+                        if (first) {
+                            first = false;
+                        } else {
+                            newPrefList.append(',');
+                        }
+                        // Copy across the entire key + value as is.
+                        newPrefList.append(value);
+                    }
+                }
+            }
+
+            if (!found) {
+                // Not found, but the rest of the keys would have been copied
+                // over already, so just append it to the end.
+                newPrefList.append(',');
+                newPrefList.append(key).append(':').append(newValue);
+            }
+        }
+
+        return newPrefList.toString();
+    }
 }