Home | History | Annotate | Download | only in app
      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.content.Context;
     20 import android.os.LocaleList;
     21 import android.provider.Settings;
     22 import android.telephony.TelephonyManager;
     23 
     24 import java.util.HashMap;
     25 import java.util.HashSet;
     26 import java.util.IllformedLocaleException;
     27 import java.util.Locale;
     28 import java.util.Set;
     29 
     30 public class LocaleStore {
     31     private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
     32     private static boolean sFullyInitialized = false;
     33 
     34     public static class LocaleInfo {
     35         private static final int SUGGESTION_TYPE_NONE = 0;
     36         private static final int SUGGESTION_TYPE_SIM = 1 << 0;
     37         private static final int SUGGESTION_TYPE_CFG = 1 << 1;
     38 
     39         private final Locale mLocale;
     40         private final Locale mParent;
     41         private final String mId;
     42         private boolean mIsTranslated;
     43         private boolean mIsPseudo;
     44         private boolean mIsChecked; // Used by the LocaleListEditor to mark entries for deletion
     45         // Combination of flags for various reasons to show a locale as a suggestion.
     46         // Can be SIM, location, etc.
     47         private int mSuggestionFlags;
     48 
     49         private String mFullNameNative;
     50         private String mFullCountryNameNative;
     51         private String mLangScriptKey;
     52 
     53         private LocaleInfo(Locale locale) {
     54             this.mLocale = locale;
     55             this.mId = locale.toLanguageTag();
     56             this.mParent = getParent(locale);
     57             this.mIsChecked = false;
     58             this.mSuggestionFlags = SUGGESTION_TYPE_NONE;
     59             this.mIsTranslated = false;
     60             this.mIsPseudo = false;
     61         }
     62 
     63         private LocaleInfo(String localeId) {
     64             this(Locale.forLanguageTag(localeId));
     65         }
     66 
     67         private static Locale getParent(Locale locale) {
     68             if (locale.getCountry().isEmpty()) {
     69                 return null;
     70             }
     71             return new Locale.Builder()
     72                     .setLocale(locale)
     73                     .setRegion("")
     74                     .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
     75                     .build();
     76         }
     77 
     78         @Override
     79         public String toString() {
     80             return mId;
     81         }
     82 
     83         public Locale getLocale() {
     84             return mLocale;
     85         }
     86 
     87         public Locale getParent() {
     88             return mParent;
     89         }
     90 
     91         public String getId() {
     92             return mId;
     93         }
     94 
     95         public boolean isTranslated() {
     96             return mIsTranslated;
     97         }
     98 
     99         public void setTranslated(boolean isTranslated) {
    100             mIsTranslated = isTranslated;
    101         }
    102 
    103         /* package */ boolean isSuggested() {
    104             if (!mIsTranslated) { // Never suggest an untranslated locale
    105                 return false;
    106             }
    107             return mSuggestionFlags != SUGGESTION_TYPE_NONE;
    108         }
    109 
    110         private boolean isSuggestionOfType(int suggestionMask) {
    111             if (!mIsTranslated) { // Never suggest an untranslated locale
    112                 return false;
    113             }
    114             return (mSuggestionFlags & suggestionMask) == suggestionMask;
    115         }
    116 
    117         public String getFullNameNative() {
    118             if (mFullNameNative == null) {
    119                 mFullNameNative =
    120                         LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */);
    121             }
    122             return mFullNameNative;
    123         }
    124 
    125         String getFullCountryNameNative() {
    126             if (mFullCountryNameNative == null) {
    127                 mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale);
    128             }
    129             return mFullCountryNameNative;
    130         }
    131 
    132         String getFullCountryNameInUiLanguage() {
    133             // We don't cache the UI name because the default locale keeps changing
    134             return LocaleHelper.getDisplayCountry(mLocale);
    135         }
    136 
    137         /** Returns the name of the locale in the language of the UI.
    138          * It is used for search, but never shown.
    139          * For instance German will show as "Deutsch" in the list, but we will also search for
    140          * "allemand" if the system UI is in French.
    141          */
    142         public String getFullNameInUiLanguage() {
    143             // We don't cache the UI name because the default locale keeps changing
    144             return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
    145         }
    146 
    147         private String getLangScriptKey() {
    148             if (mLangScriptKey == null) {
    149                 Locale baseLocale = new Locale.Builder()
    150                     .setLocale(mLocale)
    151                     .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
    152                     .build();
    153                 Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(baseLocale));
    154                 mLangScriptKey =
    155                         (parentWithScript == null)
    156                         ? mLocale.toLanguageTag()
    157                         : parentWithScript.toLanguageTag();
    158             }
    159             return mLangScriptKey;
    160         }
    161 
    162         String getLabel(boolean countryMode) {
    163             if (countryMode) {
    164                 return getFullCountryNameNative();
    165             } else {
    166                 return getFullNameNative();
    167             }
    168         }
    169 
    170         String getContentDescription(boolean countryMode) {
    171             if (countryMode) {
    172                 return getFullCountryNameInUiLanguage();
    173             } else {
    174                 return getFullNameInUiLanguage();
    175             }
    176         }
    177 
    178         public boolean getChecked() {
    179             return mIsChecked;
    180         }
    181 
    182         public void setChecked(boolean checked) {
    183             mIsChecked = checked;
    184         }
    185     }
    186 
    187     private static Set<String> getSimCountries(Context context) {
    188         Set<String> result = new HashSet<>();
    189 
    190         TelephonyManager tm = TelephonyManager.from(context);
    191 
    192         if (tm != null) {
    193             String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
    194             if (!iso.isEmpty()) {
    195                 result.add(iso);
    196             }
    197 
    198             iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
    199             if (!iso.isEmpty()) {
    200                 result.add(iso);
    201             }
    202         }
    203 
    204         return result;
    205     }
    206 
    207     /*
    208      * This method is added for SetupWizard, to force an update of the suggested locales
    209      * when the SIM is initialized.
    210      *
    211      * <p>When the device is freshly started, it sometimes gets to the language selection
    212      * before the SIM is properly initialized.
    213      * So at the time the cache is filled, the info from the SIM might not be available.
    214      * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events.
    215      * SetupWizard will call this function when that happens.</p>
    216      *
    217      * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code.
    218      * The user might change the SIM or might cross border and connect to a network
    219      * in a different country, without restarting the Settings application or the phone.</p>
    220      */
    221     public static void updateSimCountries(Context context) {
    222         Set<String> simCountries = getSimCountries(context);
    223 
    224         for (LocaleInfo li : sLocaleCache.values()) {
    225             // This method sets the suggestion flags for the (new) SIM locales, but it does not
    226             // try to clean up the old flags. After all, if the user replaces a German SIM
    227             // with a French one, it is still possible that they are speaking German.
    228             // So both French and German are reasonable suggestions.
    229             if (simCountries.contains(li.getLocale().getCountry())) {
    230                 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
    231             }
    232         }
    233     }
    234 
    235     /*
    236      * Show all the languages supported for a country in the suggested list.
    237      * This is also handy for devices without SIM (tablets).
    238      */
    239     private static void addSuggestedLocalesForRegion(Locale locale) {
    240         if (locale == null) {
    241             return;
    242         }
    243         final String country = locale.getCountry();
    244         if (country.isEmpty()) {
    245             return;
    246         }
    247 
    248         for (LocaleInfo li : sLocaleCache.values()) {
    249             if (country.equals(li.getLocale().getCountry())) {
    250                 // We don't need to differentiate between manual and SIM suggestions
    251                 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
    252             }
    253         }
    254     }
    255 
    256     public static void fillCache(Context context) {
    257         if (sFullyInitialized) {
    258             return;
    259         }
    260 
    261         Set<String> simCountries = getSimCountries(context);
    262 
    263         final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
    264                 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
    265         for (String localeId : LocalePicker.getSupportedLocales(context)) {
    266             if (localeId.isEmpty()) {
    267                 throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
    268             }
    269             LocaleInfo li = new LocaleInfo(localeId);
    270 
    271             if (LocaleList.isPseudoLocale(li.getLocale())) {
    272                 if (isInDeveloperMode) {
    273                     li.setTranslated(true);
    274                     li.mIsPseudo = true;
    275                     li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
    276                 } else {
    277                     // Do not display pseudolocales unless in development mode.
    278                     continue;
    279                 }
    280             }
    281 
    282             if (simCountries.contains(li.getLocale().getCountry())) {
    283                 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
    284             }
    285             sLocaleCache.put(li.getId(), li);
    286             final Locale parent = li.getParent();
    287             if (parent != null) {
    288                 String parentId = parent.toLanguageTag();
    289                 if (!sLocaleCache.containsKey(parentId)) {
    290                     sLocaleCache.put(parentId, new LocaleInfo(parent));
    291                 }
    292             }
    293         }
    294 
    295         // TODO: See if we can reuse what LocaleList.matchScore does
    296         final HashSet<String> localizedLocales = new HashSet<>();
    297         for (String localeId : LocalePicker.getSystemAssetLocales()) {
    298             LocaleInfo li = new LocaleInfo(localeId);
    299             final String country = li.getLocale().getCountry();
    300             // All this is to figure out if we should suggest a country
    301             if (!country.isEmpty()) {
    302                 LocaleInfo cachedLocale = null;
    303                 if (sLocaleCache.containsKey(li.getId())) { // the simple case, e.g. fr-CH
    304                     cachedLocale = sLocaleCache.get(li.getId());
    305                 } else { // e.g. zh-TW localized, zh-Hant-TW in cache
    306                     final String langScriptCtry = li.getLangScriptKey() + "-" + country;
    307                     if (sLocaleCache.containsKey(langScriptCtry)) {
    308                         cachedLocale = sLocaleCache.get(langScriptCtry);
    309                     }
    310                 }
    311                 if (cachedLocale != null) {
    312                     cachedLocale.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CFG;
    313                 }
    314             }
    315             localizedLocales.add(li.getLangScriptKey());
    316         }
    317 
    318         for (LocaleInfo li : sLocaleCache.values()) {
    319             li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
    320         }
    321 
    322         addSuggestedLocalesForRegion(Locale.getDefault());
    323 
    324         sFullyInitialized = true;
    325     }
    326 
    327     private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
    328         if (ignorables.contains(li.getId())) return 0;
    329         if (li.mIsPseudo) return 2;
    330         if (translatedOnly && !li.isTranslated()) return 0;
    331         if (li.getParent() != null) return 2;
    332         return 0;
    333     }
    334 
    335     /**
    336      * Returns a list of locales for language or region selection.
    337      * If the parent is null, then it is the language list.
    338      * If it is not null, then the list will contain all the locales that belong to that parent.
    339      * Example: if the parent is "ar", then the region list will contain all Arabic locales.
    340      * (this is not language based, but language-script, so that it works for zh-Hant and so on.
    341      */
    342     public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
    343             LocaleInfo parent, boolean translatedOnly) {
    344         fillCache(context);
    345         String parentId = parent == null ? null : parent.getId();
    346 
    347         HashSet<LocaleInfo> result = new HashSet<>();
    348         for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
    349             int level = getLevel(ignorables, li, translatedOnly);
    350             if (level == 2) {
    351                 if (parent != null) { // region selection
    352                     if (parentId.equals(li.getParent().toLanguageTag())) {
    353                         result.add(li);
    354                     }
    355                 } else { // language selection
    356                     if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
    357                         result.add(li);
    358                     } else {
    359                         result.add(getLocaleInfo(li.getParent()));
    360                     }
    361                 }
    362             }
    363         }
    364         return result;
    365     }
    366 
    367     public static LocaleInfo getLocaleInfo(Locale locale) {
    368         String id = locale.toLanguageTag();
    369         LocaleInfo result;
    370         if (!sLocaleCache.containsKey(id)) {
    371             result = new LocaleInfo(locale);
    372             sLocaleCache.put(id, result);
    373         } else {
    374             result = sLocaleCache.get(id);
    375         }
    376         return result;
    377     }
    378 }
    379