blob: 49f77e11cf80b6882e78b370e40d934d1c38475e [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
Andrei Onea15884392019-03-22 17:28:11 +000019import android.annotation.UnsupportedAppUsage;
Mihai Nita1808ff72016-01-12 08:53:54 -080020import android.content.Context;
Igor Viarheichyk025402c2017-10-06 12:46:49 -070021import android.os.LocaleList;
Mihai Nita1808ff72016-01-12 08:53:54 -080022import android.provider.Settings;
23import android.telephony.TelephonyManager;
24
tmfangb2c5ac82018-07-13 18:14:12 +080025import java.io.Serializable;
Mihai Nita1808ff72016-01-12 08:53:54 -080026import java.util.HashMap;
27import java.util.HashSet;
28import java.util.IllformedLocaleException;
29import java.util.Locale;
30import java.util.Set;
31
32public class LocaleStore {
33 private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
34 private static boolean sFullyInitialized = false;
35
tmfangb2c5ac82018-07-13 18:14:12 +080036 public static class LocaleInfo implements Serializable {
Mihai Nita86235d42016-04-01 11:50:59 -070037 private static final int SUGGESTION_TYPE_NONE = 0;
38 private static final int SUGGESTION_TYPE_SIM = 1 << 0;
39 private static final int SUGGESTION_TYPE_CFG = 1 << 1;
Mihai Nita1808ff72016-01-12 08:53:54 -080040
41 private final Locale mLocale;
42 private final Locale mParent;
43 private final String mId;
44 private boolean mIsTranslated;
45 private boolean mIsPseudo;
46 private boolean mIsChecked; // Used by the LocaleListEditor to mark entries for deletion
47 // Combination of flags for various reasons to show a locale as a suggestion.
48 // Can be SIM, location, etc.
49 private int mSuggestionFlags;
50
51 private String mFullNameNative;
52 private String mFullCountryNameNative;
53 private String mLangScriptKey;
54
55 private LocaleInfo(Locale locale) {
56 this.mLocale = locale;
57 this.mId = locale.toLanguageTag();
58 this.mParent = getParent(locale);
59 this.mIsChecked = false;
60 this.mSuggestionFlags = SUGGESTION_TYPE_NONE;
61 this.mIsTranslated = false;
62 this.mIsPseudo = false;
63 }
64
65 private LocaleInfo(String localeId) {
66 this(Locale.forLanguageTag(localeId));
67 }
68
69 private static Locale getParent(Locale locale) {
70 if (locale.getCountry().isEmpty()) {
71 return null;
72 }
73 return new Locale.Builder()
Igor Viarheichyk025402c2017-10-06 12:46:49 -070074 .setLocale(locale)
75 .setRegion("")
76 .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
Mihai Nita1808ff72016-01-12 08:53:54 -080077 .build();
78 }
79
80 @Override
81 public String toString() {
82 return mId;
83 }
84
Andrei Onea15884392019-03-22 17:28:11 +000085 @UnsupportedAppUsage
Mihai Nita1808ff72016-01-12 08:53:54 -080086 public Locale getLocale() {
87 return mLocale;
88 }
89
Andrei Onea15884392019-03-22 17:28:11 +000090 @UnsupportedAppUsage
Mihai Nita1808ff72016-01-12 08:53:54 -080091 public Locale getParent() {
92 return mParent;
93 }
94
Andrei Onea15884392019-03-22 17:28:11 +000095 @UnsupportedAppUsage
Mihai Nita1808ff72016-01-12 08:53:54 -080096 public String getId() {
97 return mId;
98 }
99
100 public boolean isTranslated() {
101 return mIsTranslated;
102 }
103
104 public void setTranslated(boolean isTranslated) {
105 mIsTranslated = isTranslated;
106 }
107
108 /* package */ boolean isSuggested() {
109 if (!mIsTranslated) { // Never suggest an untranslated locale
110 return false;
111 }
112 return mSuggestionFlags != SUGGESTION_TYPE_NONE;
113 }
114
115 private boolean isSuggestionOfType(int suggestionMask) {
Mihai Nitac67b64f2016-02-05 14:27:55 -0800116 if (!mIsTranslated) { // Never suggest an untranslated locale
117 return false;
118 }
Mihai Nita1808ff72016-01-12 08:53:54 -0800119 return (mSuggestionFlags & suggestionMask) == suggestionMask;
120 }
121
Andrei Onea15884392019-03-22 17:28:11 +0000122 @UnsupportedAppUsage
Mihai Nita1808ff72016-01-12 08:53:54 -0800123 public String getFullNameNative() {
124 if (mFullNameNative == null) {
125 mFullNameNative =
126 LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */);
127 }
128 return mFullNameNative;
129 }
130
131 String getFullCountryNameNative() {
132 if (mFullCountryNameNative == null) {
133 mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale);
134 }
135 return mFullCountryNameNative;
136 }
137
Mihai Nita43af6362016-04-19 09:09:07 -0700138 String getFullCountryNameInUiLanguage() {
139 // We don't cache the UI name because the default locale keeps changing
140 return LocaleHelper.getDisplayCountry(mLocale);
141 }
142
Mihai Nita1808ff72016-01-12 08:53:54 -0800143 /** Returns the name of the locale in the language of the UI.
144 * It is used for search, but never shown.
145 * For instance German will show as "Deutsch" in the list, but we will also search for
146 * "allemand" if the system UI is in French.
147 */
Andrei Onea15884392019-03-22 17:28:11 +0000148 @UnsupportedAppUsage
Mihai Nita1808ff72016-01-12 08:53:54 -0800149 public String getFullNameInUiLanguage() {
Mihai Nita43af6362016-04-19 09:09:07 -0700150 // We don't cache the UI name because the default locale keeps changing
Mihai Nita1808ff72016-01-12 08:53:54 -0800151 return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
152 }
153
154 private String getLangScriptKey() {
155 if (mLangScriptKey == null) {
Igor Viarheichyk6a29b492018-01-24 15:42:58 -0800156 Locale baseLocale = new Locale.Builder()
157 .setLocale(mLocale)
158 .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
159 .build();
160 Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(baseLocale));
Mihai Nita1808ff72016-01-12 08:53:54 -0800161 mLangScriptKey =
162 (parentWithScript == null)
163 ? mLocale.toLanguageTag()
164 : parentWithScript.toLanguageTag();
165 }
166 return mLangScriptKey;
167 }
168
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800169 String getLabel(boolean countryMode) {
170 if (countryMode) {
Mihai Nita1808ff72016-01-12 08:53:54 -0800171 return getFullCountryNameNative();
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800172 } else {
173 return getFullNameNative();
Mihai Nita1808ff72016-01-12 08:53:54 -0800174 }
175 }
176
Mihai Nita43af6362016-04-19 09:09:07 -0700177 String getContentDescription(boolean countryMode) {
178 if (countryMode) {
179 return getFullCountryNameInUiLanguage();
180 } else {
181 return getFullNameInUiLanguage();
182 }
183 }
184
Mihai Nita1808ff72016-01-12 08:53:54 -0800185 public boolean getChecked() {
186 return mIsChecked;
187 }
188
189 public void setChecked(boolean checked) {
190 mIsChecked = checked;
191 }
192 }
193
194 private static Set<String> getSimCountries(Context context) {
195 Set<String> result = new HashSet<>();
196
Jayachandran C16dce222019-11-15 15:42:01 -0800197 TelephonyManager tm = context.getSystemService(TelephonyManager.class);
Mihai Nita1808ff72016-01-12 08:53:54 -0800198
199 if (tm != null) {
200 String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
201 if (!iso.isEmpty()) {
202 result.add(iso);
203 }
204
205 iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
206 if (!iso.isEmpty()) {
207 result.add(iso);
208 }
209 }
210
211 return result;
212 }
213
Mihai Nita137b96e2016-01-25 11:31:15 -0800214 /*
215 * This method is added for SetupWizard, to force an update of the suggested locales
216 * when the SIM is initialized.
217 *
218 * <p>When the device is freshly started, it sometimes gets to the language selection
219 * before the SIM is properly initialized.
220 * So at the time the cache is filled, the info from the SIM might not be available.
221 * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events.
222 * SetupWizard will call this function when that happens.</p>
223 *
224 * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code.
225 * The user might change the SIM or might cross border and connect to a network
226 * in a different country, without restarting the Settings application or the phone.</p>
227 */
228 public static void updateSimCountries(Context context) {
229 Set<String> simCountries = getSimCountries(context);
230
231 for (LocaleInfo li : sLocaleCache.values()) {
232 // This method sets the suggestion flags for the (new) SIM locales, but it does not
233 // try to clean up the old flags. After all, if the user replaces a German SIM
234 // with a French one, it is still possible that they are speaking German.
235 // So both French and German are reasonable suggestions.
236 if (simCountries.contains(li.getLocale().getCountry())) {
237 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
238 }
239 }
240 }
241
Mihai Nitac67b64f2016-02-05 14:27:55 -0800242 /*
243 * Show all the languages supported for a country in the suggested list.
244 * This is also handy for devices without SIM (tablets).
245 */
246 private static void addSuggestedLocalesForRegion(Locale locale) {
247 if (locale == null) {
248 return;
249 }
250 final String country = locale.getCountry();
251 if (country.isEmpty()) {
252 return;
253 }
254
255 for (LocaleInfo li : sLocaleCache.values()) {
256 if (country.equals(li.getLocale().getCountry())) {
257 // We don't need to differentiate between manual and SIM suggestions
258 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
259 }
260 }
261 }
262
Andrei Onea15884392019-03-22 17:28:11 +0000263 @UnsupportedAppUsage
Mihai Nita1808ff72016-01-12 08:53:54 -0800264 public static void fillCache(Context context) {
265 if (sFullyInitialized) {
266 return;
267 }
268
269 Set<String> simCountries = getSimCountries(context);
270
Igor Viarheichyk025402c2017-10-06 12:46:49 -0700271 final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
272 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
Mihai Nita1808ff72016-01-12 08:53:54 -0800273 for (String localeId : LocalePicker.getSupportedLocales(context)) {
274 if (localeId.isEmpty()) {
275 throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
276 }
277 LocaleInfo li = new LocaleInfo(localeId);
Igor Viarheichyk025402c2017-10-06 12:46:49 -0700278
279 if (LocaleList.isPseudoLocale(li.getLocale())) {
280 if (isInDeveloperMode) {
281 li.setTranslated(true);
282 li.mIsPseudo = true;
283 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
284 } else {
285 // Do not display pseudolocales unless in development mode.
286 continue;
287 }
288 }
289
Mihai Nita1808ff72016-01-12 08:53:54 -0800290 if (simCountries.contains(li.getLocale().getCountry())) {
291 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
292 }
293 sLocaleCache.put(li.getId(), li);
294 final Locale parent = li.getParent();
295 if (parent != null) {
296 String parentId = parent.toLanguageTag();
297 if (!sLocaleCache.containsKey(parentId)) {
298 sLocaleCache.put(parentId, new LocaleInfo(parent));
299 }
300 }
301 }
302
Mihai Nita1808ff72016-01-12 08:53:54 -0800303 // TODO: See if we can reuse what LocaleList.matchScore does
304 final HashSet<String> localizedLocales = new HashSet<>();
305 for (String localeId : LocalePicker.getSystemAssetLocales()) {
306 LocaleInfo li = new LocaleInfo(localeId);
Mihai Nita86235d42016-04-01 11:50:59 -0700307 final String country = li.getLocale().getCountry();
308 // All this is to figure out if we should suggest a country
309 if (!country.isEmpty()) {
310 LocaleInfo cachedLocale = null;
311 if (sLocaleCache.containsKey(li.getId())) { // the simple case, e.g. fr-CH
312 cachedLocale = sLocaleCache.get(li.getId());
313 } else { // e.g. zh-TW localized, zh-Hant-TW in cache
314 final String langScriptCtry = li.getLangScriptKey() + "-" + country;
315 if (sLocaleCache.containsKey(langScriptCtry)) {
316 cachedLocale = sLocaleCache.get(langScriptCtry);
317 }
318 }
319 if (cachedLocale != null) {
320 cachedLocale.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CFG;
321 }
322 }
Mihai Nita1808ff72016-01-12 08:53:54 -0800323 localizedLocales.add(li.getLangScriptKey());
324 }
325
326 for (LocaleInfo li : sLocaleCache.values()) {
327 li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
328 }
329
Mihai Nitac67b64f2016-02-05 14:27:55 -0800330 addSuggestedLocalesForRegion(Locale.getDefault());
331
Mihai Nita1808ff72016-01-12 08:53:54 -0800332 sFullyInitialized = true;
333 }
334
335 private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
336 if (ignorables.contains(li.getId())) return 0;
337 if (li.mIsPseudo) return 2;
338 if (translatedOnly && !li.isTranslated()) return 0;
339 if (li.getParent() != null) return 2;
340 return 0;
341 }
342
343 /**
344 * Returns a list of locales for language or region selection.
345 * If the parent is null, then it is the language list.
Mihai Nita137b96e2016-01-25 11:31:15 -0800346 * 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 -0800347 * Example: if the parent is "ar", then the region list will contain all Arabic locales.
348 * (this is not language based, but language-script, so that it works for zh-Hant and so on.
349 */
Andrei Onea15884392019-03-22 17:28:11 +0000350 @UnsupportedAppUsage
Mihai Nita137b96e2016-01-25 11:31:15 -0800351 public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
Mihai Nita1808ff72016-01-12 08:53:54 -0800352 LocaleInfo parent, boolean translatedOnly) {
353 fillCache(context);
354 String parentId = parent == null ? null : parent.getId();
355
356 HashSet<LocaleInfo> result = new HashSet<>();
357 for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
358 int level = getLevel(ignorables, li, translatedOnly);
359 if (level == 2) {
360 if (parent != null) { // region selection
361 if (parentId.equals(li.getParent().toLanguageTag())) {
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800362 result.add(li);
Mihai Nita1808ff72016-01-12 08:53:54 -0800363 }
364 } else { // language selection
365 if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
366 result.add(li);
367 } else {
368 result.add(getLocaleInfo(li.getParent()));
369 }
370 }
371 }
372 }
373 return result;
374 }
375
Andrei Onea15884392019-03-22 17:28:11 +0000376 @UnsupportedAppUsage
Mihai Nita1808ff72016-01-12 08:53:54 -0800377 public static LocaleInfo getLocaleInfo(Locale locale) {
378 String id = locale.toLanguageTag();
379 LocaleInfo result;
380 if (!sLocaleCache.containsKey(id)) {
381 result = new LocaleInfo(locale);
382 sLocaleCache.put(id, result);
383 } else {
384 result = sLocaleCache.get(id);
385 }
386 return result;
387 }
388}