blob: 8a2d015a6f7bd3a2855804e3073d72faa9889483 [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
17package android.util;
18
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;
Roozbeh Pournader2591cc82015-12-08 22:21:24 -080023import android.icu.util.ULocale;
Yohei Yukawa789d8fd2015-12-03 11:27:05 -080024import android.os.Parcel;
25import android.os.Parcelable;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -070026
27import com.android.internal.annotations.GuardedBy;
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070028
29import java.util.HashSet;
30import java.util.Locale;
31
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070032/**
33 * LocaleList is an immutable list of Locales, typically used to keep an
34 * ordered user preferences for locales.
35 */
Yohei Yukawa789d8fd2015-12-03 11:27:05 -080036public final class LocaleList implements Parcelable {
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070037 private final Locale[] mList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -070038 // This is a comma-separated list of the locales in the LocaleList created at construction time,
39 // basically the result of running each locale's toLanguageTag() method and concatenating them
40 // with commas in between.
Yohei Yukawa789d8fd2015-12-03 11:27:05 -080041 @NonNull
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -070042 private final String mStringRepresentation;
43
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070044 private static final Locale[] sEmptyList = new Locale[0];
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -070045 private static final LocaleList sEmptyLocaleList = new LocaleList();
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070046
47 public Locale get(int location) {
48 return location < mList.length ? mList[location] : null;
49 }
50
Roozbeh Pournader8bca6982015-11-18 17:41:24 -080051 @Nullable
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070052 public Locale getPrimary() {
53 return mList.length == 0 ? null : get(0);
54 }
55
56 public boolean isEmpty() {
57 return mList.length == 0;
58 }
59
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -080060 @IntRange(from=0)
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070061 public int size() {
62 return mList.length;
63 }
64
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -080065 @IntRange(from=-1)
66 public int indexOf(Locale locale) {
67 for (int i = 0; i < mList.length; i++) {
68 if (mList[i].equals(locale)) {
69 return i;
70 }
71 }
72 return -1;
73 }
74
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -070075 @Override
76 public boolean equals(Object other) {
77 if (other == this)
78 return true;
79 if (!(other instanceof LocaleList))
80 return false;
81 final Locale[] otherList = ((LocaleList) other).mList;
82 if (mList.length != otherList.length)
83 return false;
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -080084 for (int i = 0; i < mList.length; i++) {
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -070085 if (!mList[i].equals(otherList[i]))
86 return false;
87 }
88 return true;
89 }
90
91 @Override
92 public int hashCode() {
93 int result = 1;
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -080094 for (int i = 0; i < mList.length; i++) {
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -070095 result = 31 * result + mList[i].hashCode();
96 }
97 return result;
98 }
99
100 @Override
101 public String toString() {
102 StringBuilder sb = new StringBuilder();
103 sb.append("[");
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800104 for (int i = 0; i < mList.length; i++) {
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700105 sb.append(mList[i]);
106 if (i < mList.length - 1) {
107 sb.append(',');
108 }
109 }
110 sb.append("]");
111 return sb.toString();
112 }
113
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800114 @Override
115 public int describeContents() {
116 return 0;
117 }
118
119 @Override
120 public void writeToParcel(Parcel dest, int parcelableFlags) {
121 dest.writeString(mStringRepresentation);
122 }
123
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700124 @NonNull
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700125 public String toLanguageTags() {
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700126 return mStringRepresentation;
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700127 }
128
129 /**
130 * It is almost always better to call {@link #getEmptyLocaleList()} instead which returns
131 * a pre-constructed empty locale list.
132 */
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700133 public LocaleList() {
134 mList = sEmptyList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700135 mStringRepresentation = "";
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700136 }
137
138 /**
139 * @throws NullPointerException if any of the input locales is <code>null</code>.
140 * @throws IllegalArgumentException if any of the input locales repeat.
141 */
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700142 public LocaleList(@Nullable Locale locale) {
143 if (locale == null) {
144 mList = sEmptyList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700145 mStringRepresentation = "";
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700146 } else {
147 mList = new Locale[1];
148 mList[0] = (Locale) locale.clone();
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700149 mStringRepresentation = locale.toLanguageTag();
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700150 }
151 }
152
153 /**
154 * @throws NullPointerException if any of the input locales is <code>null</code>.
155 * @throws IllegalArgumentException if any of the input locales repeat.
156 */
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700157 public LocaleList(@Nullable Locale[] list) {
158 if (list == null || list.length == 0) {
159 mList = sEmptyList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700160 mStringRepresentation = "";
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700161 } else {
162 final Locale[] localeList = new Locale[list.length];
163 final HashSet<Locale> seenLocales = new HashSet<Locale>();
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700164 final StringBuilder sb = new StringBuilder();
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800165 for (int i = 0; i < list.length; i++) {
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700166 final Locale l = list[i];
167 if (l == null) {
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800168 throw new NullPointerException("list[" + i + "] is null");
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700169 } else if (seenLocales.contains(l)) {
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800170 throw new IllegalArgumentException("list[" + i + "] is a repetition");
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700171 } else {
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700172 final Locale localeClone = (Locale) l.clone();
173 localeList[i] = localeClone;
174 sb.append(localeClone.toLanguageTag());
175 if (i < list.length - 1) {
176 sb.append(',');
177 }
178 seenLocales.add(localeClone);
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700179 }
180 }
181 mList = localeList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700182 mStringRepresentation = sb.toString();
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700183 }
184 }
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700185
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800186 /**
187 * Constructs a locale list, with the topLocale moved to the front if it already is
188 * in otherLocales, or added to the front if it isn't.
189 *
190 * {@hide}
191 */
192 public LocaleList(@NonNull Locale topLocale, LocaleList otherLocales) {
193 if (topLocale == null) {
194 throw new NullPointerException("topLocale is null");
195 }
196
197 final int inputLength = (otherLocales == null) ? 0 : otherLocales.mList.length;
198 int topLocaleIndex = -1;
199 for (int i = 0; i < inputLength; i++) {
200 if (topLocale.equals(otherLocales.mList[i])) {
201 topLocaleIndex = i;
202 break;
203 }
204 }
205
206 final int outputLength = inputLength + (topLocaleIndex == -1 ? 1 : 0);
207 final Locale[] localeList = new Locale[outputLength];
208 localeList[0] = (Locale) topLocale.clone();
209 if (topLocaleIndex == -1) {
210 // topLocale was not in otherLocales
211 for (int i = 0; i < inputLength; i++) {
212 localeList[i + 1] = (Locale) otherLocales.mList[i].clone();
213 }
214 } else {
215 for (int i = 0; i < topLocaleIndex; i++) {
216 localeList[i + 1] = (Locale) otherLocales.mList[i].clone();
217 }
218 for (int i = topLocaleIndex + 1; i < inputLength; i++) {
219 localeList[i] = (Locale) otherLocales.mList[i].clone();
220 }
221 }
222
223 final StringBuilder sb = new StringBuilder();
224 for (int i = 0; i < outputLength; i++) {
225 sb.append(localeList[i].toLanguageTag());
226 if (i < outputLength - 1) {
227 sb.append(',');
228 }
229 }
230
231 mList = localeList;
232 mStringRepresentation = sb.toString();
233 }
234
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800235 public static final Parcelable.Creator<LocaleList> CREATOR
236 = new Parcelable.Creator<LocaleList>() {
237 @Override
238 public LocaleList createFromParcel(Parcel source) {
239 return LocaleList.forLanguageTags(source.readString());
240 }
241
242 @Override
243 public LocaleList[] newArray(int size) {
244 return new LocaleList[size];
245 }
246 };
247
248 @NonNull
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700249 public static LocaleList getEmptyLocaleList() {
250 return sEmptyLocaleList;
251 }
252
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800253 @NonNull
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700254 public static LocaleList forLanguageTags(@Nullable String list) {
255 if (list == null || list.equals("")) {
256 return getEmptyLocaleList();
257 } else {
258 final String[] tags = list.split(",");
259 final Locale[] localeArray = new Locale[tags.length];
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800260 for (int i = 0; i < localeArray.length; i++) {
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700261 localeArray[i] = Locale.forLanguageTag(tags[i]);
262 }
263 return new LocaleList(localeArray);
264 }
265 }
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700266
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800267 private static String getLikelyScript(Locale locale) {
268 final String script = locale.getScript();
269 if (!script.isEmpty()) {
270 return script;
271 } else {
272 // TODO: Cache the results if this proves to be too slow
273 return ULocale.addLikelySubtags(ULocale.forLocale(locale)).getScript();
274 }
275 }
276
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800277 private static final String STRING_EN_XA = "en-XA";
278 private static final String STRING_AR_XB = "ar-XB";
279 private static final Locale LOCALE_EN_XA = new Locale("en", "XA");
280 private static final Locale LOCALE_AR_XB = new Locale("ar", "XB");
281 private static final int NUM_PSEUDO_LOCALES = 2;
282
283 private static boolean isPseudoLocale(String locale) {
284 return STRING_EN_XA.equals(locale) || STRING_AR_XB.equals(locale);
285 }
286
287 private static boolean isPseudoLocale(Locale locale) {
288 return LOCALE_EN_XA.equals(locale) || LOCALE_AR_XB.equals(locale);
289 }
290
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800291 @IntRange(from=0, to=1)
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800292 private static int matchScore(Locale supported, Locale desired) {
293 if (supported.equals(desired)) {
294 return 1; // return early so we don't do unnecessary computation
295 }
296 if (!supported.getLanguage().equals(desired.getLanguage())) {
297 return 0;
298 }
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800299 if (isPseudoLocale(supported) || isPseudoLocale(desired)) {
300 // The locales are not the same, but the languages are the same, and one of the locales
301 // is a pseudo-locale. So this is not a match.
302 return 0;
303 }
Roozbeh Pournaderb927c552016-01-15 11:23:42 -0800304 final String supportedScr = getLikelyScript(supported);
305 if (supportedScr.isEmpty()) {
306 // If we can't guess a script, we don't know enough about the locales' language to find
307 // if the locales match. So we fall back to old behavior of matching, which considered
308 // locales with different regions different.
309 final String supportedRegion = supported.getCountry();
310 return (supportedRegion.isEmpty() ||
311 supportedRegion.equals(desired.getCountry()))
312 ? 1 : 0;
313 }
314 final String desiredScr = getLikelyScript(desired);
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800315 // There is no match if the two locales use different scripts. This will most imporantly
316 // take care of traditional vs simplified Chinese.
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800317 return supportedScr.equals(desiredScr) ? 1 : 0;
318 }
319
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800320 private static final Locale EN_LATN = Locale.forLanguageTag("en-Latn");
321
322 private Locale computeFirstMatch(String[] supportedLocales, boolean assumeEnglishIsSupported) {
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800323 if (mList.length == 1) { // just one locale, perhaps the most common scenario
324 return mList[0];
325 }
326 if (mList.length == 0) { // empty locale list
327 return null;
328 }
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800329 int bestIndex = Integer.MAX_VALUE;
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800330 final int numSupportedLocales =
331 supportedLocales.length + (assumeEnglishIsSupported ? 1 : 0);
332 for (int i = 0; i < numSupportedLocales; i++) {
333 final Locale supportedLocale;
334 if (assumeEnglishIsSupported) {
335 // Try English first, so we can return early if it's in the LocaleList
336 supportedLocale = (i == 0) ? EN_LATN : Locale.forLanguageTag(supportedLocales[i-1]);
337 } else {
338 supportedLocale = Locale.forLanguageTag(supportedLocales[i]);
339 }
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800340 // We expect the average length of locale lists used for locale resolution to be
341 // smaller than three, so it's OK to do this as an O(mn) algorithm.
342 for (int idx = 0; idx < mList.length; idx++) {
343 final int score = matchScore(supportedLocale, mList[idx]);
344 if (score > 0) {
345 if (idx == 0) { // We have a match on the first locale, which is good enough
346 return mList[0];
347 } else if (idx < bestIndex) {
348 bestIndex = idx;
349 }
350 }
351 }
352 }
353 if (bestIndex == Integer.MAX_VALUE) { // no match was found
354 return mList[0];
355 } else {
356 return mList[bestIndex];
357 }
Roozbeh Pournader8bca6982015-11-18 17:41:24 -0800358 }
359
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800360 /**
Roozbeh Pournaderfb9236c2015-12-15 23:56:11 -0800361 * Returns the first match in the locale list given an unordered array of supported locales
362 * in BCP47 format.
363 *
364 * If the locale list is empty, null would be returned.
365 */
366 @Nullable
367 public Locale getFirstMatch(String[] supportedLocales) {
368 return computeFirstMatch(supportedLocales, false /* assume English is not supported */);
369 }
370
371 /**
372 * Same as getFirstMatch(), but with English assumed to be supported, even if it's not.
373 * {@hide}
374 */
375 @Nullable
376 public Locale getFirstMatchWithEnglishSupported(String[] supportedLocales) {
377 return computeFirstMatch(supportedLocales, true /* assume English is supported */);
378 }
379
380 /**
Roozbeh Pournader1c686f22015-12-18 14:22:14 -0800381 * Returns true if the array of locale tags only contains empty locales and pseudolocales.
382 * Assumes that there is no repetition in the input.
383 * {@hide}
384 */
385 public static boolean isPseudoLocalesOnly(String[] supportedLocales) {
386 if (supportedLocales.length > NUM_PSEUDO_LOCALES + 1) {
387 // This is for optimization. Since there's no repetition in the input, if we have more
388 // than the number of pseudo-locales plus one for the empty string, it's guaranteed
389 // that we have some meaninful locale in the list, so the list is not "practically
390 // empty".
391 return false;
392 }
393 for (String locale : supportedLocales) {
394 if (!locale.isEmpty() && !isPseudoLocale(locale)) {
395 return false;
396 }
397 }
398 return true;
399 }
400
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700401 private final static Object sLock = new Object();
402
403 @GuardedBy("sLock")
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800404 private static LocaleList sLastExplicitlySetLocaleList = null;
405 @GuardedBy("sLock")
406 private static LocaleList sDefaultLocaleList = null;
407 @GuardedBy("sLock")
408 private static Locale sLastDefaultLocale = null;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700409
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800410 /**
411 * The result is guaranteed to include the default Locale returned by Locale.getDefault(), but
412 * not necessarily at the top of the list. The default locale not being at the top of the list
413 * is an indication that the system has set the default locale to one of the user's other
414 * preferred locales, having concluded that the primary preference is not supported but a
415 * secondary preference is.
416 *
417 * Note that the default LocaleList would change if Locale.setDefault() is called. This method
418 * takes that into account by always checking the output of Locale.getDefault() and adjusting
419 * the default LocaleList if needed.
420 */
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700421 @NonNull @Size(min=1)
422 public static LocaleList getDefault() {
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800423 final Locale defaultLocale = Locale.getDefault();
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700424 synchronized (sLock) {
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800425 if (!defaultLocale.equals(sLastDefaultLocale)) {
426 sLastDefaultLocale = defaultLocale;
427 // It's either the first time someone has asked for the default locale list, or
428 // someone has called Locale.setDefault() since we last set or adjusted the default
429 // locale list. So let's adjust the locale list.
430 if (sDefaultLocaleList != null
431 && defaultLocale.equals(sDefaultLocaleList.getPrimary())) {
432 // The default Locale has changed, but it happens to be the first locale in the
433 // default locale list, so we don't need to construct a new locale list.
434 return sDefaultLocaleList;
435 }
436 sDefaultLocaleList = new LocaleList(defaultLocale, sLastExplicitlySetLocaleList);
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700437 }
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800438 // sDefaultLocaleList can't be null, since it can't be set to null by
439 // LocaleList.setDefault(), and if getDefault() is called before a call to
440 // setDefault(), sLastDefaultLocale would be null and the check above would set
441 // sDefaultLocaleList.
442 return sDefaultLocaleList;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700443 }
Roozbeh Pournader2b5ab182016-01-06 12:22:03 -0800444 }
445
446 /**
447 * Also sets the default locale by calling Locale.setDefault() with the first locale in the
448 * list.
449 *
450 * @throws NullPointerException if the input is <code>null</code>.
451 * @throws IllegalArgumentException if the input is empty.
452 */
453 public static void setDefault(@NonNull @Size(min=1) LocaleList locales) {
454 setDefault(locales, 0);
455 }
456
457 /**
458 * This may be used directly by system processes to set the default locale list for apps. For
459 * such uses, the default locale list would always come from the user preferences, but the
460 * default locale may have been chosen to be a locale other than the first locale in the locale
461 * list (based on the locales the app supports).
462 *
463 * {@hide}
464 */
465 public static void setDefault(@NonNull @Size(min=1) LocaleList locales, int localeIndex) {
466 if (locales == null) {
467 throw new NullPointerException("locales is null");
468 }
469 if (locales.isEmpty()) {
470 throw new IllegalArgumentException("locales is empty");
471 }
472 synchronized (sLock) {
473 sLastDefaultLocale = locales.get(localeIndex);
474 Locale.setDefault(sLastDefaultLocale);
475 sLastExplicitlySetLocaleList = locales;
476 sDefaultLocaleList = locales;
477 }
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700478 }
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700479}