LocaleListHelper.java revision 01f7e32cdf00e64181661c34a3190b70ad4c79af
1b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood/*
2b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * Copyright (C) 2017 The Android Open Source Project
3b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood *
4b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * Licensed under the Apache License, Version 2.0 (the "License");
5b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * you may not use this file except in compliance with the License.
6b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * You may obtain a copy of the License at
7b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood *
8b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood *      http://www.apache.org/licenses/LICENSE-2.0
9b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood *
10b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * Unless required by applicable law or agreed to in writing, software
11b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * distributed under the License is distributed on an "AS IS" BASIS,
12b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * See the License for the specific language governing permissions and
14b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * limitations under the License.
15b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood */
16b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood
17b673770f7172d4fca9bc05de1f36bc53e93eb247Mike Lockwoodpackage android.support.v4.os;
18b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood
194a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwoodimport static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
2010024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood
214a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwoodimport android.os.Build;
2210024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwoodimport android.support.annotation.GuardedBy;
2310024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwoodimport android.support.annotation.IntRange;
24d1b16fe2fb7527eee214898263ec4d6dabbfb0b4Mike Lockwoodimport android.support.annotation.NonNull;
25d1b16fe2fb7527eee214898263ec4d6dabbfb0b4Mike Lockwoodimport android.support.annotation.Nullable;
264a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwoodimport android.support.annotation.RequiresApi;
274a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwoodimport android.support.annotation.RestrictTo;
2810024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwoodimport android.support.annotation.Size;
2910024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood
3020821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwoodimport java.util.Arrays;
3117bbb50edc8ccc56c4ecc932a19884c4cc1f5b6fMikhail Naganovimport java.util.Collection;
32b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwoodimport java.util.HashSet;
3310024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwoodimport java.util.Locale;
34b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood
35b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood/**
36a7e348eb4d2ef1632f2ebe3a742743607ccfd82bMike Lockwood * LocaleListHelper is an immutable list of Locales, typically used to keep an ordered list of user
37b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood * preferences for locales.
38be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood *
3910024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood * @hide
4010024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood */
41be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood@RestrictTo(LIBRARY_GROUP)
424a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood@RequiresApi(14)
4320821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwoodfinal class LocaleListHelper {
4410024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    private final Locale[] mList;
4511fd96d6ff25bc1d710448eab545fe09da55a5f5Mike Lockwood    // This is a comma-separated list of the locales in the LocaleListHelper created at construction
4610024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    // time, basically the result of running each locale's toLanguageTag() method and concatenating
474a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood    // them with commas in between.
48be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood    @NonNull
494a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood    private final String mStringRepresentation;
5010024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood
5110024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    private static final Locale[] sEmptyList = new Locale[0];
5210024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    private static final LocaleListHelper sEmptyLocaleList = new LocaleListHelper();
5310024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood
5410024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    /**
5520821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood     * Retrieves the {@link Locale} at the specified index.
56b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood     *
5710024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood     * @param index The position to retrieve.
5810024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood     * @return The {@link Locale} in the given index.
5910024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood     * @hide
6010024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood     */
6198cc8e5a6473b3a5802d97cc81020ec4e3cd23f3Mike Lockwood    @RestrictTo(LIBRARY_GROUP)
6210024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    Locale get(int index) {
6311fd96d6ff25bc1d710448eab545fe09da55a5f5Mike Lockwood        return (0 <= index && index < mList.length) ? mList[index] : null;
6410024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    }
65b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood
66b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood    /**
67b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood     * Returns whether the {@link LocaleListHelper} contains no {@link Locale} items.
68b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood     *
69b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood     * @return {@code true} if this {@link LocaleListHelper} has no {@link Locale} items,
70b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood     *         {@code false} otherwise.
71b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood     * @hide
72b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood     */
73b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood    @RestrictTo(LIBRARY_GROUP)
747eb441cb4abcd3230a4d243469c5044f49e707c8Mike Lockwood    boolean isEmpty() {
75b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood        return mList.length == 0;
76b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood    }
77b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood
78b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood    /**
79b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood     * Returns the number of {@link Locale} items in this {@link LocaleListHelper}.
80b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood     * @hide
81b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood     */
82b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood    @RestrictTo(LIBRARY_GROUP)
83b6f50d357bd3d4d296be6bb047f5ce93a79cbca1Mike Lockwood    @IntRange(from = 0)
8410024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    int size() {
8510024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood        return mList.length;
8611fd96d6ff25bc1d710448eab545fe09da55a5f5Mike Lockwood    }
87382cb48848cb3968d5a0088a825706d66cdfbd50Mikhail Naganov
8870a8147012f2f0e364424e788a11b8ad50f44421Phil Burk    /**
8910024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood     * Searches this {@link LocaleListHelper} for the specified {@link Locale} and returns the index
9010024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood     * of the first occurrence.
9110024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood     *
9210024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood     * @param locale The {@link Locale} to search for.
9310024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood     * @return The index of the first occurrence of the {@link Locale} or {@code -1} if the item
944a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood     *     wasn't found.
9517bbb50edc8ccc56c4ecc932a19884c4cc1f5b6fMikhail Naganov     * @hide
964a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood     */
974a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood    @RestrictTo(LIBRARY_GROUP)
9820821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood    @IntRange(from = -1)
9917bbb50edc8ccc56c4ecc932a19884c4cc1f5b6fMikhail Naganov    int indexOf(Locale locale) {
10011fd96d6ff25bc1d710448eab545fe09da55a5f5Mike Lockwood        for (int i = 0; i < mList.length; i++) {
1014a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood            if (mList[i].equals(locale)) {
1024a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood                return i;
1034a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood            }
10417bbb50edc8ccc56c4ecc932a19884c4cc1f5b6fMikhail Naganov        }
10517bbb50edc8ccc56c4ecc932a19884c4cc1f5b6fMikhail Naganov        return -1;
106b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood    }
107b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood
10820821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood    @Override
10920821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood    public boolean equals(Object other) {
11020821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood        if (other == this) {
11120821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood            return true;
11220821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood        }
11320821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood        if (!(other instanceof LocaleListHelper)) {
11420821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood            return false;
11520821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood        }
11620821ecbe81ba52b260ae232096bc2bfb3e92ad0Mike Lockwood        final Locale[] otherList = ((LocaleListHelper) other).mList;
11711fd96d6ff25bc1d710448eab545fe09da55a5f5Mike Lockwood        if (mList.length != otherList.length) {
1187eb441cb4abcd3230a4d243469c5044f49e707c8Mike Lockwood            return false;
11911fd96d6ff25bc1d710448eab545fe09da55a5f5Mike Lockwood        }
120b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood        for (int i = 0; i < mList.length; i++) {
121b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood            if (!mList[i].equals(otherList[i])) {
12211fd96d6ff25bc1d710448eab545fe09da55a5f5Mike Lockwood                return false;
1237eb441cb4abcd3230a4d243469c5044f49e707c8Mike Lockwood            }
12411fd96d6ff25bc1d710448eab545fe09da55a5f5Mike Lockwood        }
12510024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood        return true;
12610024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    }
12710024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood
12810024b3dc12a8552c1547b67810c77b865045cc8Mike Lockwood    @Override
129be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood    public int hashCode() {
130be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood        int result = 1;
131be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood        for (int i = 0; i < mList.length; i++) {
132be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood            result = 31 * result + mList[i].hashCode();
133be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood        }
134be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood        return result;
135be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood    }
136be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood
137be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood    @Override
138be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood    public String toString() {
139be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood        StringBuilder sb = new StringBuilder();
1404a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood        sb.append("[");
141be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood        for (int i = 0; i < mList.length; i++) {
1424a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood            sb.append(mList[i]);
1434a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood            if (i < mList.length - 1) {
1444a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood                sb.append(',');
1454a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood            }
1464a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood        }
1474a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood        sb.append("]");
148492e9e851cadca62df84eaff1a3c1ba788492fbaNarayan Kamath        return sb.toString();
149492e9e851cadca62df84eaff1a3c1ba788492fbaNarayan Kamath    }
150492e9e851cadca62df84eaff1a3c1ba788492fbaNarayan Kamath
151492e9e851cadca62df84eaff1a3c1ba788492fbaNarayan Kamath    /**
152be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood     * Retrieves a String representation of the language tags in this list.
153be215dd57282888b05b234c39bba44cc0a864b8aMike Lockwood     * @hide
1544a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood     */
1554a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood    @RestrictTo(LIBRARY_GROUP)
1564a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood    @NonNull
1574a3d7ed45d98ad2fe900221755845b87f26b554aMike Lockwood    String toLanguageTags() {
158b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood        return mStringRepresentation;
159b6b9a91c02b7a44cf56943e5358cee68fa4aece5Mike Lockwood    }
160
161    /**
162     * Creates a new {@link LocaleListHelper}.
163     *
164     * <p>For empty lists of {@link Locale} items it is better to use {@link #getEmptyLocaleList()},
165     * which returns a pre-constructed empty list.</p>
166     *
167     * @throws NullPointerException if any of the input locales is <code>null</code>.
168     * @throws IllegalArgumentException if any of the input locales repeat.
169     *
170     * @hide
171     */
172    @RestrictTo(LIBRARY_GROUP)
173    LocaleListHelper(@NonNull Locale... list) {
174        if (list.length == 0) {
175            mList = sEmptyList;
176            mStringRepresentation = "";
177        } else {
178            final Locale[] localeList = new Locale[list.length];
179            final HashSet<Locale> seenLocales = new HashSet<Locale>();
180            final StringBuilder sb = new StringBuilder();
181            for (int i = 0; i < list.length; i++) {
182                final Locale l = list[i];
183                if (l == null) {
184                    throw new NullPointerException("list[" + i + "] is null");
185                } else if (seenLocales.contains(l)) {
186                    throw new IllegalArgumentException("list[" + i + "] is a repetition");
187                } else {
188                    final Locale localeClone = (Locale) l.clone();
189                    localeList[i] = localeClone;
190                    sb.append(LocaleHelper.toLanguageTag(localeClone));
191                    if (i < list.length - 1) {
192                        sb.append(',');
193                    }
194                    seenLocales.add(localeClone);
195                }
196            }
197            mList = localeList;
198            mStringRepresentation = sb.toString();
199        }
200    }
201
202    /**
203     * Constructs a locale list, with the topLocale moved to the front if it already is
204     * in otherLocales, or added to the front if it isn't.
205     *
206     * @hide
207     */
208    @RestrictTo(LIBRARY_GROUP)
209    LocaleListHelper(@NonNull Locale topLocale, LocaleListHelper otherLocales) {
210        if (topLocale == null) {
211            throw new NullPointerException("topLocale is null");
212        }
213
214        final int inputLength = (otherLocales == null) ? 0 : otherLocales.mList.length;
215        int topLocaleIndex = -1;
216        for (int i = 0; i < inputLength; i++) {
217            if (topLocale.equals(otherLocales.mList[i])) {
218                topLocaleIndex = i;
219                break;
220            }
221        }
222
223        final int outputLength = inputLength + (topLocaleIndex == -1 ? 1 : 0);
224        final Locale[] localeList = new Locale[outputLength];
225        localeList[0] = (Locale) topLocale.clone();
226        if (topLocaleIndex == -1) {
227            // topLocale was not in otherLocales
228            for (int i = 0; i < inputLength; i++) {
229                localeList[i + 1] = (Locale) otherLocales.mList[i].clone();
230            }
231        } else {
232            for (int i = 0; i < topLocaleIndex; i++) {
233                localeList[i + 1] = (Locale) otherLocales.mList[i].clone();
234            }
235            for (int i = topLocaleIndex + 1; i < inputLength; i++) {
236                localeList[i] = (Locale) otherLocales.mList[i].clone();
237            }
238        }
239
240        final StringBuilder sb = new StringBuilder();
241        for (int i = 0; i < outputLength; i++) {
242            sb.append(LocaleHelper.toLanguageTag(localeList[i]));
243
244            if (i < outputLength - 1) {
245                sb.append(',');
246            }
247        }
248
249        mList = localeList;
250        mStringRepresentation = sb.toString();
251    }
252
253    /**
254     * Retrieve an empty instance of {@link LocaleListHelper}.
255     * @hide
256     */
257    @RestrictTo(LIBRARY_GROUP)
258    @NonNull
259    static LocaleListHelper getEmptyLocaleList() {
260        return sEmptyLocaleList;
261    }
262
263    /**
264     * Generates a new LocaleListHelper with the given language tags.
265     *
266     * @param list The language tags to be included as a single {@link String} separated by commas.
267     * @return A new instance with the {@link Locale} items identified by the given tags.
268     *
269     * @hide
270     */
271    @RestrictTo(LIBRARY_GROUP)
272    @NonNull
273    static LocaleListHelper forLanguageTags(@Nullable String list) {
274        if (list == null || list.equals("")) {
275            return getEmptyLocaleList();
276        } else {
277            final String[] tags = list.split(",");
278            final Locale[] localeArray = new Locale[tags.length];
279            for (int i = 0; i < localeArray.length; i++) {
280                localeArray[i] = LocaleHelper.forLanguageTag(tags[i]);
281            }
282            return new LocaleListHelper(localeArray);
283        }
284    }
285
286    private static String getLikelyScript(Locale locale) {
287        if (Build.VERSION.SDK_INT >= 21) {
288            final String script = locale.getScript();
289            if (!script.isEmpty()) {
290                return script;
291            } else {
292                return "";
293            }
294        }
295        return "";
296    }
297
298    private static final String STRING_EN_XA = "en-XA";
299    private static final String STRING_AR_XB = "ar-XB";
300    private static final Locale LOCALE_EN_XA = new Locale("en", "XA");
301    private static final Locale LOCALE_AR_XB = new Locale("ar", "XB");
302    private static final int NUM_PSEUDO_LOCALES = 2;
303
304    private static boolean isPseudoLocale(String locale) {
305        return STRING_EN_XA.equals(locale) || STRING_AR_XB.equals(locale);
306    }
307
308    private static boolean isPseudoLocale(Locale locale) {
309        return LOCALE_EN_XA.equals(locale) || LOCALE_AR_XB.equals(locale);
310    }
311
312    @IntRange(from = 0, to = 1)
313    private static int matchScore(Locale supported, Locale desired) {
314        if (supported.equals(desired)) {
315            return 1;  // return early so we don't do unnecessary computation
316        }
317        if (!supported.getLanguage().equals(desired.getLanguage())) {
318            return 0;
319        }
320        if (isPseudoLocale(supported) || isPseudoLocale(desired)) {
321            // The locales are not the same, but the languages are the same, and one of the locales
322            // is a pseudo-locale. So this is not a match.
323            return 0;
324        }
325        final String supportedScr = getLikelyScript(supported);
326        if (supportedScr.isEmpty()) {
327            // If we can't guess a script, we don't know enough about the locales' language to find
328            // if the locales match. So we fall back to old behavior of matching, which considered
329            // locales with different regions different.
330            final String supportedRegion = supported.getCountry();
331            return (supportedRegion.isEmpty() || supportedRegion.equals(desired.getCountry()))
332                    ? 1
333                    : 0;
334        }
335        final String desiredScr = getLikelyScript(desired);
336        // There is no match if the two locales use different scripts. This will most imporantly
337        // take care of traditional vs simplified Chinese.
338        return supportedScr.equals(desiredScr) ? 1 : 0;
339    }
340
341    private int findFirstMatchIndex(Locale supportedLocale) {
342        for (int idx = 0; idx < mList.length; idx++) {
343            final int score = matchScore(supportedLocale, mList[idx]);
344            if (score > 0) {
345                return idx;
346            }
347        }
348        return Integer.MAX_VALUE;
349    }
350
351    private static final Locale EN_LATN = LocaleHelper.forLanguageTag("en-Latn");
352
353    private int computeFirstMatchIndex(Collection<String> supportedLocales,
354            boolean assumeEnglishIsSupported) {
355        if (mList.length == 1) {  // just one locale, perhaps the most common scenario
356            return 0;
357        }
358        if (mList.length == 0) {  // empty locale list
359            return -1;
360        }
361
362        int bestIndex = Integer.MAX_VALUE;
363        // Try English first, so we can return early if it's in the LocaleListHelper
364        if (assumeEnglishIsSupported) {
365            final int idx = findFirstMatchIndex(EN_LATN);
366            if (idx == 0) { // We have a match on the first locale, which is good enough
367                return 0;
368            } else if (idx < bestIndex) {
369                bestIndex = idx;
370            }
371        }
372        for (String languageTag : supportedLocales) {
373            final Locale supportedLocale = LocaleHelper.forLanguageTag(languageTag);
374            // We expect the average length of locale lists used for locale resolution to be
375            // smaller than three, so it's OK to do this as an O(mn) algorithm.
376            final int idx = findFirstMatchIndex(supportedLocale);
377            if (idx == 0) { // We have a match on the first locale, which is good enough
378                return 0;
379            } else if (idx < bestIndex) {
380                bestIndex = idx;
381            }
382        }
383        if (bestIndex == Integer.MAX_VALUE) {
384            // no match was found, so we fall back to the first locale in the locale list
385            return 0;
386        } else {
387            return bestIndex;
388        }
389    }
390
391    private Locale computeFirstMatch(Collection<String> supportedLocales,
392            boolean assumeEnglishIsSupported) {
393        int bestIndex = computeFirstMatchIndex(supportedLocales, assumeEnglishIsSupported);
394        return bestIndex == -1 ? null : mList[bestIndex];
395    }
396
397    /**
398     * Returns the first match in the locale list given an unordered array of supported locales
399     * in BCP 47 format.
400     *
401     * @return The first {@link Locale} from this list that appears in the given array, or
402     *     {@code null} if the {@link LocaleListHelper} is empty.
403     *
404     * @hide
405     */
406    @RestrictTo(LIBRARY_GROUP)
407    @Nullable
408    Locale getFirstMatch(String[] supportedLocales) {
409        return computeFirstMatch(Arrays.asList(supportedLocales),
410                false /* assume English is not supported */);
411    }
412
413    /**
414     * @hide
415     */
416    @RestrictTo(LIBRARY_GROUP)
417    int getFirstMatchIndex(String[] supportedLocales) {
418        return computeFirstMatchIndex(Arrays.asList(supportedLocales),
419                false /* assume English is not supported */);
420    }
421
422    /**
423     * Same as getFirstMatch(), but with English assumed to be supported, even if it's not.
424     * @hide
425     */
426    @RestrictTo(LIBRARY_GROUP)
427    @Nullable
428    Locale getFirstMatchWithEnglishSupported(String[] supportedLocales) {
429        return computeFirstMatch(Arrays.asList(supportedLocales),
430                true /* assume English is supported */);
431    }
432
433    /**
434     * @hide
435     */
436    @RestrictTo(LIBRARY_GROUP)
437    int getFirstMatchIndexWithEnglishSupported(Collection<String> supportedLocales) {
438        return computeFirstMatchIndex(supportedLocales, true /* assume English is supported */);
439    }
440
441    /**
442     * @hide
443     */
444    @RestrictTo(LIBRARY_GROUP)
445    int getFirstMatchIndexWithEnglishSupported(String[] supportedLocales) {
446        return getFirstMatchIndexWithEnglishSupported(Arrays.asList(supportedLocales));
447    }
448
449    /**
450     * Returns true if the collection of locale tags only contains empty locales and pseudolocales.
451     * Assumes that there is no repetition in the input.
452     * @hide
453     */
454    @RestrictTo(LIBRARY_GROUP)
455    static boolean isPseudoLocalesOnly(@Nullable String[] supportedLocales) {
456        if (supportedLocales == null) {
457            return true;
458        }
459
460        if (supportedLocales.length > NUM_PSEUDO_LOCALES + 1) {
461            // This is for optimization. Since there's no repetition in the input, if we have more
462            // than the number of pseudo-locales plus one for the empty string, it's guaranteed
463            // that we have some meaninful locale in the collection, so the list is not "practically
464            // empty".
465            return false;
466        }
467        for (String locale : supportedLocales) {
468            if (!locale.isEmpty() && !isPseudoLocale(locale)) {
469                return false;
470            }
471        }
472        return true;
473    }
474
475    /** Lock for mutable static fields */
476    private final static Object sLock = new Object();
477
478    @GuardedBy("sLock")
479    private static LocaleListHelper sLastExplicitlySetLocaleList = null;
480    @GuardedBy("sLock")
481    private static LocaleListHelper sDefaultLocaleList = null;
482    @GuardedBy("sLock")
483    private static LocaleListHelper sDefaultAdjustedLocaleList = null;
484    @GuardedBy("sLock")
485    private static Locale sLastDefaultLocale = null;
486
487    /**
488     * The result is guaranteed to include the default Locale returned by Locale.getDefault(), but
489     * not necessarily at the top of the list. The default locale not being at the top of the list
490     * is an indication that the system has set the default locale to one of the user's other
491     * preferred locales, having concluded that the primary preference is not supported but a
492     * secondary preference is.
493     *
494     * <p>Note that the default LocaleListHelper would change if Locale.setDefault() is called. This
495     * method takes that into account by always checking the output of Locale.getDefault() and
496     * recalculating the default LocaleListHelper if needed.</p>
497     *
498     * @hide
499     */
500    @RestrictTo(LIBRARY_GROUP)
501    @NonNull @Size(min = 1)
502    static LocaleListHelper getDefault() {
503        final Locale defaultLocale = Locale.getDefault();
504        synchronized (sLock) {
505            if (!defaultLocale.equals(sLastDefaultLocale)) {
506                sLastDefaultLocale = defaultLocale;
507                // It's either the first time someone has asked for the default locale list, or
508                // someone has called Locale.setDefault() since we last set or adjusted the default
509                // locale list. So let's recalculate the locale list.
510                if (sDefaultLocaleList != null
511                        && defaultLocale.equals(sDefaultLocaleList.get(0))) {
512                    // The default Locale has changed, but it happens to be the first locale in the
513                    // default locale list, so we don't need to construct a new locale list.
514                    return sDefaultLocaleList;
515                }
516                sDefaultLocaleList = new LocaleListHelper(
517                        defaultLocale, sLastExplicitlySetLocaleList);
518                sDefaultAdjustedLocaleList = sDefaultLocaleList;
519            }
520            // sDefaultLocaleList can't be null, since it can't be set to null by
521            // LocaleListHelper.setDefault(), and if getDefault() is called before a call to
522            // setDefault(), sLastDefaultLocale would be null and the check above would set
523            // sDefaultLocaleList.
524            return sDefaultLocaleList;
525        }
526    }
527
528    /**
529     * Returns the default locale list, adjusted by moving the default locale to its first
530     * position.
531     */
532    @NonNull @Size(min = 1)
533    static LocaleListHelper getAdjustedDefault() {
534        getDefault(); // to recalculate the default locale list, if necessary
535        synchronized (sLock) {
536            return sDefaultAdjustedLocaleList;
537        }
538    }
539
540    /**
541     * Also sets the default locale by calling Locale.setDefault() with the first locale in the
542     * list.
543     *
544     * @throws NullPointerException if the input is <code>null</code>.
545     * @throws IllegalArgumentException if the input is empty.
546     *
547     * @hide
548     */
549    @RestrictTo(LIBRARY_GROUP)
550    static void setDefault(@NonNull @Size(min = 1) LocaleListHelper locales) {
551        setDefault(locales, 0);
552    }
553
554    /**
555     * This may be used directly by system processes to set the default locale list for apps. For
556     * such uses, the default locale list would always come from the user preferences, but the
557     * default locale may have been chosen to be a locale other than the first locale in the locale
558     * list (based on the locales the app supports).
559     *
560     * @hide
561     */
562    @RestrictTo(LIBRARY_GROUP)
563    static void setDefault(@NonNull @Size(min = 1) LocaleListHelper locales,
564            int localeIndex) {
565        if (locales == null) {
566            throw new NullPointerException("locales is null");
567        }
568        if (locales.isEmpty()) {
569            throw new IllegalArgumentException("locales is empty");
570        }
571        synchronized (sLock) {
572            sLastDefaultLocale = locales.get(localeIndex);
573            Locale.setDefault(sLastDefaultLocale);
574            sLastExplicitlySetLocaleList = locales;
575            sDefaultLocaleList = locales;
576            if (localeIndex == 0) {
577                sDefaultAdjustedLocaleList = sDefaultLocaleList;
578            } else {
579                sDefaultAdjustedLocaleList = new LocaleListHelper(
580                        sLastDefaultLocale, sDefaultLocaleList);
581            }
582        }
583    }
584}
585