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