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