1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.app; 18 19 import android.annotation.IntDef; 20 import android.app.LocaleManager; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.Context; 23 import android.os.LocaleList; 24 import android.provider.Settings; 25 import android.telephony.TelephonyManager; 26 import android.text.TextUtils; 27 import android.util.Log; 28 import android.view.inputmethod.InputMethodSubtype; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 32 import java.io.Serializable; 33 import java.lang.annotation.Retention; 34 import java.lang.annotation.RetentionPolicy; 35 import java.util.Collection; 36 import java.util.HashMap; 37 import java.util.HashSet; 38 import java.util.IllformedLocaleException; 39 import java.util.List; 40 import java.util.Locale; 41 import java.util.Set; 42 43 public class LocaleStore { 44 private static final int TIER_LANGUAGE = 1; 45 private static final int TIER_REGION = 2; 46 private static final int TIER_NUMBERING = 3; 47 private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>(); 48 private static final String TAG = LocaleStore.class.getSimpleName(); 49 private static boolean sFullyInitialized = false; 50 51 public static class LocaleInfo implements Serializable { 52 public static final int SUGGESTION_TYPE_NONE = 0; 53 // A mask used to identify the suggested locale is from SIM. 54 public static final int SUGGESTION_TYPE_SIM = 1 << 0; 55 // A mask used to identify the suggested locale is from the config. 56 public static final int SUGGESTION_TYPE_CFG = 1 << 1; 57 // Only for per-app language picker 58 // A mask used to identify the suggested locale is from the same application's current 59 // configured locale. 60 public static final int SUGGESTION_TYPE_CURRENT = 1 << 2; 61 // Only for per-app language picker 62 // A mask used to identify the suggested locale is the system default language. 63 public static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3; 64 // Only for per-app language picker 65 // A mask used to identify the suggested locale is from other applications' configured 66 // locales. 67 public static final int SUGGESTION_TYPE_OTHER_APP_LANGUAGE = 1 << 4; 68 // Only for per-app language picker 69 // A mask used to identify the suggested locale is what the active IME supports. 70 public static final int SUGGESTION_TYPE_IME_LANGUAGE = 1 << 5; 71 // Only for per-app language picker 72 // A mask used to identify the suggested locale is in the current system languages. 73 public static final int SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE = 1 << 6; 74 /** @hide */ 75 @IntDef(prefix = { "SUGGESTION_TYPE_" }, value = { 76 SUGGESTION_TYPE_NONE, 77 SUGGESTION_TYPE_SIM, 78 SUGGESTION_TYPE_CFG, 79 SUGGESTION_TYPE_CURRENT, 80 SUGGESTION_TYPE_SYSTEM_LANGUAGE, 81 SUGGESTION_TYPE_OTHER_APP_LANGUAGE, 82 SUGGESTION_TYPE_IME_LANGUAGE, 83 SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE 84 }) 85 @Retention(RetentionPolicy.SOURCE) 86 public @interface SuggestionType {} 87 88 private final Locale mLocale; 89 private final Locale mParent; 90 private final String mId; 91 private boolean mIsTranslated; 92 private boolean mIsPseudo; 93 private boolean mIsChecked; // Used by the LocaleListEditor to mark entries for deletion 94 // Combination of flags for various reasons to show a locale as a suggestion. 95 // Can be SIM, location, etc. 96 // Set to public to be accessible during runtime from the test app. 97 @VisibleForTesting public int mSuggestionFlags; 98 99 private String mFullNameNative; 100 private String mFullCountryNameNative; 101 private String mLangScriptKey; 102 103 private boolean mHasNumberingSystems; 104 LocaleInfo(Locale locale)105 private LocaleInfo(Locale locale) { 106 this.mLocale = locale; 107 this.mId = locale.toLanguageTag(); 108 this.mParent = getParent(locale); 109 this.mHasNumberingSystems = false; 110 this.mIsChecked = false; 111 this.mSuggestionFlags = SUGGESTION_TYPE_NONE; 112 this.mIsTranslated = false; 113 this.mIsPseudo = false; 114 } 115 LocaleInfo(String localeId)116 private LocaleInfo(String localeId) { 117 this(Locale.forLanguageTag(localeId)); 118 } 119 LocaleInfo(LocaleInfo localeInfo)120 private LocaleInfo(LocaleInfo localeInfo) { 121 this.mLocale = localeInfo.getLocale(); 122 this.mId = localeInfo.getId(); 123 this.mParent = localeInfo.getParent(); 124 this.mHasNumberingSystems = localeInfo.mHasNumberingSystems; 125 this.mIsChecked = localeInfo.getChecked(); 126 this.mSuggestionFlags = localeInfo.mSuggestionFlags; 127 this.mIsTranslated = localeInfo.isTranslated(); 128 this.mIsPseudo = localeInfo.mIsPseudo; 129 } 130 getParent(Locale locale)131 private static Locale getParent(Locale locale) { 132 if (locale.getCountry().isEmpty()) { 133 return null; 134 } 135 return new Locale.Builder() 136 .setLocale(locale) 137 .setRegion("") 138 .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "") 139 .build(); 140 } 141 142 /** Return true if there are any same locales with different numbering system. */ hasNumberingSystems()143 public boolean hasNumberingSystems() { 144 return mHasNumberingSystems; 145 } 146 147 @Override toString()148 public String toString() { 149 return mId; 150 } 151 152 @UnsupportedAppUsage getLocale()153 public Locale getLocale() { 154 return mLocale; 155 } 156 157 @UnsupportedAppUsage getParent()158 public Locale getParent() { 159 return mParent; 160 } 161 162 /** 163 * TODO: This method may rename to be more generic i.e. toLanguageTag(). 164 */ 165 @UnsupportedAppUsage getId()166 public String getId() { 167 return mId; 168 } 169 isTranslated()170 public boolean isTranslated() { 171 return mIsTranslated; 172 } 173 setTranslated(boolean isTranslated)174 public void setTranslated(boolean isTranslated) { 175 mIsTranslated = isTranslated; 176 } 177 isSuggested()178 public boolean isSuggested() { 179 if (!mIsTranslated) { // Never suggest an untranslated locale 180 return false; 181 } 182 return mSuggestionFlags != SUGGESTION_TYPE_NONE; 183 } 184 185 /** 186 * Check whether the LocaleInfo is suggested by a specific mask 187 * 188 * @param suggestionMask The mask which is used to identify the suggestion flag. 189 * @return true if the locale is suggested by a specific suggestion flag. Otherwise, false. 190 */ isSuggestionOfType(int suggestionMask)191 public boolean isSuggestionOfType(int suggestionMask) { 192 if (!mIsTranslated) { // Never suggest an untranslated locale 193 return false; 194 } 195 return (mSuggestionFlags & suggestionMask) == suggestionMask; 196 } 197 198 /** 199 * Extend the locale's suggestion type 200 * 201 * @param suggestionMask The mask to extend the suggestion flag 202 */ extendSuggestionOfType(@uggestionType int suggestionMask)203 public void extendSuggestionOfType(@SuggestionType int suggestionMask) { 204 if (!mIsTranslated) { // Never suggest an untranslated locale 205 return; 206 } 207 mSuggestionFlags |= suggestionMask; 208 } 209 210 @UnsupportedAppUsage getFullNameNative()211 public String getFullNameNative() { 212 if (mFullNameNative == null) { 213 Locale locale = mLocale.stripExtensions(); 214 mFullNameNative = 215 LocaleHelper.getDisplayName(locale, locale, true /* sentence case */); 216 } 217 return mFullNameNative; 218 } 219 getFullCountryNameNative()220 public String getFullCountryNameNative() { 221 if (mFullCountryNameNative == null) { 222 mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale); 223 } 224 return mFullCountryNameNative; 225 } 226 getFullCountryNameInUiLanguage()227 String getFullCountryNameInUiLanguage() { 228 // We don't cache the UI name because the default locale keeps changing 229 return LocaleHelper.getDisplayCountry(mLocale); 230 } 231 232 /** Returns the name of the locale in the language of the UI. 233 * It is used for search, but never shown. 234 * For instance German will show as "Deutsch" in the list, but we will also search for 235 * "allemand" if the system UI is in French. 236 */ 237 @UnsupportedAppUsage getFullNameInUiLanguage()238 public String getFullNameInUiLanguage() { 239 Locale locale = mLocale.stripExtensions(); 240 // We don't cache the UI name because the default locale keeps changing 241 return LocaleHelper.getDisplayName(locale, true /* sentence case */); 242 } 243 getLangScriptKey()244 private String getLangScriptKey() { 245 if (mLangScriptKey == null) { 246 Locale baseLocale = new Locale.Builder() 247 .setLocale(mLocale) 248 .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "") 249 .build(); 250 Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(baseLocale)); 251 mLangScriptKey = 252 (parentWithScript == null) 253 ? mLocale.toLanguageTag() 254 : parentWithScript.toLanguageTag(); 255 } 256 return mLangScriptKey; 257 } 258 getLabel(boolean countryMode)259 String getLabel(boolean countryMode) { 260 if (countryMode) { 261 return getFullCountryNameNative(); 262 } else { 263 return getFullNameNative(); 264 } 265 } 266 getNumberingSystem()267 String getNumberingSystem() { 268 return LocaleHelper.getDisplayNumberingSystemKeyValue(mLocale, mLocale); 269 } 270 getContentDescription(boolean countryMode)271 String getContentDescription(boolean countryMode) { 272 if (countryMode) { 273 return getFullCountryNameInUiLanguage(); 274 } else { 275 return getFullNameInUiLanguage(); 276 } 277 } 278 getChecked()279 public boolean getChecked() { 280 return mIsChecked; 281 } 282 setChecked(boolean checked)283 public void setChecked(boolean checked) { 284 mIsChecked = checked; 285 } 286 isAppCurrentLocale()287 public boolean isAppCurrentLocale() { 288 return (mSuggestionFlags & SUGGESTION_TYPE_CURRENT) > 0; 289 } 290 isSystemLocale()291 public boolean isSystemLocale() { 292 return (mSuggestionFlags & SUGGESTION_TYPE_SYSTEM_LANGUAGE) > 0; 293 } 294 isInCurrentSystemLocales()295 public boolean isInCurrentSystemLocales() { 296 return (mSuggestionFlags & SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE) > 0; 297 } 298 } 299 getSimCountries(Context context)300 private static Set<String> getSimCountries(Context context) { 301 Set<String> result = new HashSet<>(); 302 303 TelephonyManager tm = context.getSystemService(TelephonyManager.class); 304 305 if (tm != null) { 306 String iso = tm.getSimCountryIso().toUpperCase(Locale.US); 307 if (!iso.isEmpty()) { 308 result.add(iso); 309 } 310 311 iso = tm.getNetworkCountryIso().toUpperCase(Locale.US); 312 if (!iso.isEmpty()) { 313 result.add(iso); 314 } 315 } 316 317 return result; 318 } 319 320 /* 321 * This method is added for SetupWizard, to force an update of the suggested locales 322 * when the SIM is initialized. 323 * 324 * <p>When the device is freshly started, it sometimes gets to the language selection 325 * before the SIM is properly initialized. 326 * So at the time the cache is filled, the info from the SIM might not be available. 327 * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events. 328 * SetupWizard will call this function when that happens.</p> 329 * 330 * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code. 331 * The user might change the SIM or might cross border and connect to a network 332 * in a different country, without restarting the Settings application or the phone.</p> 333 */ updateSimCountries(Context context)334 public static void updateSimCountries(Context context) { 335 Set<String> simCountries = getSimCountries(context); 336 337 for (LocaleInfo li : sLocaleCache.values()) { 338 // This method sets the suggestion flags for the (new) SIM locales, but it does not 339 // try to clean up the old flags. After all, if the user replaces a German SIM 340 // with a French one, it is still possible that they are speaking German. 341 // So both French and German are reasonable suggestions. 342 if (simCountries.contains(li.getLocale().getCountry())) { 343 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; 344 } 345 } 346 } 347 348 /** 349 * Get the application's activated locale. 350 * 351 * @param context UI activity's context. 352 * @param appPackageName The application's package name. 353 * @param isAppSelected True if the application is selected in the UI; false otherwise. 354 * @return A LocaleInfo with the application's activated locale. 355 */ getAppActivatedLocaleInfo(Context context, String appPackageName, boolean isAppSelected)356 public static LocaleInfo getAppActivatedLocaleInfo(Context context, String appPackageName, 357 boolean isAppSelected) { 358 if (appPackageName == null) { 359 return null; 360 } 361 362 LocaleManager localeManager = context.getSystemService(LocaleManager.class); 363 try { 364 LocaleList localeList = (localeManager == null) 365 ? null : localeManager.getApplicationLocales(appPackageName); 366 Locale locale = localeList == null ? null : localeList.get(0); 367 368 if (locale != null) { 369 LocaleInfo cacheInfo = getLocaleInfo(locale, sLocaleCache); 370 LocaleInfo localeInfo = new LocaleInfo(cacheInfo); 371 if (isAppSelected) { 372 localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT; 373 } else { 374 localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE; 375 } 376 return localeInfo; 377 } 378 } catch (IllegalArgumentException e) { 379 Log.d(TAG, "IllegalArgumentException ", e); 380 } 381 return null; 382 } 383 384 /** 385 * Transform IME's language tag to LocaleInfo. 386 * 387 * @param list A list which includes IME's subtype. 388 * @return A LocaleInfo set which includes IME's language tags. 389 */ transformImeLanguageTagToLocaleInfo( List<InputMethodSubtype> list)390 public static Set<LocaleInfo> transformImeLanguageTagToLocaleInfo( 391 List<InputMethodSubtype> list) { 392 Set<LocaleInfo> imeLocales = new HashSet<>(); 393 Set<String> languageTagSet = new HashSet<>(); 394 for (InputMethodSubtype subtype : list) { 395 String languageTag = subtype.getLanguageTag(); 396 if (!languageTagSet.contains(languageTag)) { 397 languageTagSet.add(languageTag); 398 Locale locale = Locale.forLanguageTag(languageTag); 399 LocaleInfo cacheInfo = getLocaleInfo(locale, sLocaleCache); 400 LocaleInfo localeInfo = new LocaleInfo(cacheInfo); 401 localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE; 402 imeLocales.add(localeInfo); 403 } 404 } 405 return imeLocales; 406 } 407 408 /** 409 * Returns a list of system locale that removes all extensions except for the numbering system. 410 */ getSystemCurrentLocales()411 public static Set<LocaleInfo> getSystemCurrentLocales() { 412 Set<LocaleInfo> localeList = new HashSet<>(); 413 LocaleList systemLangList = LocaleList.getDefault(); 414 for(int i = 0; i < systemLangList.size(); i++) { 415 Locale sysLocale = getLocaleWithOnlyNumberingSystem(systemLangList.get(i)); 416 LocaleInfo cacheInfo = getLocaleInfo(sysLocale, sLocaleCache); 417 LocaleInfo localeInfo = new LocaleInfo(cacheInfo); 418 localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE; 419 localeList.add(localeInfo); 420 } 421 return localeList; 422 } 423 424 /** 425 * The "system default" is special case for per-app picker. Intentionally keep the locale 426 * empty to let activity know "system default" been selected. 427 */ getSystemDefaultLocaleInfo(boolean hasAppLanguage)428 public static LocaleInfo getSystemDefaultLocaleInfo(boolean hasAppLanguage) { 429 LocaleInfo systemDefaultInfo = new LocaleInfo(""); 430 systemDefaultInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SYSTEM_LANGUAGE; 431 if (hasAppLanguage) { 432 systemDefaultInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT; 433 } 434 systemDefaultInfo.mIsTranslated = true; 435 return systemDefaultInfo; 436 } 437 438 /* 439 * Show all the languages supported for a country in the suggested list. 440 * This is also handy for devices without SIM (tablets). 441 */ addSuggestedLocalesForRegion(Locale locale)442 private static void addSuggestedLocalesForRegion(Locale locale) { 443 if (locale == null) { 444 return; 445 } 446 final String country = locale.getCountry(); 447 if (country.isEmpty()) { 448 return; 449 } 450 451 for (LocaleInfo li : sLocaleCache.values()) { 452 if (country.equals(li.getLocale().getCountry())) { 453 // We don't need to differentiate between manual and SIM suggestions 454 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; 455 } 456 } 457 } 458 459 @UnsupportedAppUsage fillCache(Context context)460 public static void fillCache(Context context) { 461 if (sFullyInitialized) { 462 return; 463 } 464 465 Set<String> simCountries = getSimCountries(context); 466 467 final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(), 468 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; 469 Set<Locale> numberSystemLocaleList = new HashSet<>(); 470 for (String localeId : LocalePicker.getSupportedLocales(context)) { 471 if (Locale.forLanguageTag(localeId).getUnicodeLocaleType("nu") != null) { 472 numberSystemLocaleList.add(Locale.forLanguageTag(localeId)); 473 } 474 } 475 for (String localeId : LocalePicker.getSupportedLocales(context)) { 476 if (localeId.isEmpty()) { 477 throw new IllformedLocaleException("Bad locale entry in locale_config.xml"); 478 } 479 LocaleInfo li = new LocaleInfo(localeId); 480 481 if (LocaleList.isPseudoLocale(li.getLocale())) { 482 if (isInDeveloperMode) { 483 li.setTranslated(true); 484 li.mIsPseudo = true; 485 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; 486 } else { 487 // Do not display pseudolocales unless in development mode. 488 continue; 489 } 490 } 491 492 if (simCountries.contains(li.getLocale().getCountry())) { 493 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; 494 } 495 numberSystemLocaleList.forEach(l -> { 496 if (li.getLocale().stripExtensions().equals(l.stripExtensions())) { 497 li.mHasNumberingSystems = true; 498 } 499 }); 500 501 sLocaleCache.put(li.getId(), li); 502 final Locale parent = li.getParent(); 503 if (parent != null) { 504 String parentId = parent.toLanguageTag(); 505 if (!sLocaleCache.containsKey(parentId)) { 506 sLocaleCache.put(parentId, new LocaleInfo(parent)); 507 } 508 } 509 } 510 511 // TODO: See if we can reuse what LocaleList.matchScore does 512 final HashSet<String> localizedLocales = new HashSet<>(); 513 for (String localeId : LocalePicker.getSystemAssetLocales()) { 514 LocaleInfo li = new LocaleInfo(localeId); 515 final String country = li.getLocale().getCountry(); 516 // All this is to figure out if we should suggest a country 517 if (!country.isEmpty()) { 518 LocaleInfo cachedLocale = null; 519 if (sLocaleCache.containsKey(li.getId())) { // the simple case, e.g. fr-CH 520 cachedLocale = sLocaleCache.get(li.getId()); 521 } else { // e.g. zh-TW localized, zh-Hant-TW in cache 522 final String langScriptCtry = li.getLangScriptKey() + "-" + country; 523 if (sLocaleCache.containsKey(langScriptCtry)) { 524 cachedLocale = sLocaleCache.get(langScriptCtry); 525 } 526 } 527 if (cachedLocale != null) { 528 cachedLocale.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CFG; 529 } 530 } 531 localizedLocales.add(li.getLangScriptKey()); 532 } 533 534 for (LocaleInfo li : sLocaleCache.values()) { 535 li.setTranslated(localizedLocales.contains(li.getLangScriptKey())); 536 } 537 538 addSuggestedLocalesForRegion(Locale.getDefault()); 539 540 sFullyInitialized = true; 541 } 542 isShallIgnore( Set<String> ignorables, LocaleInfo li, boolean translatedOnly)543 private static boolean isShallIgnore( 544 Set<String> ignorables, LocaleInfo li, boolean translatedOnly) { 545 if (ignorables.stream().anyMatch(tag -> 546 Locale.forLanguageTag(tag).stripExtensions() 547 .equals(li.getLocale().stripExtensions()))) { 548 return true; 549 } 550 if (li.mIsPseudo) return false; 551 if (translatedOnly && !li.isTranslated()) return true; 552 if (li.getParent() != null) return false; 553 return true; 554 } 555 getLocaleTier(LocaleInfo parent)556 private static int getLocaleTier(LocaleInfo parent) { 557 if (parent == null) { 558 return TIER_LANGUAGE; 559 } else if (parent.getLocale().getCountry().isEmpty()) { 560 return TIER_REGION; 561 } else { 562 return TIER_NUMBERING; 563 } 564 } 565 566 /** 567 * Returns a list of locales for language or region selection. 568 * 569 * If the parent is null, then it is the language list. 570 * 571 * If it is not null, then the list will contain all the locales that belong to that parent. 572 * Example: if the parent is "ar", then the region list will contain all Arabic locales. 573 * (this is not language based, but language-script, so that it works for zh-Hant and so on.) 574 * 575 * If it is not null and has country, then the list will contain all locales with that parent's 576 * language and country, i.e. containing alternate numbering systems. 577 * 578 * Example: if the parent is "ff-Adlm-BF", then the numbering list will contain all 579 * Fula (Adlam, Burkina Faso) i.e. "ff-Adlm-BF" and "ff-Adlm-BF-u-nu-latn" 580 */ 581 @UnsupportedAppUsage getLevelLocales(Context context, Set<String> ignorables, LocaleInfo parent, boolean translatedOnly)582 public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables, 583 LocaleInfo parent, boolean translatedOnly) { 584 return getLevelLocales(context, ignorables, parent, translatedOnly, null); 585 } 586 587 /** 588 * @param explicitLocales Indicates only the locales within this list should be shown in the 589 * locale picker. 590 * 591 * Returns a list of locales for language or region selection. 592 * If the parent is null, then it is the language list. 593 * If it is not null, then the list will contain all the locales that belong to that parent. 594 * Example: if the parent is "ar", then the region list will contain all Arabic locales. 595 * (this is not language based, but language-script, so that it works for zh-Hant and so on. 596 */ getLevelLocales(Context context, Set<String> ignorables, LocaleInfo parent, boolean translatedOnly, LocaleList explicitLocales)597 public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables, 598 LocaleInfo parent, boolean translatedOnly, LocaleList explicitLocales) { 599 if (context != null) { 600 fillCache(context); 601 } 602 HashMap<String, LocaleInfo> supportedLcoaleInfos = 603 explicitLocales == null 604 ? sLocaleCache 605 : convertExplicitLocales(explicitLocales, sLocaleCache.values()); 606 return getTierLocales(ignorables, parent, translatedOnly, supportedLcoaleInfos); 607 } 608 getTierLocales( Set<String> ignorables, LocaleInfo parent, boolean translatedOnly, HashMap<String, LocaleInfo> supportedLocaleInfos)609 private static Set<LocaleInfo> getTierLocales( 610 Set<String> ignorables, 611 LocaleInfo parent, 612 boolean translatedOnly, 613 HashMap<String, LocaleInfo> supportedLocaleInfos) { 614 615 boolean hasTargetParent = parent != null; 616 String parentId = hasTargetParent ? parent.getId() : null; 617 HashSet<LocaleInfo> result = new HashSet<>(); 618 for (LocaleStore.LocaleInfo li : supportedLocaleInfos.values()) { 619 if (isShallIgnore(ignorables, li, translatedOnly)) { 620 continue; 621 } 622 switch(getLocaleTier(parent)) { 623 case TIER_LANGUAGE: 624 if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) { 625 result.add(li); 626 } else { 627 Locale locale = li.getParent(); 628 LocaleInfo localeInfo = getLocaleInfo(locale, supportedLocaleInfos); 629 addLocaleInfoToMap(locale, localeInfo, supportedLocaleInfos); 630 result.add(localeInfo); 631 } 632 break; 633 case TIER_REGION: 634 if (parentId.equals(li.getParent().toLanguageTag())) { 635 Locale locale = li.getLocale().stripExtensions(); 636 LocaleInfo localeInfo = getLocaleInfo(locale, supportedLocaleInfos); 637 addLocaleInfoToMap(locale, localeInfo, supportedLocaleInfos); 638 result.add(localeInfo); 639 } 640 break; 641 case TIER_NUMBERING: 642 if (parent.getLocale().stripExtensions() 643 .equals(li.getLocale().stripExtensions())) { 644 result.add(li); 645 } 646 break; 647 } 648 } 649 return result; 650 } 651 652 /** Converts string array of explicit locales to HashMap */ convertExplicitLocales( LocaleList explicitLocales, Collection<LocaleInfo> localeinfo)653 public static HashMap<String, LocaleInfo> convertExplicitLocales( 654 LocaleList explicitLocales, Collection<LocaleInfo> localeinfo) { 655 // Trys to find the matched locale within android supported locales. If there is no matched 656 // locale, it will still keep the unsupported lcoale in list. 657 // Note: This currently does not support unicode extension check. 658 LocaleList localeList = matchLocaleFromSupportedLocaleList( 659 explicitLocales, localeinfo); 660 661 HashMap<String, LocaleInfo> localeInfos = new HashMap<>(); 662 for (int i = 0; i < localeList.size(); i++) { 663 Locale locale = localeList.get(i); 664 if (locale.toString().isEmpty()) { 665 throw new IllformedLocaleException("Bad locale entry"); 666 } 667 668 LocaleInfo li = new LocaleInfo(locale); 669 if (localeInfos.containsKey(li.getId())) { 670 continue; 671 } 672 localeInfos.put(li.getId(), li); 673 Locale parent = li.getParent(); 674 if (parent != null) { 675 String parentId = parent.toLanguageTag(); 676 if (!localeInfos.containsKey(parentId)) { 677 localeInfos.put(parentId, new LocaleInfo(parent)); 678 } 679 } 680 } 681 return localeInfos; 682 } 683 matchLocaleFromSupportedLocaleList( LocaleList explicitLocales, Collection<LocaleInfo> localeInfos)684 private static LocaleList matchLocaleFromSupportedLocaleList( 685 LocaleList explicitLocales, Collection<LocaleInfo> localeInfos) { 686 if (localeInfos == null) { 687 return explicitLocales; 688 } 689 //TODO: Adds a function for unicode extension if needed. 690 Locale[] resultLocales = new Locale[explicitLocales.size()]; 691 for (int i = 0; i < explicitLocales.size(); i++) { 692 Locale locale = explicitLocales.get(i); 693 if (!TextUtils.isEmpty(locale.getCountry())) { 694 for (LocaleInfo localeInfo :localeInfos) { 695 if (LocaleList.matchesLanguageAndScript(locale, localeInfo.getLocale()) 696 && TextUtils.equals(locale.getCountry(), 697 localeInfo.getLocale().getCountry())) { 698 resultLocales[i] = localeInfo.getLocale(); 699 break; 700 } 701 } 702 } 703 if (resultLocales[i] == null) { 704 resultLocales[i] = locale; 705 } 706 } 707 return new LocaleList(resultLocales); 708 } 709 710 @UnsupportedAppUsage getLocaleInfo(Locale locale)711 public static LocaleInfo getLocaleInfo(Locale locale) { 712 LocaleInfo localeInfo = getLocaleInfo(locale, sLocaleCache); 713 addLocaleInfoToMap(locale, localeInfo, sLocaleCache); 714 return localeInfo; 715 } 716 getLocaleInfo( Locale locale, HashMap<String, LocaleInfo> localeInfos)717 private static LocaleInfo getLocaleInfo( 718 Locale locale, HashMap<String, LocaleInfo> localeInfos) { 719 String id = locale.toLanguageTag(); 720 LocaleInfo result; 721 if (!localeInfos.containsKey(id)) { 722 // Locale preferences can modify the language tag to current system languages, so we 723 // need to check the input locale without extra u extension except numbering system. 724 Locale filteredLocale = getLocaleWithOnlyNumberingSystem(locale); 725 if (localeInfos.containsKey(filteredLocale.toLanguageTag())) { 726 result = new LocaleInfo(locale); 727 LocaleInfo localeInfo = localeInfos.get(filteredLocale.toLanguageTag()); 728 // This locale is included in supported locales, so follow the settings 729 // of supported locales. 730 result.mIsPseudo = localeInfo.mIsPseudo; 731 result.mIsTranslated = localeInfo.mIsTranslated; 732 result.mHasNumberingSystems = localeInfo.mHasNumberingSystems; 733 result.mSuggestionFlags = localeInfo.mSuggestionFlags; 734 return result; 735 } 736 result = new LocaleInfo(locale); 737 } else { 738 result = localeInfos.get(id); 739 } 740 return result; 741 } 742 getLocaleWithOnlyNumberingSystem(Locale locale)743 private static Locale getLocaleWithOnlyNumberingSystem(Locale locale) { 744 return new Locale.Builder() 745 .setLocale(locale.stripExtensions()) 746 .setUnicodeLocaleKeyword("nu", locale.getUnicodeLocaleType("nu")) 747 .build(); 748 } 749 addLocaleInfoToMap(Locale locale, LocaleInfo localeInfo, HashMap<String, LocaleInfo> map)750 private static void addLocaleInfoToMap(Locale locale, LocaleInfo localeInfo, 751 HashMap<String, LocaleInfo> map) { 752 if (!map.containsKey(locale.toLanguageTag())) { 753 Locale localeWithNumberingSystem = getLocaleWithOnlyNumberingSystem(locale); 754 if (!map.containsKey(localeWithNumberingSystem.toLanguageTag())) { 755 map.put(locale.toLanguageTag(), localeInfo); 756 } 757 } 758 } 759 760 /** 761 * API for testing. 762 */ 763 @UnsupportedAppUsage 764 @VisibleForTesting fromLocale(Locale locale)765 public static LocaleInfo fromLocale(Locale locale) { 766 return new LocaleInfo(locale); 767 } 768 } 769