LocaleStore.java revision c67b64fda4324d366da18014791406f32f48d025
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 if (!mIsTranslated) { // Never suggest an untranslated locale 108 return false; 109 } 110 return (mSuggestionFlags & suggestionMask) == suggestionMask; 111 } 112 113 public String getFullNameNative() { 114 if (mFullNameNative == null) { 115 mFullNameNative = 116 LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */); 117 } 118 return mFullNameNative; 119 } 120 121 String getFullCountryNameNative() { 122 if (mFullCountryNameNative == null) { 123 mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale); 124 } 125 return mFullCountryNameNative; 126 } 127 128 /** Returns the name of the locale in the language of the UI. 129 * It is used for search, but never shown. 130 * For instance German will show as "Deutsch" in the list, but we will also search for 131 * "allemand" if the system UI is in French. 132 */ 133 public String getFullNameInUiLanguage() { 134 return LocaleHelper.getDisplayName(mLocale, true /* sentence case */); 135 } 136 137 private String getLangScriptKey() { 138 if (mLangScriptKey == null) { 139 Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(mLocale)); 140 mLangScriptKey = 141 (parentWithScript == null) 142 ? mLocale.toLanguageTag() 143 : parentWithScript.toLanguageTag(); 144 } 145 return mLangScriptKey; 146 } 147 148 String getLabel() { 149 if (getParent() == null || this.isSuggestionOfType(SUGGESTION_TYPE_SIM)) { 150 return getFullNameNative(); 151 } else { 152 return getFullCountryNameNative(); 153 } 154 } 155 156 public boolean getChecked() { 157 return mIsChecked; 158 } 159 160 public void setChecked(boolean checked) { 161 mIsChecked = checked; 162 } 163 } 164 165 private static Set<String> getSimCountries(Context context) { 166 Set<String> result = new HashSet<>(); 167 168 TelephonyManager tm = TelephonyManager.from(context); 169 170 if (tm != null) { 171 String iso = tm.getSimCountryIso().toUpperCase(Locale.US); 172 if (!iso.isEmpty()) { 173 result.add(iso); 174 } 175 176 iso = tm.getNetworkCountryIso().toUpperCase(Locale.US); 177 if (!iso.isEmpty()) { 178 result.add(iso); 179 } 180 } 181 182 return result; 183 } 184 185 /* 186 * This method is added for SetupWizard, to force an update of the suggested locales 187 * when the SIM is initialized. 188 * 189 * <p>When the device is freshly started, it sometimes gets to the language selection 190 * before the SIM is properly initialized. 191 * So at the time the cache is filled, the info from the SIM might not be available. 192 * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events. 193 * SetupWizard will call this function when that happens.</p> 194 * 195 * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code. 196 * The user might change the SIM or might cross border and connect to a network 197 * in a different country, without restarting the Settings application or the phone.</p> 198 */ 199 public static void updateSimCountries(Context context) { 200 Set<String> simCountries = getSimCountries(context); 201 202 for (LocaleInfo li : sLocaleCache.values()) { 203 // This method sets the suggestion flags for the (new) SIM locales, but it does not 204 // try to clean up the old flags. After all, if the user replaces a German SIM 205 // with a French one, it is still possible that they are speaking German. 206 // So both French and German are reasonable suggestions. 207 if (simCountries.contains(li.getLocale().getCountry())) { 208 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; 209 } 210 } 211 } 212 213 /* 214 * Show all the languages supported for a country in the suggested list. 215 * This is also handy for devices without SIM (tablets). 216 */ 217 private static void addSuggestedLocalesForRegion(Locale locale) { 218 if (locale == null) { 219 return; 220 } 221 final String country = locale.getCountry(); 222 if (country.isEmpty()) { 223 return; 224 } 225 226 for (LocaleInfo li : sLocaleCache.values()) { 227 if (country.equals(li.getLocale().getCountry())) { 228 // We don't need to differentiate between manual and SIM suggestions 229 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; 230 } 231 } 232 } 233 234 public static void fillCache(Context context) { 235 if (sFullyInitialized) { 236 return; 237 } 238 239 Set<String> simCountries = getSimCountries(context); 240 241 for (String localeId : LocalePicker.getSupportedLocales(context)) { 242 if (localeId.isEmpty()) { 243 throw new IllformedLocaleException("Bad locale entry in locale_config.xml"); 244 } 245 LocaleInfo li = new LocaleInfo(localeId); 246 if (simCountries.contains(li.getLocale().getCountry())) { 247 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; 248 } 249 sLocaleCache.put(li.getId(), li); 250 final Locale parent = li.getParent(); 251 if (parent != null) { 252 String parentId = parent.toLanguageTag(); 253 if (!sLocaleCache.containsKey(parentId)) { 254 sLocaleCache.put(parentId, new LocaleInfo(parent)); 255 } 256 } 257 } 258 259 boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(), 260 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; 261 for (String localeId : LocalePicker.getPseudoLocales()) { 262 LocaleInfo li = getLocaleInfo(Locale.forLanguageTag(localeId)); 263 if (isInDeveloperMode) { 264 li.setTranslated(true); 265 li.mIsPseudo = true; 266 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; 267 } else { 268 sLocaleCache.remove(li.getId()); 269 } 270 } 271 272 // TODO: See if we can reuse what LocaleList.matchScore does 273 final HashSet<String> localizedLocales = new HashSet<>(); 274 for (String localeId : LocalePicker.getSystemAssetLocales()) { 275 LocaleInfo li = new LocaleInfo(localeId); 276 localizedLocales.add(li.getLangScriptKey()); 277 } 278 279 for (LocaleInfo li : sLocaleCache.values()) { 280 li.setTranslated(localizedLocales.contains(li.getLangScriptKey())); 281 } 282 283 addSuggestedLocalesForRegion(Locale.getDefault()); 284 285 sFullyInitialized = true; 286 } 287 288 private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) { 289 if (ignorables.contains(li.getId())) return 0; 290 if (li.mIsPseudo) return 2; 291 if (translatedOnly && !li.isTranslated()) return 0; 292 if (li.getParent() != null) return 2; 293 return 0; 294 } 295 296 /** 297 * Returns a list of locales for language or region selection. 298 * If the parent is null, then it is the language list. 299 * If it is not null, then the list will contain all the locales that belong to that parent. 300 * Example: if the parent is "ar", then the region list will contain all Arabic locales. 301 * (this is not language based, but language-script, so that it works for zh-Hant and so on. 302 */ 303 public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables, 304 LocaleInfo parent, boolean translatedOnly) { 305 fillCache(context); 306 String parentId = parent == null ? null : parent.getId(); 307 308 HashSet<LocaleInfo> result = new HashSet<>(); 309 for (LocaleStore.LocaleInfo li : sLocaleCache.values()) { 310 int level = getLevel(ignorables, li, translatedOnly); 311 if (level == 2) { 312 if (parent != null) { // region selection 313 if (parentId.equals(li.getParent().toLanguageTag())) { 314 if (!li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) { 315 result.add(li); 316 } 317 } 318 } else { // language selection 319 if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) { 320 result.add(li); 321 } else { 322 result.add(getLocaleInfo(li.getParent())); 323 } 324 } 325 } 326 } 327 return result; 328 } 329 330 public static LocaleInfo getLocaleInfo(Locale locale) { 331 String id = locale.toLanguageTag(); 332 LocaleInfo result; 333 if (!sLocaleCache.containsKey(id)) { 334 result = new LocaleInfo(locale); 335 sLocaleCache.put(id, result); 336 } else { 337 result = sLocaleCache.get(id); 338 } 339 return result; 340 } 341} 342