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