Home | History | Annotate | Download | only in os
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.os;
     18 
     19 import android.annotation.IntRange;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.annotation.Size;
     23 import android.content.LocaleProto;
     24 import android.icu.util.ULocale;
     25 import android.util.proto.ProtoOutputStream;
     26 
     27 import com.android.internal.annotations.GuardedBy;
     28 
     29 import java.util.Arrays;
     30 import java.util.Collection;
     31 import java.util.HashSet;
     32 import java.util.Locale;
     33 
     34 /**
     35  * LocaleList is an immutable list of Locales, typically used to keep an ordered list of user
     36  * preferences for locales.
     37  */
     38 public final class LocaleList implements Parcelable {
     39     private final Locale[] mList;
     40     // This is a comma-separated list of the locales in the LocaleList created at construction time,
     41     // basically the result of running each locale's toLanguageTag() method and concatenating them
     42     // with commas in between.
     43     @NonNull
     44     private final String mStringRepresentation;
     45 
     46     private static final Locale[] sEmptyList = new Locale[0];
     47     private static final LocaleList sEmptyLocaleList = new LocaleList();
     48 
     49     /**
     50      * Retrieves the {@link Locale} at the specified index.
     51      *
     52      * @param index The position to retrieve.
     53      * @return The {@link Locale} in the given index.
     54      */
     55     public Locale get(int index) {
     56         return (0 <= index && index < mList.length) ? mList[index] : null;
     57     }
     58 
     59     /**
     60      * Returns whether the {@link LocaleList} contains no {@link Locale} items.
     61      *
     62      * @return {@code true} if this {@link LocaleList} has no {@link Locale} items, {@code false}
     63      *     otherwise.
     64      */
     65     public boolean isEmpty() {
     66         return mList.length == 0;
     67     }
     68 
     69     /**
     70      * Returns the number of {@link Locale} items in this {@link LocaleList}.
     71      */
     72     @IntRange(from=0)
     73     public int size() {
     74         return mList.length;
     75     }
     76 
     77     /**
     78      * Searches this {@link LocaleList} for the specified {@link Locale} and returns the index of
     79      * the first occurrence.
     80      *
     81      * @param locale The {@link Locale} to search for.
     82      * @return The index of the first occurrence of the {@link Locale} or {@code -1} if the item
     83      *     wasn't found.
     84      */
     85     @IntRange(from=-1)
     86     public int indexOf(Locale locale) {
     87         for (int i = 0; i < mList.length; i++) {
     88             if (mList[i].equals(locale)) {
     89                 return i;
     90             }
     91         }
     92         return -1;
     93     }
     94 
     95     @Override
     96     public boolean equals(Object other) {
     97         if (other == this)
     98             return true;
     99         if (!(other instanceof LocaleList))
    100             return false;
    101         final Locale[] otherList = ((LocaleList) other).mList;
    102         if (mList.length != otherList.length)
    103             return false;
    104         for (int i = 0; i < mList.length; i++) {
    105             if (!mList[i].equals(otherList[i]))
    106                 return false;
    107         }
    108         return true;
    109     }
    110 
    111     @Override
    112     public int hashCode() {
    113         int result = 1;
    114         for (int i = 0; i < mList.length; i++) {
    115             result = 31 * result + mList[i].hashCode();
    116         }
    117         return result;
    118     }
    119 
    120     @Override
    121     public String toString() {
    122         StringBuilder sb = new StringBuilder();
    123         sb.append("[");
    124         for (int i = 0; i < mList.length; i++) {
    125             sb.append(mList[i]);
    126             if (i < mList.length - 1) {
    127                 sb.append(',');
    128             }
    129         }
    130         sb.append("]");
    131         return sb.toString();
    132     }
    133 
    134     @Override
    135     public int describeContents() {
    136         return 0;
    137     }
    138 
    139     @Override
    140     public void writeToParcel(Parcel dest, int parcelableFlags) {
    141         dest.writeString(mStringRepresentation);
    142     }
    143 
    144     /**
    145      * Helper to write LocaleList to a protocol buffer output stream.  Assumes the parent
    146      * protobuf has declared the locale as repeated.
    147      *
    148      * @param protoOutputStream Stream to write the locale to.
    149      * @param fieldId Field Id of the Locale as defined in the parent message.
    150      * @hide
    151      */
    152     public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) {
    153         for (int i = 0; i < mList.length; i++) {
    154             final Locale locale = mList[i];
    155             final long token = protoOutputStream.start(fieldId);
    156             protoOutputStream.write(LocaleProto.LANGUAGE, locale.getLanguage());
    157             protoOutputStream.write(LocaleProto.COUNTRY, locale.getCountry());
    158             protoOutputStream.write(LocaleProto.VARIANT, locale.getVariant());
    159             protoOutputStream.end(token);
    160         }
    161     }
    162 
    163     /**
    164      * Retrieves a String representation of the language tags in this list.
    165      */
    166     @NonNull
    167     public String toLanguageTags() {
    168         return mStringRepresentation;
    169     }
    170 
    171     /**
    172      * Creates a new {@link LocaleList}.
    173      *
    174      * <p>For empty lists of {@link Locale} items it is better to use {@link #getEmptyLocaleList()},
    175      * which returns a pre-constructed empty list.</p>
    176      *
    177      * @throws NullPointerException if any of the input locales is <code>null</code>.
    178      * @throws IllegalArgumentException if any of the input locales repeat.
    179      */
    180     public LocaleList(@NonNull Locale... list) {
    181         if (list.length == 0) {
    182             mList = sEmptyList;
    183             mStringRepresentation = "";
    184         } else {
    185             final Locale[] localeList = new Locale[list.length];
    186             final HashSet<Locale> seenLocales = new HashSet<Locale>();
    187             final StringBuilder sb = new StringBuilder();
    188             for (int i = 0; i < list.length; i++) {
    189                 final Locale l = list[i];
    190                 if (l == null) {
    191                     throw new NullPointerException("list[" + i + "] is null");
    192                 } else if (seenLocales.contains(l)) {
    193                     throw new IllegalArgumentException("list[" + i + "] is a repetition");
    194                 } else {
    195                     final Locale localeClone = (Locale) l.clone();
    196                     localeList[i] = localeClone;
    197                     sb.append(localeClone.toLanguageTag());
    198                     if (i < list.length - 1) {
    199                         sb.append(',');
    200                     }
    201                     seenLocales.add(localeClone);
    202                 }
    203             }
    204             mList = localeList;
    205             mStringRepresentation = sb.toString();
    206         }
    207     }
    208 
    209     /**
    210      * Constructs a locale list, with the topLocale moved to the front if it already is
    211      * in otherLocales, or added to the front if it isn't.
    212      *
    213      * {@hide}
    214      */
    215     public LocaleList(@NonNull Locale topLocale, LocaleList otherLocales) {
    216         if (topLocale == null) {
    217             throw new NullPointerException("topLocale is null");
    218         }
    219 
    220         final int inputLength = (otherLocales == null) ? 0 : otherLocales.mList.length;
    221         int topLocaleIndex = -1;
    222         for (int i = 0; i < inputLength; i++) {
    223             if (topLocale.equals(otherLocales.mList[i])) {
    224                 topLocaleIndex = i;
    225                 break;
    226             }
    227         }
    228 
    229         final int outputLength = inputLength + (topLocaleIndex == -1 ? 1 : 0);
    230         final Locale[] localeList = new Locale[outputLength];
    231         localeList[0] = (Locale) topLocale.clone();
    232         if (topLocaleIndex == -1) {
    233             // topLocale was not in otherLocales
    234             for (int i = 0; i < inputLength; i++) {
    235                 localeList[i + 1] = (Locale) otherLocales.mList[i].clone();
    236             }
    237         } else {
    238             for (int i = 0; i < topLocaleIndex; i++) {
    239                 localeList[i + 1] = (Locale) otherLocales.mList[i].clone();
    240             }
    241             for (int i = topLocaleIndex + 1; i < inputLength; i++) {
    242                 localeList[i] = (Locale) otherLocales.mList[i].clone();
    243             }
    244         }
    245 
    246         final StringBuilder sb = new StringBuilder();
    247         for (int i = 0; i < outputLength; i++) {
    248             sb.append(localeList[i].toLanguageTag());
    249             if (i < outputLength - 1) {
    250                 sb.append(',');
    251             }
    252         }
    253 
    254         mList = localeList;
    255         mStringRepresentation = sb.toString();
    256     }
    257 
    258     public static final Parcelable.Creator<LocaleList> CREATOR
    259             = new Parcelable.Creator<LocaleList>() {
    260         @Override
    261         public LocaleList createFromParcel(Parcel source) {
    262             return LocaleList.forLanguageTags(source.readString());
    263         }
    264 
    265         @Override
    266         public LocaleList[] newArray(int size) {
    267             return new LocaleList[size];
    268         }
    269     };
    270 
    271     /**
    272      * Retrieve an empty instance of {@link LocaleList}.
    273      */
    274     @NonNull
    275     public static LocaleList getEmptyLocaleList() {
    276         return sEmptyLocaleList;
    277     }
    278 
    279     /**
    280      * Generates a new LocaleList with the given language tags.
    281      *
    282      * @param list The language tags to be included as a single {@link String} separated by commas.
    283      * @return A new instance with the {@link Locale} items identified by the given tags.
    284      */
    285     @NonNull
    286     public static LocaleList forLanguageTags(@Nullable String list) {
    287         if (list == null || list.equals("")) {
    288             return getEmptyLocaleList();
    289         } else {
    290             final String[] tags = list.split(",");
    291             final Locale[] localeArray = new Locale[tags.length];
    292             for (int i = 0; i < localeArray.length; i++) {
    293                 localeArray[i] = Locale.forLanguageTag(tags[i]);
    294             }
    295             return new LocaleList(localeArray);
    296         }
    297     }
    298 
    299     private static String getLikelyScript(Locale locale) {
    300         final String script = locale.getScript();
    301         if (!script.isEmpty()) {
    302             return script;
    303         } else {
    304             // TODO: Cache the results if this proves to be too slow
    305             return ULocale.addLikelySubtags(ULocale.forLocale(locale)).getScript();
    306         }
    307     }
    308 
    309     private static final String STRING_EN_XA = "en-XA";
    310     private static final String STRING_AR_XB = "ar-XB";
    311     private static final Locale LOCALE_EN_XA = new Locale("en", "XA");
    312     private static final Locale LOCALE_AR_XB = new Locale("ar", "XB");
    313     private static final int NUM_PSEUDO_LOCALES = 2;
    314 
    315     private static boolean isPseudoLocale(String locale) {
    316         return STRING_EN_XA.equals(locale) || STRING_AR_XB.equals(locale);
    317     }
    318 
    319     /**
    320      * Returns true if locale is a pseudo-locale, false otherwise.
    321      * {@hide}
    322      */
    323     public static boolean isPseudoLocale(Locale locale) {
    324         return LOCALE_EN_XA.equals(locale) || LOCALE_AR_XB.equals(locale);
    325     }
    326 
    327     @IntRange(from=0, to=1)
    328     private static int matchScore(Locale supported, Locale desired) {
    329         if (supported.equals(desired)) {
    330             return 1;  // return early so we don't do unnecessary computation
    331         }
    332         if (!supported.getLanguage().equals(desired.getLanguage())) {
    333             return 0;
    334         }
    335         if (isPseudoLocale(supported) || isPseudoLocale(desired)) {
    336             // The locales are not the same, but the languages are the same, and one of the locales
    337             // is a pseudo-locale. So this is not a match.
    338             return 0;
    339         }
    340         final String supportedScr = getLikelyScript(supported);
    341         if (supportedScr.isEmpty()) {
    342             // If we can't guess a script, we don't know enough about the locales' language to find
    343             // if the locales match. So we fall back to old behavior of matching, which considered
    344             // locales with different regions different.
    345             final String supportedRegion = supported.getCountry();
    346             return (supportedRegion.isEmpty() ||
    347                     supportedRegion.equals(desired.getCountry()))
    348                     ? 1 : 0;
    349         }
    350         final String desiredScr = getLikelyScript(desired);
    351         // There is no match if the two locales use different scripts. This will most imporantly
    352         // take care of traditional vs simplified Chinese.
    353         return supportedScr.equals(desiredScr) ? 1 : 0;
    354     }
    355 
    356     private int findFirstMatchIndex(Locale supportedLocale) {
    357         for (int idx = 0; idx < mList.length; idx++) {
    358             final int score = matchScore(supportedLocale, mList[idx]);
    359             if (score > 0) {
    360                 return idx;
    361             }
    362         }
    363         return Integer.MAX_VALUE;
    364     }
    365 
    366     private static final Locale EN_LATN = Locale.forLanguageTag("en-Latn");
    367 
    368     private int computeFirstMatchIndex(Collection<String> supportedLocales,
    369             boolean assumeEnglishIsSupported) {
    370         if (mList.length == 1) {  // just one locale, perhaps the most common scenario
    371             return 0;
    372         }
    373         if (mList.length == 0) {  // empty locale list
    374             return -1;
    375         }
    376 
    377         int bestIndex = Integer.MAX_VALUE;
    378         // Try English first, so we can return early if it's in the LocaleList
    379         if (assumeEnglishIsSupported) {
    380             final int idx = findFirstMatchIndex(EN_LATN);
    381             if (idx == 0) { // We have a match on the first locale, which is good enough
    382                 return 0;
    383             } else if (idx < bestIndex) {
    384                 bestIndex = idx;
    385             }
    386         }
    387         for (String languageTag : supportedLocales) {
    388             final Locale supportedLocale = Locale.forLanguageTag(languageTag);
    389             // We expect the average length of locale lists used for locale resolution to be
    390             // smaller than three, so it's OK to do this as an O(mn) algorithm.
    391             final int idx = findFirstMatchIndex(supportedLocale);
    392             if (idx == 0) { // We have a match on the first locale, which is good enough
    393                 return 0;
    394             } else if (idx < bestIndex) {
    395                 bestIndex = idx;
    396             }
    397         }
    398         if (bestIndex == Integer.MAX_VALUE) {
    399             // no match was found, so we fall back to the first locale in the locale list
    400             return 0;
    401         } else {
    402             return bestIndex;
    403         }
    404     }
    405 
    406     private Locale computeFirstMatch(Collection<String> supportedLocales,
    407             boolean assumeEnglishIsSupported) {
    408         int bestIndex = computeFirstMatchIndex(supportedLocales, assumeEnglishIsSupported);
    409         return bestIndex == -1 ? null : mList[bestIndex];
    410     }
    411 
    412     /**
    413      * Returns the first match in the locale list given an unordered array of supported locales
    414      * in BCP 47 format.
    415      *
    416      * @return The first {@link Locale} from this list that appears in the given array, or
    417      *     {@code null} if the {@link LocaleList} is empty.
    418      */
    419     @Nullable
    420     public Locale getFirstMatch(String[] supportedLocales) {
    421         return computeFirstMatch(Arrays.asList(supportedLocales),
    422                 false /* assume English is not supported */);
    423     }
    424 
    425     /**
    426      * {@hide}
    427      */
    428     public int getFirstMatchIndex(String[] supportedLocales) {
    429         return computeFirstMatchIndex(Arrays.asList(supportedLocales),
    430                 false /* assume English is not supported */);
    431     }
    432 
    433     /**
    434      * Same as getFirstMatch(), but with English assumed to be supported, even if it's not.
    435      * {@hide}
    436      */
    437     @Nullable
    438     public Locale getFirstMatchWithEnglishSupported(String[] supportedLocales) {
    439         return computeFirstMatch(Arrays.asList(supportedLocales),
    440                 true /* assume English is supported */);
    441     }
    442 
    443     /**
    444      * {@hide}
    445      */
    446     public int getFirstMatchIndexWithEnglishSupported(Collection<String> supportedLocales) {
    447         return computeFirstMatchIndex(supportedLocales, true /* assume English is supported */);
    448     }
    449 
    450     /**
    451      * {@hide}
    452      */
    453     public int getFirstMatchIndexWithEnglishSupported(String[] supportedLocales) {
    454         return getFirstMatchIndexWithEnglishSupported(Arrays.asList(supportedLocales));
    455     }
    456 
    457     /**
    458      * Returns true if the collection of locale tags only contains empty locales and pseudolocales.
    459      * Assumes that there is no repetition in the input.
    460      * {@hide}
    461      */
    462     public static boolean isPseudoLocalesOnly(@Nullable String[] supportedLocales) {
    463         if (supportedLocales == null) {
    464             return true;
    465         }
    466 
    467         if (supportedLocales.length > NUM_PSEUDO_LOCALES + 1) {
    468             // This is for optimization. Since there's no repetition in the input, if we have more
    469             // than the number of pseudo-locales plus one for the empty string, it's guaranteed
    470             // that we have some meaninful locale in the collection, so the list is not "practically
    471             // empty".
    472             return false;
    473         }
    474         for (String locale : supportedLocales) {
    475             if (!locale.isEmpty() && !isPseudoLocale(locale)) {
    476                 return false;
    477             }
    478         }
    479         return true;
    480     }
    481 
    482     private final static Object sLock = new Object();
    483 
    484     @GuardedBy("sLock")
    485     private static LocaleList sLastExplicitlySetLocaleList = null;
    486     @GuardedBy("sLock")
    487     private static LocaleList sDefaultLocaleList = null;
    488     @GuardedBy("sLock")
    489     private static LocaleList sDefaultAdjustedLocaleList = null;
    490     @GuardedBy("sLock")
    491     private static Locale sLastDefaultLocale = null;
    492 
    493     /**
    494      * The result is guaranteed to include the default Locale returned by Locale.getDefault(), but
    495      * not necessarily at the top of the list. The default locale not being at the top of the list
    496      * is an indication that the system has set the default locale to one of the user's other
    497      * preferred locales, having concluded that the primary preference is not supported but a
    498      * secondary preference is.
    499      *
    500      * <p>Note that the default LocaleList would change if Locale.setDefault() is called. This
    501      * method takes that into account by always checking the output of Locale.getDefault() and
    502      * recalculating the default LocaleList if needed.</p>
    503      */
    504     @NonNull @Size(min=1)
    505     public static LocaleList getDefault() {
    506         final Locale defaultLocale = Locale.getDefault();
    507         synchronized (sLock) {
    508             if (!defaultLocale.equals(sLastDefaultLocale)) {
    509                 sLastDefaultLocale = defaultLocale;
    510                 // It's either the first time someone has asked for the default locale list, or
    511                 // someone has called Locale.setDefault() since we last set or adjusted the default
    512                 // locale list. So let's recalculate the locale list.
    513                 if (sDefaultLocaleList != null
    514                         && defaultLocale.equals(sDefaultLocaleList.get(0))) {
    515                     // The default Locale has changed, but it happens to be the first locale in the
    516                     // default locale list, so we don't need to construct a new locale list.
    517                     return sDefaultLocaleList;
    518                 }
    519                 sDefaultLocaleList = new LocaleList(defaultLocale, sLastExplicitlySetLocaleList);
    520                 sDefaultAdjustedLocaleList = sDefaultLocaleList;
    521             }
    522             // sDefaultLocaleList can't be null, since it can't be set to null by
    523             // LocaleList.setDefault(), and if getDefault() is called before a call to
    524             // setDefault(), sLastDefaultLocale would be null and the check above would set
    525             // sDefaultLocaleList.
    526             return sDefaultLocaleList;
    527         }
    528     }
    529 
    530     /**
    531      * Returns the default locale list, adjusted by moving the default locale to its first
    532      * position.
    533      */
    534     @NonNull @Size(min=1)
    535     public static LocaleList getAdjustedDefault() {
    536         getDefault(); // to recalculate the default locale list, if necessary
    537         synchronized (sLock) {
    538             return sDefaultAdjustedLocaleList;
    539         }
    540     }
    541 
    542     /**
    543      * Also sets the default locale by calling Locale.setDefault() with the first locale in the
    544      * list.
    545      *
    546      * @throws NullPointerException if the input is <code>null</code>.
    547      * @throws IllegalArgumentException if the input is empty.
    548      */
    549     public static void setDefault(@NonNull @Size(min=1) LocaleList locales) {
    550         setDefault(locales, 0);
    551     }
    552 
    553     /**
    554      * This may be used directly by system processes to set the default locale list for apps. For
    555      * such uses, the default locale list would always come from the user preferences, but the
    556      * default locale may have been chosen to be a locale other than the first locale in the locale
    557      * list (based on the locales the app supports).
    558      *
    559      * {@hide}
    560      */
    561     public static void setDefault(@NonNull @Size(min=1) LocaleList locales, int localeIndex) {
    562         if (locales == null) {
    563             throw new NullPointerException("locales is null");
    564         }
    565         if (locales.isEmpty()) {
    566             throw new IllegalArgumentException("locales is empty");
    567         }
    568         synchronized (sLock) {
    569             sLastDefaultLocale = locales.get(localeIndex);
    570             Locale.setDefault(sLastDefaultLocale);
    571             sLastExplicitlySetLocaleList = locales;
    572             sDefaultLocaleList = locales;
    573             if (localeIndex == 0) {
    574                 sDefaultAdjustedLocaleList = sDefaultLocaleList;
    575             } else {
    576                 sDefaultAdjustedLocaleList = new LocaleList(
    577                         sLastDefaultLocale, sDefaultLocaleList);
    578             }
    579         }
    580     }
    581 }
    582