diff --git a/api/current.txt b/api/current.txt index d2f80c7448fa7..9dcea2fbc6c09 100644 --- a/api/current.txt +++ b/api/current.txt @@ -39109,7 +39109,9 @@ package android.util { method public static android.util.LocaleList getEmptyLocaleList(); method public java.util.Locale getFirstMatch(java.lang.String[]); method public java.util.Locale getPrimary(); + method public int indexOf(java.util.Locale); method public boolean isEmpty(); + method public static void setDefault(android.util.LocaleList); method public int size(); method public java.lang.String toLanguageTags(); method public void writeToParcel(android.os.Parcel, int); diff --git a/api/system-current.txt b/api/system-current.txt index e83c90fc1edd4..db858acd69cd4 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -41457,7 +41457,9 @@ package android.util { method public static android.util.LocaleList getEmptyLocaleList(); method public java.util.Locale getFirstMatch(java.lang.String[]); method public java.util.Locale getPrimary(); + method public int indexOf(java.util.Locale); method public boolean isEmpty(); + method public static void setDefault(android.util.LocaleList); method public int size(); method public java.lang.String toLanguageTags(); method public void writeToParcel(android.os.Parcel, int); diff --git a/api/test-current.txt b/api/test-current.txt index b3a69df6b6de0..f2040d4c70570 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -39125,7 +39125,9 @@ package android.util { method public static android.util.LocaleList getEmptyLocaleList(); method public java.util.Locale getFirstMatch(java.lang.String[]); method public java.util.Locale getPrimary(); + method public int indexOf(java.util.Locale); method public boolean isEmpty(); + method public static void setDefault(android.util.LocaleList); method public int size(); method public java.lang.String toLanguageTags(); method public void writeToParcel(android.os.Parcel, int); diff --git a/core/java/android/util/LocaleList.java b/core/java/android/util/LocaleList.java index f22cde0c1ca77..24883e317955b 100644 --- a/core/java/android/util/LocaleList.java +++ b/core/java/android/util/LocaleList.java @@ -16,6 +16,7 @@ package android.util; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Size; @@ -56,10 +57,21 @@ public final class LocaleList implements Parcelable { return mList.length == 0; } + @IntRange(from=0) public int size() { return mList.length; } + @IntRange(from=-1) + public int indexOf(Locale locale) { + for (int i = 0; i < mList.length; i++) { + if (mList[i].equals(locale)) { + return i; + } + } + return -1; + } + @Override public boolean equals(Object other) { if (other == this) @@ -69,7 +81,7 @@ public final class LocaleList implements Parcelable { final Locale[] otherList = ((LocaleList) other).mList; if (mList.length != otherList.length) return false; - for (int i = 0; i < mList.length; ++i) { + for (int i = 0; i < mList.length; i++) { if (!mList[i].equals(otherList[i])) return false; } @@ -79,7 +91,7 @@ public final class LocaleList implements Parcelable { @Override public int hashCode() { int result = 1; - for (int i = 0; i < mList.length; ++i) { + for (int i = 0; i < mList.length; i++) { result = 31 * result + mList[i].hashCode(); } return result; @@ -89,7 +101,7 @@ public final class LocaleList implements Parcelable { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("["); - for (int i = 0; i < mList.length; ++i) { + for (int i = 0; i < mList.length; i++) { sb.append(mList[i]); if (i < mList.length - 1) { sb.append(','); @@ -150,12 +162,12 @@ public final class LocaleList implements Parcelable { final Locale[] localeList = new Locale[list.length]; final HashSet seenLocales = new HashSet(); final StringBuilder sb = new StringBuilder(); - for (int i = 0; i < list.length; ++i) { + for (int i = 0; i < list.length; i++) { final Locale l = list[i]; if (l == null) { - throw new NullPointerException(); + throw new NullPointerException("list[" + i + "] is null"); } else if (seenLocales.contains(l)) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException("list[" + i + "] is a repetition"); } else { final Locale localeClone = (Locale) l.clone(); localeList[i] = localeClone; @@ -171,6 +183,55 @@ public final class LocaleList implements Parcelable { } } + /** + * Constructs a locale list, with the topLocale moved to the front if it already is + * in otherLocales, or added to the front if it isn't. + * + * {@hide} + */ + public LocaleList(@NonNull Locale topLocale, LocaleList otherLocales) { + if (topLocale == null) { + throw new NullPointerException("topLocale is null"); + } + + final int inputLength = (otherLocales == null) ? 0 : otherLocales.mList.length; + int topLocaleIndex = -1; + for (int i = 0; i < inputLength; i++) { + if (topLocale.equals(otherLocales.mList[i])) { + topLocaleIndex = i; + break; + } + } + + final int outputLength = inputLength + (topLocaleIndex == -1 ? 1 : 0); + final Locale[] localeList = new Locale[outputLength]; + localeList[0] = (Locale) topLocale.clone(); + if (topLocaleIndex == -1) { + // topLocale was not in otherLocales + for (int i = 0; i < inputLength; i++) { + localeList[i + 1] = (Locale) otherLocales.mList[i].clone(); + } + } else { + for (int i = 0; i < topLocaleIndex; i++) { + localeList[i + 1] = (Locale) otherLocales.mList[i].clone(); + } + for (int i = topLocaleIndex + 1; i < inputLength; i++) { + localeList[i] = (Locale) otherLocales.mList[i].clone(); + } + } + + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < outputLength; i++) { + sb.append(localeList[i].toLanguageTag()); + if (i < outputLength - 1) { + sb.append(','); + } + } + + mList = localeList; + mStringRepresentation = sb.toString(); + } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override @@ -196,7 +257,7 @@ public final class LocaleList implements Parcelable { } else { final String[] tags = list.split(","); final Locale[] localeArray = new Locale[tags.length]; - for (int i = 0; i < localeArray.length; ++i) { + for (int i = 0; i < localeArray.length; i++) { localeArray[i] = Locale.forLanguageTag(tags[i]); } return new LocaleList(localeArray); @@ -227,6 +288,7 @@ public final class LocaleList implements Parcelable { return LOCALE_EN_XA.equals(locale) || LOCALE_AR_XB.equals(locale); } + @IntRange(from=0, to=1) private static int matchScore(Locale supported, Locale desired) { if (supported.equals(desired)) { return 1; // return early so we don't do unnecessary computation @@ -330,18 +392,79 @@ public final class LocaleList implements Parcelable { private final static Object sLock = new Object(); @GuardedBy("sLock") - private static LocaleList sDefaultLocaleList; + private static LocaleList sLastExplicitlySetLocaleList = null; + @GuardedBy("sLock") + private static LocaleList sDefaultLocaleList = null; + @GuardedBy("sLock") + private static Locale sLastDefaultLocale = null; - // TODO: fix this to return the default system locale list once we have that + /** + * The result is guaranteed to include the default Locale returned by Locale.getDefault(), but + * not necessarily at the top of the list. The default locale not being at the top of the list + * is an indication that the system has set the default locale to one of the user's other + * preferred locales, having concluded that the primary preference is not supported but a + * secondary preference is. + * + * Note that the default LocaleList would change if Locale.setDefault() is called. This method + * takes that into account by always checking the output of Locale.getDefault() and adjusting + * the default LocaleList if needed. + */ @NonNull @Size(min=1) public static LocaleList getDefault() { - Locale defaultLocale = Locale.getDefault(); + final Locale defaultLocale = Locale.getDefault(); synchronized (sLock) { - if (sDefaultLocaleList == null || sDefaultLocaleList.size() != 1 - || !defaultLocale.equals(sDefaultLocaleList.getPrimary())) { - sDefaultLocaleList = new LocaleList(defaultLocale); + if (!defaultLocale.equals(sLastDefaultLocale)) { + sLastDefaultLocale = defaultLocale; + // It's either the first time someone has asked for the default locale list, or + // someone has called Locale.setDefault() since we last set or adjusted the default + // locale list. So let's adjust the locale list. + if (sDefaultLocaleList != null + && defaultLocale.equals(sDefaultLocaleList.getPrimary())) { + // The default Locale has changed, but it happens to be the first locale in the + // default locale list, so we don't need to construct a new locale list. + return sDefaultLocaleList; + } + sDefaultLocaleList = new LocaleList(defaultLocale, sLastExplicitlySetLocaleList); } + // sDefaultLocaleList can't be null, since it can't be set to null by + // LocaleList.setDefault(), and if getDefault() is called before a call to + // setDefault(), sLastDefaultLocale would be null and the check above would set + // sDefaultLocaleList. + return sDefaultLocaleList; + } + } + + /** + * Also sets the default locale by calling Locale.setDefault() with the first locale in the + * list. + * + * @throws NullPointerException if the input is null. + * @throws IllegalArgumentException if the input is empty. + */ + public static void setDefault(@NonNull @Size(min=1) LocaleList locales) { + setDefault(locales, 0); + } + + /** + * This may be used directly by system processes to set the default locale list for apps. For + * such uses, the default locale list would always come from the user preferences, but the + * default locale may have been chosen to be a locale other than the first locale in the locale + * list (based on the locales the app supports). + * + * {@hide} + */ + public static void setDefault(@NonNull @Size(min=1) LocaleList locales, int localeIndex) { + if (locales == null) { + throw new NullPointerException("locales is null"); + } + if (locales.isEmpty()) { + throw new IllegalArgumentException("locales is empty"); + } + synchronized (sLock) { + sLastDefaultLocale = locales.get(localeIndex); + Locale.setDefault(sLastDefaultLocale); + sLastExplicitlySetLocaleList = locales; + sDefaultLocaleList = locales; } - return sDefaultLocaleList; } } diff --git a/core/tests/coretests/src/android/util/LocaleListTest.java b/core/tests/coretests/src/android/util/LocaleListTest.java new file mode 100644 index 0000000000000..de1382d5d45e8 --- /dev/null +++ b/core/tests/coretests/src/android/util/LocaleListTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 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.util; + +import android.test.suitebuilder.annotation.SmallTest; +import android.util.LocaleList; + +import java.util.Locale; + +import junit.framework.TestCase; + +public class LocaleListTest extends TestCase { + @SmallTest + public void testConstructor() throws Exception { + LocaleList ll; + ll = new LocaleList(Locale.forLanguageTag("fr"), null); + assertEquals("fr", ll.toLanguageTags()); + + ll = new LocaleList(Locale.forLanguageTag("fr"), LocaleList.getEmptyLocaleList()); + assertEquals("fr", ll.toLanguageTags()); + + ll = new LocaleList(Locale.forLanguageTag("fr"), LocaleList.forLanguageTags("fr")); + assertEquals("fr", ll.toLanguageTags()); + + ll = new LocaleList(Locale.forLanguageTag("fr"), LocaleList.forLanguageTags("de")); + assertEquals("fr,de", ll.toLanguageTags()); + + ll = new LocaleList(Locale.forLanguageTag("fr"), LocaleList.forLanguageTags("de,ja")); + assertEquals("fr,de,ja", ll.toLanguageTags()); + + ll = new LocaleList(Locale.forLanguageTag("fr"), LocaleList.forLanguageTags("de,fr,ja")); + assertEquals("fr,de,ja", ll.toLanguageTags()); + + ll = new LocaleList(Locale.forLanguageTag("fr"), LocaleList.forLanguageTags("de,fr")); + assertEquals("fr,de", ll.toLanguageTags()); + + ll = new LocaleList(Locale.forLanguageTag("fr"), LocaleList.forLanguageTags("fr,de")); + assertEquals("fr,de", ll.toLanguageTags()); + } + + @SmallTest + public void testConstructor_nullThrows() throws Exception { + try { + final LocaleList ll = new LocaleList(null, LocaleList.getEmptyLocaleList()); + fail("Constructing with locale and locale list should throw with a null locale."); + } catch (Throwable e) { + assertEquals(NullPointerException.class, e.getClass()); + } + } + + @SmallTest + public void testGetDefault_localeSetDefaultCalledButNoChangeNecessary() throws Exception { + final Locale originalLocale = Locale.getDefault(); + final LocaleList originalLocaleList = LocaleList.getDefault(); + final int originalLocaleIndex = originalLocaleList.indexOf(originalLocale); + + // This simulates a situation potentially set by the system processes + LocaleList.setDefault(LocaleList.forLanguageTags("ae,en,ja"), 1 /* en */); + + // check our assumptions about input + assertEquals("en", Locale.getDefault().toLanguageTag()); + final LocaleList firstResult = LocaleList.getDefault(); + assertEquals("ae,en,ja", LocaleList.getDefault().toLanguageTags()); + + Locale.setDefault(Locale.forLanguageTag("ae")); + assertSame(firstResult, LocaleList.getDefault()); + + // restore the original values + LocaleList.setDefault(originalLocaleList, originalLocaleIndex); + } +}