blob: a9da0806f9d073a605ed5ae100a20addf7c6eaa5 [file] [log] [blame]
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -07001/*
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
Yohei Yukawa23cbe852016-05-17 16:42:58 -070017package android.os;
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070018
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -080019import android.annotation.IntRange;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -070020import android.annotation.NonNull;
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070021import android.annotation.Nullable;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -070022import android.annotation.Size;
Maurice Lam522ecbe2018-12-04 20:21:40 -080023import android.annotation.SystemApi;
Dianne Hackbornbf5ba6b2018-02-20 10:31:02 -080024import android.content.LocaleProto;
Roozbeh Pournader2591cc82015-12-08 22:21:24 -080025import android.icu.util.ULocale;
Dianne Hackbornbf5ba6b2018-02-20 10:31:02 -080026import android.util.proto.ProtoOutputStream;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -070027
28import com.android.internal.annotations.GuardedBy;
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070029
Roozbeh Pournader834641b2016-01-23 22:34:57 -080030import java.util.Arrays;
31import java.util.Collection;
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070032import java.util.HashSet;
33import java.util.Locale;
34
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070035/**
Clara Bayarri66f6bd32016-04-26 12:18:36 +010036 * LocaleList is an immutable list of Locales, typically used to keep an ordered list of user
37 * preferences for locales.
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070038 */
Yohei Yukawa789d8fd2015-12-03 11:27:05 -080039public final class LocaleList implements Parcelable {
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070040 private final Locale[] mList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -070041 // This is a comma-separated list of the locales in the LocaleList created at construction time,
42 // basically the result of running each locale's toLanguageTag() method and concatenating them
43 // with commas in between.
Yohei Yukawa789d8fd2015-12-03 11:27:05 -080044 @NonNull
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -070045 private final String mStringRepresentation;
46
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070047 private static final Locale[] sEmptyList = new Locale[0];
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -070048 private static final LocaleList sEmptyLocaleList = new LocaleList();
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070049
Clara Bayarri66f6bd32016-04-26 12:18:36 +010050 /**
51 * Retrieves the {@link Locale} at the specified index.
52 *
53 * @param index The position to retrieve.
54 * @return The {@link Locale} in the given index.
55 */
56 public Locale get(int index) {
57 return (0 <= index && index < mList.length) ? mList[index] : null;
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070058 }
59
Clara Bayarri66f6bd32016-04-26 12:18:36 +010060 /**
61 * Returns whether the {@link LocaleList} contains no {@link Locale} items.
62 *
63 * @return {@code true} if this {@link LocaleList} has no {@link Locale} items, {@code false}
64 * otherwise.
65 */
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070066 public boolean isEmpty() {
67 return mList.length == 0;
68 }
69
Clara Bayarri66f6bd32016-04-26 12:18:36 +010070 /**
71 * Returns the number of {@link Locale} items in this {@link LocaleList}.
72 */
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -080073 @IntRange(from=0)
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070074 public int size() {
75 return mList.length;
76 }
77
Clara Bayarri66f6bd32016-04-26 12:18:36 +010078 /**
79 * Searches this {@link LocaleList} for the specified {@link Locale} and returns the index of
80 * the first occurrence.
81 *
82 * @param locale The {@link Locale} to search for.
83 * @return The index of the first occurrence of the {@link Locale} or {@code -1} if the item
84 * wasn't found.
85 */
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -080086 @IntRange(from=-1)
87 public int indexOf(Locale locale) {
88 for (int i = 0; i < mList.length; i++) {
89 if (mList[i].equals(locale)) {
90 return i;
91 }
92 }
93 return -1;
94 }
95
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -070096 @Override
97 public boolean equals(Object other) {
98 if (other == this)
99 return true;
100 if (!(other instanceof LocaleList))
101 return false;
102 final Locale[] otherList = ((LocaleList) other).mList;
103 if (mList.length != otherList.length)
104 return false;
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800105 for (int i = 0; i < mList.length; i++) {
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700106 if (!mList[i].equals(otherList[i]))
107 return false;
108 }
109 return true;
110 }
111
112 @Override
113 public int hashCode() {
114 int result = 1;
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800115 for (int i = 0; i < mList.length; i++) {
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700116 result = 31 * result + mList[i].hashCode();
117 }
118 return result;
119 }
120
121 @Override
122 public String toString() {
123 StringBuilder sb = new StringBuilder();
124 sb.append("[");
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800125 for (int i = 0; i < mList.length; i++) {
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700126 sb.append(mList[i]);
127 if (i < mList.length - 1) {
128 sb.append(',');
129 }
130 }
131 sb.append("]");
132 return sb.toString();
133 }
134
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800135 @Override
136 public int describeContents() {
137 return 0;
138 }
139
140 @Override
141 public void writeToParcel(Parcel dest, int parcelableFlags) {
142 dest.writeString(mStringRepresentation);
143 }
144
Clara Bayarri66f6bd32016-04-26 12:18:36 +0100145 /**
Dianne Hackbornbf5ba6b2018-02-20 10:31:02 -0800146 * Helper to write LocaleList to a protocol buffer output stream. Assumes the parent
147 * protobuf has declared the locale as repeated.
148 *
149 * @param protoOutputStream Stream to write the locale to.
150 * @param fieldId Field Id of the Locale as defined in the parent message.
151 * @hide
152 */
153 public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) {
154 for (int i = 0; i < mList.length; i++) {
155 final Locale locale = mList[i];
156 final long token = protoOutputStream.start(fieldId);
157 protoOutputStream.write(LocaleProto.LANGUAGE, locale.getLanguage());
158 protoOutputStream.write(LocaleProto.COUNTRY, locale.getCountry());
159 protoOutputStream.write(LocaleProto.VARIANT, locale.getVariant());
160 protoOutputStream.end(token);
161 }
162 }
163
164 /**
Clara Bayarri66f6bd32016-04-26 12:18:36 +0100165 * Retrieves a String representation of the language tags in this list.
166 */
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700167 @NonNull
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700168 public String toLanguageTags() {
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700169 return mStringRepresentation;
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700170 }
171
172 /**
Clara Bayarri66f6bd32016-04-26 12:18:36 +0100173 * Creates a new {@link LocaleList}.
174 *
175 * <p>For empty lists of {@link Locale} items it is better to use {@link #getEmptyLocaleList()},
176 * which returns a pre-constructed empty list.</p>
177 *
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700178 * @throws NullPointerException if any of the input locales is <code>null</code>.
179 * @throws IllegalArgumentException if any of the input locales repeat.
180 */
Raph Levien10ea92a2016-05-02 12:56:01 -0700181 public LocaleList(@NonNull Locale... list) {
182 if (list.length == 0) {
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700183 mList = sEmptyList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700184 mStringRepresentation = "";
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700185 } else {
186 final Locale[] localeList = new Locale[list.length];
187 final HashSet<Locale> seenLocales = new HashSet<Locale>();
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700188 final StringBuilder sb = new StringBuilder();
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800189 for (int i = 0; i < list.length; i++) {
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700190 final Locale l = list[i];
191 if (l == null) {
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800192 throw new NullPointerException("list[" + i + "] is null");
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700193 } else if (seenLocales.contains(l)) {
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800194 throw new IllegalArgumentException("list[" + i + "] is a repetition");
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700195 } else {
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700196 final Locale localeClone = (Locale) l.clone();
197 localeList[i] = localeClone;
198 sb.append(localeClone.toLanguageTag());
199 if (i < list.length - 1) {
200 sb.append(',');
201 }
202 seenLocales.add(localeClone);
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700203 }
204 }
205 mList = localeList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700206 mStringRepresentation = sb.toString();
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700207 }
208 }
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700209
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800210 /**
211 * Constructs a locale list, with the topLocale moved to the front if it already is
212 * in otherLocales, or added to the front if it isn't.
213 *
214 * {@hide}
215 */
216 public LocaleList(@NonNull Locale topLocale, LocaleList otherLocales) {
217 if (topLocale == null) {
218 throw new NullPointerException("topLocale is null");
219 }
220
221 final int inputLength = (otherLocales == null) ? 0 : otherLocales.mList.length;
222 int topLocaleIndex = -1;
223 for (int i = 0; i < inputLength; i++) {
224 if (topLocale.equals(otherLocales.mList[i])) {
225 topLocaleIndex = i;
226 break;
227 }
228 }
229
230 final int outputLength = inputLength + (topLocaleIndex == -1 ? 1 : 0);
231 final Locale[] localeList = new Locale[outputLength];
232 localeList[0] = (Locale) topLocale.clone();
233 if (topLocaleIndex == -1) {
234 // topLocale was not in otherLocales
235 for (int i = 0; i < inputLength; i++) {
236 localeList[i + 1] = (Locale) otherLocales.mList[i].clone();
237 }
238 } else {
239 for (int i = 0; i < topLocaleIndex; i++) {
240 localeList[i + 1] = (Locale) otherLocales.mList[i].clone();
241 }
242 for (int i = topLocaleIndex + 1; i < inputLength; i++) {
243 localeList[i] = (Locale) otherLocales.mList[i].clone();
244 }
245 }
246
247 final StringBuilder sb = new StringBuilder();
248 for (int i = 0; i < outputLength; i++) {
249 sb.append(localeList[i].toLanguageTag());
250 if (i < outputLength - 1) {
251 sb.append(',');
252 }
253 }
254
255 mList = localeList;
256 mStringRepresentation = sb.toString();
257 }
258
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -0700259 public static final @android.annotation.NonNull Parcelable.Creator<LocaleList> CREATOR
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800260 = new Parcelable.Creator<LocaleList>() {
261 @Override
262 public LocaleList createFromParcel(Parcel source) {
263 return LocaleList.forLanguageTags(source.readString());
264 }
265
266 @Override
267 public LocaleList[] newArray(int size) {
268 return new LocaleList[size];
269 }
270 };
271
Clara Bayarri66f6bd32016-04-26 12:18:36 +0100272 /**
273 * Retrieve an empty instance of {@link LocaleList}.
274 */
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800275 @NonNull
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700276 public static LocaleList getEmptyLocaleList() {
277 return sEmptyLocaleList;
278 }
279
Clara Bayarri66f6bd32016-04-26 12:18:36 +0100280 /**
281 * Generates a new LocaleList with the given language tags.
282 *
283 * @param list The language tags to be included as a single {@link String} separated by commas.
284 * @return A new instance with the {@link Locale} items identified by the given tags.
285 */
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800286 @NonNull
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700287 public static LocaleList forLanguageTags(@Nullable String list) {
288 if (list == null || list.equals("")) {
289 return getEmptyLocaleList();
290 } else {
291 final String[] tags = list.split(",");
292 final Locale[] localeArray = new Locale[tags.length];
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800293 for (int i = 0; i < localeArray.length; i++) {
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700294 localeArray[i] = Locale.forLanguageTag(tags[i]);
295 }
296 return new LocaleList(localeArray);
297 }
298 }
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700299
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800300 private static String getLikelyScript(Locale locale) {
301 final String script = locale.getScript();
302 if (!script.isEmpty()) {
303 return script;
304 } else {
305 // TODO: Cache the results if this proves to be too slow
306 return ULocale.addLikelySubtags(ULocale.forLocale(locale)).getScript();
307 }
308 }
309
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800310 private static final String STRING_EN_XA = "en-XA";
311 private static final String STRING_AR_XB = "ar-XB";
312 private static final Locale LOCALE_EN_XA = new Locale("en", "XA");
313 private static final Locale LOCALE_AR_XB = new Locale("ar", "XB");
314 private static final int NUM_PSEUDO_LOCALES = 2;
315
316 private static boolean isPseudoLocale(String locale) {
317 return STRING_EN_XA.equals(locale) || STRING_AR_XB.equals(locale);
318 }
319
Igor Viarheichyk025402c2017-10-06 12:46:49 -0700320 /**
321 * Returns true if locale is a pseudo-locale, false otherwise.
322 * {@hide}
323 */
324 public static boolean isPseudoLocale(Locale locale) {
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800325 return LOCALE_EN_XA.equals(locale) || LOCALE_AR_XB.equals(locale);
326 }
327
Maurice Lam522ecbe2018-12-04 20:21:40 -0800328 /**
329 * Returns true if locale is a pseudo-locale, false otherwise.
330 * {@hide}
331 */
332 @SystemApi
333 public static boolean isPseudoLocale(@Nullable ULocale locale) {
334 return isPseudoLocale(locale != null ? locale.toLocale() : null);
335 }
336
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800337 @IntRange(from=0, to=1)
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800338 private static int matchScore(Locale supported, Locale desired) {
339 if (supported.equals(desired)) {
340 return 1; // return early so we don't do unnecessary computation
341 }
342 if (!supported.getLanguage().equals(desired.getLanguage())) {
343 return 0;
344 }
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800345 if (isPseudoLocale(supported) || isPseudoLocale(desired)) {
346 // The locales are not the same, but the languages are the same, and one of the locales
347 // is a pseudo-locale. So this is not a match.
348 return 0;
349 }
Roozbeh Pournaderb927c552016-01-15 11:23:42 -0800350 final String supportedScr = getLikelyScript(supported);
351 if (supportedScr.isEmpty()) {
352 // If we can't guess a script, we don't know enough about the locales' language to find
353 // if the locales match. So we fall back to old behavior of matching, which considered
354 // locales with different regions different.
355 final String supportedRegion = supported.getCountry();
356 return (supportedRegion.isEmpty() ||
357 supportedRegion.equals(desired.getCountry()))
358 ? 1 : 0;
359 }
360 final String desiredScr = getLikelyScript(desired);
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800361 // There is no match if the two locales use different scripts. This will most imporantly
362 // take care of traditional vs simplified Chinese.
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800363 return supportedScr.equals(desiredScr) ? 1 : 0;
364 }
365
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800366 private int findFirstMatchIndex(Locale supportedLocale) {
367 for (int idx = 0; idx < mList.length; idx++) {
368 final int score = matchScore(supportedLocale, mList[idx]);
369 if (score > 0) {
370 return idx;
371 }
372 }
373 return Integer.MAX_VALUE;
374 }
375
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800376 private static final Locale EN_LATN = Locale.forLanguageTag("en-Latn");
377
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800378 private int computeFirstMatchIndex(Collection<String> supportedLocales,
379 boolean assumeEnglishIsSupported) {
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800380 if (mList.length == 1) { // just one locale, perhaps the most common scenario
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800381 return 0;
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800382 }
383 if (mList.length == 0) { // empty locale list
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800384 return -1;
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800385 }
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800386
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800387 int bestIndex = Integer.MAX_VALUE;
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800388 // Try English first, so we can return early if it's in the LocaleList
389 if (assumeEnglishIsSupported) {
390 final int idx = findFirstMatchIndex(EN_LATN);
391 if (idx == 0) { // We have a match on the first locale, which is good enough
392 return 0;
393 } else if (idx < bestIndex) {
394 bestIndex = idx;
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800395 }
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800396 }
397 for (String languageTag : supportedLocales) {
398 final Locale supportedLocale = Locale.forLanguageTag(languageTag);
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800399 // We expect the average length of locale lists used for locale resolution to be
400 // smaller than three, so it's OK to do this as an O(mn) algorithm.
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800401 final int idx = findFirstMatchIndex(supportedLocale);
402 if (idx == 0) { // We have a match on the first locale, which is good enough
403 return 0;
404 } else if (idx < bestIndex) {
405 bestIndex = idx;
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800406 }
407 }
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800408 if (bestIndex == Integer.MAX_VALUE) {
409 // no match was found, so we fall back to the first locale in the locale list
410 return 0;
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800411 } else {
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800412 return bestIndex;
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800413 }
Roozbeh Pournader8bca6982015-11-18 17:41:24 -0800414 }
415
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800416 private Locale computeFirstMatch(Collection<String> supportedLocales,
417 boolean assumeEnglishIsSupported) {
418 int bestIndex = computeFirstMatchIndex(supportedLocales, assumeEnglishIsSupported);
419 return bestIndex == -1 ? null : mList[bestIndex];
420 }
421
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800422 /**
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800423 * Returns the first match in the locale list given an unordered array of supported locales
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800424 * in BCP 47 format.
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800425 *
Clara Bayarri66f6bd32016-04-26 12:18:36 +0100426 * @return The first {@link Locale} from this list that appears in the given array, or
427 * {@code null} if the {@link LocaleList} is empty.
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800428 */
429 @Nullable
430 public Locale getFirstMatch(String[] supportedLocales) {
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800431 return computeFirstMatch(Arrays.asList(supportedLocales),
432 false /* assume English is not supported */);
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800433 }
434
435 /**
Adam Lesinskib61e4052016-05-19 18:23:05 -0700436 * {@hide}
437 */
438 public int getFirstMatchIndex(String[] supportedLocales) {
439 return computeFirstMatchIndex(Arrays.asList(supportedLocales),
440 false /* assume English is not supported */);
441 }
442
443 /**
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800444 * Same as getFirstMatch(), but with English assumed to be supported, even if it's not.
445 * {@hide}
446 */
447 @Nullable
448 public Locale getFirstMatchWithEnglishSupported(String[] supportedLocales) {
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800449 return computeFirstMatch(Arrays.asList(supportedLocales),
450 true /* assume English is supported */);
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800451 }
452
453 /**
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800454 * {@hide}
455 */
456 public int getFirstMatchIndexWithEnglishSupported(Collection<String> supportedLocales) {
457 return computeFirstMatchIndex(supportedLocales, true /* assume English is supported */);
458 }
459
460 /**
461 * {@hide}
462 */
463 public int getFirstMatchIndexWithEnglishSupported(String[] supportedLocales) {
464 return getFirstMatchIndexWithEnglishSupported(Arrays.asList(supportedLocales));
465 }
466
467 /**
468 * Returns true if the collection of locale tags only contains empty locales and pseudolocales.
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800469 * Assumes that there is no repetition in the input.
470 * {@hide}
471 */
Adam Lesinskib61e4052016-05-19 18:23:05 -0700472 public static boolean isPseudoLocalesOnly(@Nullable String[] supportedLocales) {
473 if (supportedLocales == null) {
474 return true;
475 }
476
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800477 if (supportedLocales.length > NUM_PSEUDO_LOCALES + 1) {
478 // This is for optimization. Since there's no repetition in the input, if we have more
479 // than the number of pseudo-locales plus one for the empty string, it's guaranteed
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800480 // that we have some meaninful locale in the collection, so the list is not "practically
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800481 // empty".
482 return false;
483 }
484 for (String locale : supportedLocales) {
485 if (!locale.isEmpty() && !isPseudoLocale(locale)) {
486 return false;
487 }
488 }
489 return true;
490 }
491
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700492 private final static Object sLock = new Object();
493
494 @GuardedBy("sLock")
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800495 private static LocaleList sLastExplicitlySetLocaleList = null;
496 @GuardedBy("sLock")
497 private static LocaleList sDefaultLocaleList = null;
498 @GuardedBy("sLock")
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800499 private static LocaleList sDefaultAdjustedLocaleList = null;
500 @GuardedBy("sLock")
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800501 private static Locale sLastDefaultLocale = null;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700502
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800503 /**
504 * The result is guaranteed to include the default Locale returned by Locale.getDefault(), but
505 * not necessarily at the top of the list. The default locale not being at the top of the list
506 * is an indication that the system has set the default locale to one of the user's other
507 * preferred locales, having concluded that the primary preference is not supported but a
508 * secondary preference is.
509 *
Clara Bayarri66f6bd32016-04-26 12:18:36 +0100510 * <p>Note that the default LocaleList would change if Locale.setDefault() is called. This
511 * method takes that into account by always checking the output of Locale.getDefault() and
512 * recalculating the default LocaleList if needed.</p>
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800513 */
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700514 @NonNull @Size(min=1)
515 public static LocaleList getDefault() {
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800516 final Locale defaultLocale = Locale.getDefault();
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700517 synchronized (sLock) {
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800518 if (!defaultLocale.equals(sLastDefaultLocale)) {
519 sLastDefaultLocale = defaultLocale;
520 // It's either the first time someone has asked for the default locale list, or
521 // someone has called Locale.setDefault() since we last set or adjusted the default
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800522 // locale list. So let's recalculate the locale list.
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800523 if (sDefaultLocaleList != null
Roozbeh Pournaderfee44842016-02-04 15:24:24 -0800524 && defaultLocale.equals(sDefaultLocaleList.get(0))) {
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800525 // The default Locale has changed, but it happens to be the first locale in the
526 // default locale list, so we don't need to construct a new locale list.
527 return sDefaultLocaleList;
528 }
529 sDefaultLocaleList = new LocaleList(defaultLocale, sLastExplicitlySetLocaleList);
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800530 sDefaultAdjustedLocaleList = sDefaultLocaleList;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700531 }
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800532 // sDefaultLocaleList can't be null, since it can't be set to null by
533 // LocaleList.setDefault(), and if getDefault() is called before a call to
534 // setDefault(), sLastDefaultLocale would be null and the check above would set
535 // sDefaultLocaleList.
536 return sDefaultLocaleList;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700537 }
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800538 }
539
540 /**
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800541 * Returns the default locale list, adjusted by moving the default locale to its first
542 * position.
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800543 */
544 @NonNull @Size(min=1)
545 public static LocaleList getAdjustedDefault() {
546 getDefault(); // to recalculate the default locale list, if necessary
547 synchronized (sLock) {
548 return sDefaultAdjustedLocaleList;
549 }
550 }
551
552 /**
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800553 * Also sets the default locale by calling Locale.setDefault() with the first locale in the
554 * list.
555 *
556 * @throws NullPointerException if the input is <code>null</code>.
557 * @throws IllegalArgumentException if the input is empty.
558 */
559 public static void setDefault(@NonNull @Size(min=1) LocaleList locales) {
560 setDefault(locales, 0);
561 }
562
563 /**
564 * This may be used directly by system processes to set the default locale list for apps. For
565 * such uses, the default locale list would always come from the user preferences, but the
566 * default locale may have been chosen to be a locale other than the first locale in the locale
567 * list (based on the locales the app supports).
568 *
569 * {@hide}
570 */
571 public static void setDefault(@NonNull @Size(min=1) LocaleList locales, int localeIndex) {
572 if (locales == null) {
573 throw new NullPointerException("locales is null");
574 }
575 if (locales.isEmpty()) {
576 throw new IllegalArgumentException("locales is empty");
577 }
578 synchronized (sLock) {
579 sLastDefaultLocale = locales.get(localeIndex);
580 Locale.setDefault(sLastDefaultLocale);
581 sLastExplicitlySetLocaleList = locales;
582 sDefaultLocaleList = locales;
Roozbeh Pournader834641b2016-01-23 22:34:57 -0800583 if (localeIndex == 0) {
584 sDefaultAdjustedLocaleList = sDefaultLocaleList;
585 } else {
586 sDefaultAdjustedLocaleList = new LocaleList(
587 sLastDefaultLocale, sDefaultLocaleList);
588 }
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800589 }
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700590 }
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700591}