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