blob: 2b0b5eec6c56c461415ee4a642629f26736e513a [file] [log] [blame]
Mihai Nita1808ff72016-01-12 08:53:54 -08001/*
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;
Igor Viarheichyk025402c2017-10-06 12:46:49 -070020import android.os.LocaleList;
Mihai Nita1808ff72016-01-12 08:53:54 -080021import android.provider.Settings;
22import android.telephony.TelephonyManager;
23
24import java.util.HashMap;
25import java.util.HashSet;
26import java.util.IllformedLocaleException;
27import java.util.Locale;
28import java.util.Set;
29
30public class LocaleStore {
31 private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
32 private static boolean sFullyInitialized = false;
33
34 public static class LocaleInfo {
Mihai Nita86235d42016-04-01 11:50:59 -070035 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;
Mihai Nita1808ff72016-01-12 08:53:54 -080038
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()
Igor Viarheichyk025402c2017-10-06 12:46:49 -070072 .setLocale(locale)
73 .setRegion("")
74 .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
Mihai Nita1808ff72016-01-12 08:53:54 -080075 .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) {
Mihai Nitac67b64f2016-02-05 14:27:55 -0800111 if (!mIsTranslated) { // Never suggest an untranslated locale
112 return false;
113 }
Mihai Nita1808ff72016-01-12 08:53:54 -0800114 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
Mihai Nita43af6362016-04-19 09:09:07 -0700132 String getFullCountryNameInUiLanguage() {
133 // We don't cache the UI name because the default locale keeps changing
134 return LocaleHelper.getDisplayCountry(mLocale);
135 }
136
Mihai Nita1808ff72016-01-12 08:53:54 -0800137 /** 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() {
Mihai Nita43af6362016-04-19 09:09:07 -0700143 // We don't cache the UI name because the default locale keeps changing
Mihai Nita1808ff72016-01-12 08:53:54 -0800144 return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
145 }
146
147 private String getLangScriptKey() {
148 if (mLangScriptKey == null) {
149 Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(mLocale));
150 mLangScriptKey =
151 (parentWithScript == null)
152 ? mLocale.toLanguageTag()
153 : parentWithScript.toLanguageTag();
154 }
155 return mLangScriptKey;
156 }
157
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800158 String getLabel(boolean countryMode) {
159 if (countryMode) {
Mihai Nita1808ff72016-01-12 08:53:54 -0800160 return getFullCountryNameNative();
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800161 } else {
162 return getFullNameNative();
Mihai Nita1808ff72016-01-12 08:53:54 -0800163 }
164 }
165
Mihai Nita43af6362016-04-19 09:09:07 -0700166 String getContentDescription(boolean countryMode) {
167 if (countryMode) {
168 return getFullCountryNameInUiLanguage();
169 } else {
170 return getFullNameInUiLanguage();
171 }
172 }
173
Mihai Nita1808ff72016-01-12 08:53:54 -0800174 public boolean getChecked() {
175 return mIsChecked;
176 }
177
178 public void setChecked(boolean checked) {
179 mIsChecked = checked;
180 }
181 }
182
183 private static Set<String> getSimCountries(Context context) {
184 Set<String> result = new HashSet<>();
185
186 TelephonyManager tm = TelephonyManager.from(context);
187
188 if (tm != null) {
189 String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
190 if (!iso.isEmpty()) {
191 result.add(iso);
192 }
193
194 iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
195 if (!iso.isEmpty()) {
196 result.add(iso);
197 }
198 }
199
200 return result;
201 }
202
Mihai Nita137b96e2016-01-25 11:31:15 -0800203 /*
204 * This method is added for SetupWizard, to force an update of the suggested locales
205 * when the SIM is initialized.
206 *
207 * <p>When the device is freshly started, it sometimes gets to the language selection
208 * before the SIM is properly initialized.
209 * So at the time the cache is filled, the info from the SIM might not be available.
210 * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events.
211 * SetupWizard will call this function when that happens.</p>
212 *
213 * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code.
214 * The user might change the SIM or might cross border and connect to a network
215 * in a different country, without restarting the Settings application or the phone.</p>
216 */
217 public static void updateSimCountries(Context context) {
218 Set<String> simCountries = getSimCountries(context);
219
220 for (LocaleInfo li : sLocaleCache.values()) {
221 // This method sets the suggestion flags for the (new) SIM locales, but it does not
222 // try to clean up the old flags. After all, if the user replaces a German SIM
223 // with a French one, it is still possible that they are speaking German.
224 // So both French and German are reasonable suggestions.
225 if (simCountries.contains(li.getLocale().getCountry())) {
226 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
227 }
228 }
229 }
230
Mihai Nitac67b64f2016-02-05 14:27:55 -0800231 /*
232 * Show all the languages supported for a country in the suggested list.
233 * This is also handy for devices without SIM (tablets).
234 */
235 private static void addSuggestedLocalesForRegion(Locale locale) {
236 if (locale == null) {
237 return;
238 }
239 final String country = locale.getCountry();
240 if (country.isEmpty()) {
241 return;
242 }
243
244 for (LocaleInfo li : sLocaleCache.values()) {
245 if (country.equals(li.getLocale().getCountry())) {
246 // We don't need to differentiate between manual and SIM suggestions
247 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
248 }
249 }
250 }
251
Mihai Nita1808ff72016-01-12 08:53:54 -0800252 public static void fillCache(Context context) {
253 if (sFullyInitialized) {
254 return;
255 }
256
257 Set<String> simCountries = getSimCountries(context);
258
Igor Viarheichyk025402c2017-10-06 12:46:49 -0700259 final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
260 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
Mihai Nita1808ff72016-01-12 08:53:54 -0800261 for (String localeId : LocalePicker.getSupportedLocales(context)) {
262 if (localeId.isEmpty()) {
263 throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
264 }
265 LocaleInfo li = new LocaleInfo(localeId);
Igor Viarheichyk025402c2017-10-06 12:46:49 -0700266
267 if (LocaleList.isPseudoLocale(li.getLocale())) {
268 if (isInDeveloperMode) {
269 li.setTranslated(true);
270 li.mIsPseudo = true;
271 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
272 } else {
273 // Do not display pseudolocales unless in development mode.
274 continue;
275 }
276 }
277
Mihai Nita1808ff72016-01-12 08:53:54 -0800278 if (simCountries.contains(li.getLocale().getCountry())) {
279 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
280 }
281 sLocaleCache.put(li.getId(), li);
282 final Locale parent = li.getParent();
283 if (parent != null) {
284 String parentId = parent.toLanguageTag();
285 if (!sLocaleCache.containsKey(parentId)) {
286 sLocaleCache.put(parentId, new LocaleInfo(parent));
287 }
288 }
289 }
290
Mihai Nita1808ff72016-01-12 08:53:54 -0800291 // TODO: See if we can reuse what LocaleList.matchScore does
292 final HashSet<String> localizedLocales = new HashSet<>();
293 for (String localeId : LocalePicker.getSystemAssetLocales()) {
294 LocaleInfo li = new LocaleInfo(localeId);
Mihai Nita86235d42016-04-01 11:50:59 -0700295 final String country = li.getLocale().getCountry();
296 // All this is to figure out if we should suggest a country
297 if (!country.isEmpty()) {
298 LocaleInfo cachedLocale = null;
299 if (sLocaleCache.containsKey(li.getId())) { // the simple case, e.g. fr-CH
300 cachedLocale = sLocaleCache.get(li.getId());
301 } else { // e.g. zh-TW localized, zh-Hant-TW in cache
302 final String langScriptCtry = li.getLangScriptKey() + "-" + country;
303 if (sLocaleCache.containsKey(langScriptCtry)) {
304 cachedLocale = sLocaleCache.get(langScriptCtry);
305 }
306 }
307 if (cachedLocale != null) {
308 cachedLocale.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CFG;
309 }
310 }
Mihai Nita1808ff72016-01-12 08:53:54 -0800311 localizedLocales.add(li.getLangScriptKey());
312 }
313
314 for (LocaleInfo li : sLocaleCache.values()) {
315 li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
316 }
317
Mihai Nitac67b64f2016-02-05 14:27:55 -0800318 addSuggestedLocalesForRegion(Locale.getDefault());
319
Mihai Nita1808ff72016-01-12 08:53:54 -0800320 sFullyInitialized = true;
321 }
322
323 private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
324 if (ignorables.contains(li.getId())) return 0;
325 if (li.mIsPseudo) return 2;
326 if (translatedOnly && !li.isTranslated()) return 0;
327 if (li.getParent() != null) return 2;
328 return 0;
329 }
330
331 /**
332 * Returns a list of locales for language or region selection.
333 * If the parent is null, then it is the language list.
Mihai Nita137b96e2016-01-25 11:31:15 -0800334 * If it is not null, then the list will contain all the locales that belong to that parent.
Mihai Nita1808ff72016-01-12 08:53:54 -0800335 * Example: if the parent is "ar", then the region list will contain all Arabic locales.
336 * (this is not language based, but language-script, so that it works for zh-Hant and so on.
337 */
Mihai Nita137b96e2016-01-25 11:31:15 -0800338 public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
Mihai Nita1808ff72016-01-12 08:53:54 -0800339 LocaleInfo parent, boolean translatedOnly) {
340 fillCache(context);
341 String parentId = parent == null ? null : parent.getId();
342
343 HashSet<LocaleInfo> result = new HashSet<>();
344 for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
345 int level = getLevel(ignorables, li, translatedOnly);
346 if (level == 2) {
347 if (parent != null) { // region selection
348 if (parentId.equals(li.getParent().toLanguageTag())) {
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800349 result.add(li);
Mihai Nita1808ff72016-01-12 08:53:54 -0800350 }
351 } else { // language selection
352 if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
353 result.add(li);
354 } else {
355 result.add(getLocaleInfo(li.getParent()));
356 }
357 }
358 }
359 }
360 return result;
361 }
362
363 public static LocaleInfo getLocaleInfo(Locale locale) {
364 String id = locale.toLanguageTag();
365 LocaleInfo result;
366 if (!sLocaleCache.containsKey(id)) {
367 result = new LocaleInfo(locale);
368 sLocaleCache.put(id, result);
369 } else {
370 result = sLocaleCache.get(id);
371 }
372 return result;
373 }
374}