Make res.Configuration support locale lists.

We also deprecate the locale attribute, but works around the cases in
which people would call it.

Also add various methods to LocaleList to support the features
Configuration requires.

Change-Id: Iacc537e5fc1a3d4c1ea7e5517347876ca4e07e0a
diff --git a/api/current.txt b/api/current.txt
index df74b45..933e4df 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9663,12 +9663,14 @@
     method public int diff(android.content.res.Configuration);
     method public boolean equals(android.content.res.Configuration);
     method public int getLayoutDirection();
+    method public android.util.LocaleList getLocales();
     method public boolean isLayoutSizeAtLeast(int);
     method public boolean isScreenRound();
     method public static boolean needNewResources(int, int);
     method public void readFromParcel(android.os.Parcel);
     method public void setLayoutDirection(java.util.Locale);
     method public void setLocale(java.util.Locale);
+    method public void setLocales(android.util.LocaleList);
     method public void setTo(android.content.res.Configuration);
     method public void setToDefaults();
     method public int updateFrom(android.content.res.Configuration);
@@ -9742,7 +9744,7 @@
     field public int hardKeyboardHidden;
     field public int keyboard;
     field public int keyboardHidden;
-    field public java.util.Locale locale;
+    field public deprecated java.util.Locale locale;
     field public int mcc;
     field public int mnc;
     field public int navigation;
@@ -34200,11 +34202,15 @@
 
   public final class LocaleList {
     ctor public LocaleList();
+    ctor public LocaleList(java.util.Locale);
     ctor public LocaleList(java.util.Locale[]);
+    method public static android.util.LocaleList forLanguageTags(java.lang.String);
     method public java.util.Locale get(int);
+    method public static android.util.LocaleList getEmptyLocaleList();
     method public java.util.Locale getPrimary();
     method public boolean isEmpty();
     method public int size();
+    method public java.lang.String toLanguageTags();
   }
 
   public final class Log {
diff --git a/api/system-current.txt b/api/system-current.txt
index bcae74b..97e3bb7 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -10000,12 +10000,14 @@
     method public int diff(android.content.res.Configuration);
     method public boolean equals(android.content.res.Configuration);
     method public int getLayoutDirection();
+    method public android.util.LocaleList getLocales();
     method public boolean isLayoutSizeAtLeast(int);
     method public boolean isScreenRound();
     method public static boolean needNewResources(int, int);
     method public void readFromParcel(android.os.Parcel);
     method public void setLayoutDirection(java.util.Locale);
     method public void setLocale(java.util.Locale);
+    method public void setLocales(android.util.LocaleList);
     method public void setTo(android.content.res.Configuration);
     method public void setToDefaults();
     method public int updateFrom(android.content.res.Configuration);
@@ -10079,7 +10081,7 @@
     field public int hardKeyboardHidden;
     field public int keyboard;
     field public int keyboardHidden;
-    field public java.util.Locale locale;
+    field public deprecated java.util.Locale locale;
     field public int mcc;
     field public int mnc;
     field public int navigation;
@@ -36494,11 +36496,15 @@
 
   public final class LocaleList {
     ctor public LocaleList();
+    ctor public LocaleList(java.util.Locale);
     ctor public LocaleList(java.util.Locale[]);
+    method public static android.util.LocaleList forLanguageTags(java.lang.String);
     method public java.util.Locale get(int);
+    method public static android.util.LocaleList getEmptyLocaleList();
     method public java.util.Locale getPrimary();
     method public boolean isEmpty();
     method public int size();
+    method public java.lang.String toLanguageTags();
   }
 
   public final class Log {
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index fd60476..3ecbaa0 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -22,11 +22,13 @@
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlSerializer;
 
+import android.annotation.Nullable;
 import android.content.pm.ActivityInfo;
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
+import android.util.LocaleList;
 import android.view.View;
 
 import java.io.IOException;
@@ -36,7 +38,7 @@
 /**
  * This class describes all device configuration information that can
  * impact the resources the application retrieves.  This includes both
- * user-specified configuration options (locale and scaling) as well
+ * user-specified configuration options (locale list and scaling) as well
  * as device configurations (such as input modes, screen size and screen orientation).
  * <p>You can acquire this object from {@link Resources}, using {@link
  * Resources#getConfiguration}. Thus, from an activity, you can get it by chaining the request
@@ -78,8 +80,13 @@
      * Current user preference for the locale, corresponding to
      * <a href="{@docRoot}guide/topics/resources/providing-resources.html#LocaleQualifier">locale</a>
      * resource qualifier.
+     *
+     * @deprecated Do not set or read this directly. Use {@link #getLocales()} and
+     * {@link #setLocales(LocaleList)}.
      */
-    public Locale locale;
+    @Deprecated public Locale locale;
+
+    private LocaleList mLocaleList;
 
     /**
      * Locale should persist on setting.  This is hidden because it is really
@@ -648,6 +655,15 @@
         setTo(o);
     }
 
+    /* This brings mLocaleList in sync with locale in case a user of the older API who doesn't know
+     * about setLocales() has changed locale directly. */
+    private void fixUpLocaleList() {
+        if ((locale == null && !mLocaleList.isEmpty()) ||
+                (locale != null && !locale.equals(mLocaleList.getPrimary()))) {
+            mLocaleList = new LocaleList(locale);
+        }
+    }
+
     public void setTo(Configuration o) {
         fontScale = o.fontScale;
         mcc = o.mcc;
@@ -655,6 +671,8 @@
         if (o.locale != null) {
             locale = (Locale) o.locale.clone();
         }
+        o.fixUpLocaleList();
+        mLocaleList = o.mLocaleList;
         userSetLocale = o.userSetLocale;
         touchscreen = o.touchscreen;
         keyboard = o.keyboard;
@@ -692,11 +710,12 @@
         } else {
             sb.append("?mnc");
         }
-        if (locale != null) {
+        fixUpLocaleList();
+        if (!mLocaleList.isEmpty()) {
             sb.append(" ");
-            sb.append(locale);
+            sb.append(mLocaleList);
         } else {
-            sb.append(" ?locale");
+            sb.append(" ?localeList");
         }
         int layoutDir = (screenLayout&SCREENLAYOUT_LAYOUTDIR_MASK);
         switch (layoutDir) {
@@ -819,6 +838,7 @@
     public void setToDefaults() {
         fontScale = 1;
         mcc = mnc = 0;
+        mLocaleList = LocaleList.getEmptyLocaleList();
         locale = null;
         userSetLocale = false;
         touchscreen = TOUCHSCREEN_UNDEFINED;
@@ -864,16 +884,20 @@
             changed |= ActivityInfo.CONFIG_MNC;
             mnc = delta.mnc;
         }
-        if (delta.locale != null
-                && (locale == null || !locale.equals(delta.locale))) {
+        fixUpLocaleList();
+        delta.fixUpLocaleList();
+        if (!delta.mLocaleList.isEmpty() && !mLocaleList.equals(delta.mLocaleList)) {
             changed |= ActivityInfo.CONFIG_LOCALE;
-            locale = delta.locale != null
-                    ? (Locale) delta.locale.clone() : null;
-            // If locale has changed, then layout direction is also changed ...
-            changed |= ActivityInfo.CONFIG_LAYOUT_DIRECTION;
-            // ... and we need to update the layout direction (represented by the first
-            // 2 most significant bits in screenLayout).
-            setLayoutDirection(locale);
+            mLocaleList = delta.mLocaleList;
+            // delta.locale can't be null, since delta.mLocaleList is not empty.
+            if (!delta.locale.equals(locale)) {
+                locale = (Locale) delta.locale.clone();
+                // If locale has changed, then layout direction is also changed ...
+                changed |= ActivityInfo.CONFIG_LAYOUT_DIRECTION;
+                // ... and we need to update the layout direction (represented by the first
+                // 2 most significant bits in screenLayout).
+                setLayoutDirection(locale);
+            }
         }
         final int deltaScreenLayoutDir = delta.screenLayout & SCREENLAYOUT_LAYOUTDIR_MASK;
         if (deltaScreenLayoutDir != SCREENLAYOUT_LAYOUTDIR_UNDEFINED &&
@@ -1023,8 +1047,9 @@
         if (delta.mnc != 0 && mnc != delta.mnc) {
             changed |= ActivityInfo.CONFIG_MNC;
         }
-        if (delta.locale != null
-                && (locale == null || !locale.equals(delta.locale))) {
+        fixUpLocaleList();
+        delta.fixUpLocaleList();
+        if (!mLocaleList.equals(delta.mLocaleList)) {
             changed |= ActivityInfo.CONFIG_LOCALE;
             changed |= ActivityInfo.CONFIG_LAYOUT_DIRECTION;
         }
@@ -1146,14 +1171,15 @@
         dest.writeFloat(fontScale);
         dest.writeInt(mcc);
         dest.writeInt(mnc);
-        if (locale == null) {
-            dest.writeInt(0);
-        } else {
-            dest.writeInt(1);
-            dest.writeString(locale.getLanguage());
-            dest.writeString(locale.getCountry());
-            dest.writeString(locale.getVariant());
+
+        fixUpLocaleList();
+        final int localeListSize = mLocaleList.size();
+        dest.writeInt(localeListSize);
+        for (int i = 0; i < localeListSize; ++i) {
+            final Locale l = mLocaleList.get(i);
+            dest.writeString(l.toLanguageTag());
         }
+
         if(userSetLocale) {
             dest.writeInt(1);
         } else {
@@ -1182,10 +1208,15 @@
         fontScale = source.readFloat();
         mcc = source.readInt();
         mnc = source.readInt();
-        if (source.readInt() != 0) {
-            locale = new Locale(source.readString(), source.readString(),
-                    source.readString());
+
+        final int localeListSize = source.readInt();
+        final Locale[] localeArray = new Locale[localeListSize];
+        for (int i = 0; i < localeListSize; ++i) {
+            localeArray[i] = Locale.forLanguageTag(source.readString());
         }
+        mLocaleList = new LocaleList(localeArray);
+        locale = mLocaleList.getPrimary();
+
         userSetLocale = (source.readInt()==1);
         touchscreen = source.readInt();
         keyboard = source.readInt();
@@ -1234,18 +1265,33 @@
         if (n != 0) return n;
         n = this.mnc - that.mnc;
         if (n != 0) return n;
-        if (this.locale == null) {
-            if (that.locale != null) return 1;
-        } else if (that.locale == null) {
+
+        fixUpLocaleList();
+        that.fixUpLocaleList();
+        // for backward compatibility, we consider an empty locale list to be greater
+        // than any non-empty locale list.
+        if (this.mLocaleList.isEmpty()) {
+            if (!that.mLocaleList.isEmpty()) return 1;
+        } else if (that.mLocaleList.isEmpty()) {
             return -1;
         } else {
-            n = this.locale.getLanguage().compareTo(that.locale.getLanguage());
-            if (n != 0) return n;
-            n = this.locale.getCountry().compareTo(that.locale.getCountry());
-            if (n != 0) return n;
-            n = this.locale.getVariant().compareTo(that.locale.getVariant());
+            final int minSize = Math.min(this.mLocaleList.size(), that.mLocaleList.size());
+            for (int i = 0; i < minSize; ++i) {
+                final Locale thisLocale = this.mLocaleList.get(i);
+                final Locale thatLocale = that.mLocaleList.get(i);
+                n = thisLocale.getLanguage().compareTo(thatLocale.getLanguage());
+                if (n != 0) return n;
+                n = thisLocale.getCountry().compareTo(thatLocale.getCountry());
+                if (n != 0) return n;
+                n = thisLocale.getVariant().compareTo(thatLocale.getVariant());
+                if (n != 0) return n;
+                n = thisLocale.toLanguageTag().compareTo(thatLocale.toLanguageTag());
+                if (n != 0) return n;
+            }
+            n = this.mLocaleList.size() - that.mLocaleList.size();
             if (n != 0) return n;
         }
+
         n = this.touchscreen - that.touchscreen;
         if (n != 0) return n;
         n = this.keyboard - that.keyboard;
@@ -1288,13 +1334,13 @@
         }
         return false;
     }
-    
+
     public int hashCode() {
         int result = 17;
         result = 31 * result + Float.floatToIntBits(fontScale);
         result = 31 * result + mcc;
         result = 31 * result + mnc;
-        result = 31 * result + (locale != null ? locale.hashCode() : 0);
+        result = 31 * result + mLocaleList.hashCode();
         result = 31 * result + touchscreen;
         result = 31 * result + keyboard;
         result = 31 * result + keyboardHidden;
@@ -1312,14 +1358,47 @@
     }
 
     /**
-     * Set the locale. This is the preferred way for setting up the locale (instead of using the
-     * direct accessor). This will also set the layout direction according to the locale.
+     * Get the locale list. This is the preferred way for getting the locales (instead of using
+     * the direct accessor to {@link #locale}, which would only provide the primary locale).
+     *
+     * @return The locale list.
+     */
+    public LocaleList getLocales() {
+        fixUpLocaleList();
+        return mLocaleList;
+    }
+
+    /**
+     * Set the locale list. This is the preferred way for setting up the locales (instead of using
+     * the direct accessor or {@link #setLocale(Locale)}). This will also set the layout direction
+     * according to the first locale in the list.
+     *
+     * Note that the layout direction will always come from the first locale in the locale list,
+     * even if the locale is not supported by the resources (the resources may only support
+     * another locale further down the list which has a different direction).
+     *
+     * @param locales The locale list. If null, an empty LocaleList will be assigned.
+     */
+    public void setLocales(@Nullable LocaleList locales) {
+        mLocaleList = locales == null ? LocaleList.getEmptyLocaleList() : locales;
+        locale = mLocaleList.getPrimary();
+        setLayoutDirection(locale);
+    }
+
+    /**
+     * Set the locale list to a list of just one locale. This will also set the layout direction
+     * according to the locale.
+     *
+     * Note that after this is run, calling <code>.equals()</code> on the input locale and the
+     * {@link #locale} attribute would return <code>true</code> if they are not null, but there is
+     * no guarantee that they would be the same object.
+     *
+     * See also the note about layout direction in {@link #setLocales(LocaleList)}.
      *
      * @param loc The locale. Can be null.
      */
-    public void setLocale(Locale loc) {
-        locale = loc;
-        setLayoutDirection(locale);
+    public void setLocale(@Nullable Locale loc) {
+        setLocales(new LocaleList(loc));
     }
 
     /**
@@ -1335,19 +1414,19 @@
     }
 
     /**
-     * Set the layout direction from the Locale.
+     * Set the layout direction from a Locale.
      *
-     * @param locale The Locale. If null will set the layout direction to
+     * @param loc The Locale. If null will set the layout direction to
      * {@link View#LAYOUT_DIRECTION_LTR}. If not null will set it to the layout direction
      * corresponding to the Locale.
      *
      * @see View#LAYOUT_DIRECTION_LTR
      * @see View#LAYOUT_DIRECTION_RTL
      */
-    public void setLayoutDirection(Locale locale) {
+    public void setLayoutDirection(Locale loc) {
         // There is a "1" difference between the configuration values for
         // layout direction and View constants for layout direction, just add "1".
-        final int layoutDirection = 1 + TextUtils.getLayoutDirectionFromLocale(locale);
+        final int layoutDirection = 1 + TextUtils.getLayoutDirectionFromLocale(loc);
         screenLayout = (screenLayout&~SCREENLAYOUT_LAYOUTDIR_MASK)|
                 (layoutDirection << SCREENLAYOUT_LAYOUTDIR_SHIFT);
     }
@@ -1370,21 +1449,21 @@
      *
      * @hide
      */
-    public static String localeToResourceQualifier(Locale locale) {
+    public static String localeToResourceQualifier(Locale loc) {
         StringBuilder sb = new StringBuilder();
-        boolean l = (locale.getLanguage().length() != 0);
-        boolean c = (locale.getCountry().length() != 0);
-        boolean s = (locale.getScript().length() != 0);
-        boolean v = (locale.getVariant().length() != 0);
-
+        boolean l = (loc.getLanguage().length() != 0);
+        boolean c = (loc.getCountry().length() != 0);
+        boolean s = (loc.getScript().length() != 0);
+        boolean v = (loc.getVariant().length() != 0);
+        // TODO: take script and extensions into account
         if (l) {
-            sb.append(locale.getLanguage());
+            sb.append(loc.getLanguage());
             if (c) {
-                sb.append("-r").append(locale.getCountry());
+                sb.append("-r").append(loc.getCountry());
                 if (s) {
-                    sb.append("-s").append(locale.getScript());
+                    sb.append("-s").append(loc.getScript());
                     if (v) {
-                        sb.append("-v").append(locale.getVariant());
+                        sb.append("-v").append(loc.getVariant());
                     }
                 }
             }
@@ -1409,6 +1488,7 @@
             }
         }
 
+        // TODO: send the whole locale list
         if (config.locale != null && !config.locale.getLanguage().isEmpty()) {
             parts.add(localeToResourceQualifier(config.locale));
         }
@@ -1646,8 +1726,10 @@
             delta.mnc = change.mnc;
         }
 
-        if ((base.locale == null && change.locale != null) ||
-                (base.locale != null && !base.locale.equals(change.locale)))  {
+        base.fixUpLocaleList();
+        change.fixUpLocaleList();
+        if (!base.mLocaleList.equals(change.mLocaleList))  {
+            delta.mLocaleList = change.mLocaleList;
             delta.locale = change.locale;
         }
 
@@ -1724,7 +1806,7 @@
     private static final String XML_ATTR_FONT_SCALE = "fs";
     private static final String XML_ATTR_MCC = "mcc";
     private static final String XML_ATTR_MNC = "mnc";
-    private static final String XML_ATTR_LOCALE = "locale";
+    private static final String XML_ATTR_LOCALES = "locales";
     private static final String XML_ATTR_TOUCHSCREEN = "touch";
     private static final String XML_ATTR_KEYBOARD = "key";
     private static final String XML_ATTR_KEYBOARD_HIDDEN = "keyHid";
@@ -1754,10 +1836,9 @@
         configOut.mcc = XmlUtils.readIntAttribute(parser, XML_ATTR_MCC, 0);
         configOut.mnc = XmlUtils.readIntAttribute(parser, XML_ATTR_MNC, 0);
 
-        final String localeStr = XmlUtils.readStringAttribute(parser, XML_ATTR_LOCALE);
-        if (localeStr != null) {
-            configOut.locale = Locale.forLanguageTag(localeStr);
-        }
+        final String localesStr = XmlUtils.readStringAttribute(parser, XML_ATTR_LOCALES);
+        configOut.mLocaleList = LocaleList.forLanguageTags(localesStr);
+        configOut.locale = configOut.mLocaleList.getPrimary();
 
         configOut.touchscreen = XmlUtils.readIntAttribute(parser, XML_ATTR_TOUCHSCREEN,
                 TOUCHSCREEN_UNDEFINED);
@@ -1807,8 +1888,9 @@
         if (config.mnc != 0) {
             XmlUtils.writeIntAttribute(xml, XML_ATTR_MNC, config.mnc);
         }
-        if (config.locale != null) {
-            XmlUtils.writeStringAttribute(xml, XML_ATTR_LOCALE, config.locale.toLanguageTag());
+        config.fixUpLocaleList();
+        if (!config.mLocaleList.isEmpty()) {
+           XmlUtils.writeStringAttribute(xml, XML_ATTR_LOCALES, config.mLocaleList.toLanguageTags());
         }
         if (config.touchscreen != TOUCHSCREEN_UNDEFINED) {
             XmlUtils.writeIntAttribute(xml, XML_ATTR_TOUCHSCREEN, config.touchscreen);
diff --git a/core/java/android/util/LocaleList.java b/core/java/android/util/LocaleList.java
index 017735a..afae9ac 100644
--- a/core/java/android/util/LocaleList.java
+++ b/core/java/android/util/LocaleList.java
@@ -23,8 +23,8 @@
 
 // TODO: We don't except too many LocaleLists to exist at the same time, and
 // we need access to the data at native level, so we should pass the data
-// down to the native level, create a mapt of every list seen there, take a
-// pointer back, and just keep that pointed in the Java-level object, so
+// down to the native level, create a map of every list seen there, take a
+// pointer back, and just keep that pointer in the Java-level object, so
 // things could be copied very quickly.
 
 /**
@@ -34,6 +34,7 @@
 public final class LocaleList {
     private final Locale[] mList;
     private static final Locale[] sEmptyList = new Locale[0];
+    private static final LocaleList sEmptyLocaleList = new LocaleList();
 
     public Locale get(int location) {
         return location < mList.length ? mList[location] : null;
@@ -51,6 +52,60 @@
         return mList.length;
     }
 
+    @Override
+    public boolean equals(Object other) {
+        if (other == this)
+            return true;
+        if (!(other instanceof LocaleList))
+            return false;
+        final Locale[] otherList = ((LocaleList) other).mList;
+        if (mList.length != otherList.length)
+            return false;
+        for (int i = 0; i < mList.length; ++i) {
+            if (!mList[i].equals(otherList[i]))
+                return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 1;
+        for (int i = 0; i < mList.length; ++i) {
+            result = 31 * result + mList[i].hashCode();
+        }
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        for (int i = 0; i < mList.length; ++i) {
+            sb.append(mList[i]);
+            if (i < mList.length - 1) {
+                sb.append(',');
+            }
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    public String toLanguageTags() {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < mList.length; ++i) {
+            sb.append(mList[i].toLanguageTag());
+            if (i < mList.length - 1) {
+                sb.append(',');
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * It is almost always better to call {@link #getEmptyLocaleList()} instead which returns
+     * a pre-constructed empty locale list.
+     */
     public LocaleList() {
         mList = sEmptyList;
     }
@@ -59,6 +114,19 @@
      * @throws NullPointerException if any of the input locales is <code>null</code>.
      * @throws IllegalArgumentException if any of the input locales repeat.
      */
+    public LocaleList(@Nullable Locale locale) {
+        if (locale == null) {
+            mList = sEmptyList;
+        } else {
+            mList = new Locale[1];
+            mList[0] = (Locale) locale.clone();
+        }
+    }
+
+    /**
+     * @throws NullPointerException if any of the input locales is <code>null</code>.
+     * @throws IllegalArgumentException if any of the input locales repeat.
+     */
     public LocaleList(@Nullable Locale[] list) {
         if (list == null || list.length == 0) {
             mList = sEmptyList;
@@ -79,4 +147,21 @@
             mList = localeList;
         }
     }
+
+    public static LocaleList getEmptyLocaleList() {
+        return sEmptyLocaleList;
+    }
+
+    public static LocaleList forLanguageTags(@Nullable String list) {
+        if (list == null || list.equals("")) {
+            return getEmptyLocaleList();
+        } else {
+            final String[] tags = list.split(",");
+            final Locale[] localeArray = new Locale[tags.length];
+            for (int i = 0; i < localeArray.length; ++i) {
+                localeArray[i] = Locale.forLanguageTag(tags[i]);
+            }
+            return new LocaleList(localeArray);
+        }
+    }
 }