blob: 7803e52c45e1ad4eb3d00c6a31495d75a2714d1a [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;
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 {
Mihai Nita86235d42016-04-01 11:50:59 -070034 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;
Mihai Nita1808ff72016-01-12 08:53:54 -080037
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) {
Mihai Nitac67b64f2016-02-05 14:27:55 -0800108 if (!mIsTranslated) { // Never suggest an untranslated locale
109 return false;
110 }
Mihai Nita1808ff72016-01-12 08:53:54 -0800111 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 /** Returns the name of the locale in the language of the UI.
130 * It is used for search, but never shown.
131 * For instance German will show as "Deutsch" in the list, but we will also search for
132 * "allemand" if the system UI is in French.
133 */
134 public String getFullNameInUiLanguage() {
135 return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
136 }
137
138 private String getLangScriptKey() {
139 if (mLangScriptKey == null) {
140 Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(mLocale));
141 mLangScriptKey =
142 (parentWithScript == null)
143 ? mLocale.toLanguageTag()
144 : parentWithScript.toLanguageTag();
145 }
146 return mLangScriptKey;
147 }
148
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800149 String getLabel(boolean countryMode) {
150 if (countryMode) {
Mihai Nita1808ff72016-01-12 08:53:54 -0800151 return getFullCountryNameNative();
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800152 } else {
153 return getFullNameNative();
Mihai Nita1808ff72016-01-12 08:53:54 -0800154 }
155 }
156
157 public boolean getChecked() {
158 return mIsChecked;
159 }
160
161 public void setChecked(boolean checked) {
162 mIsChecked = checked;
163 }
164 }
165
166 private static Set<String> getSimCountries(Context context) {
167 Set<String> result = new HashSet<>();
168
169 TelephonyManager tm = TelephonyManager.from(context);
170
171 if (tm != null) {
172 String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
173 if (!iso.isEmpty()) {
174 result.add(iso);
175 }
176
177 iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
178 if (!iso.isEmpty()) {
179 result.add(iso);
180 }
181 }
182
183 return result;
184 }
185
Mihai Nita137b96e2016-01-25 11:31:15 -0800186 /*
187 * This method is added for SetupWizard, to force an update of the suggested locales
188 * when the SIM is initialized.
189 *
190 * <p>When the device is freshly started, it sometimes gets to the language selection
191 * before the SIM is properly initialized.
192 * So at the time the cache is filled, the info from the SIM might not be available.
193 * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events.
194 * SetupWizard will call this function when that happens.</p>
195 *
196 * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code.
197 * The user might change the SIM or might cross border and connect to a network
198 * in a different country, without restarting the Settings application or the phone.</p>
199 */
200 public static void updateSimCountries(Context context) {
201 Set<String> simCountries = getSimCountries(context);
202
203 for (LocaleInfo li : sLocaleCache.values()) {
204 // This method sets the suggestion flags for the (new) SIM locales, but it does not
205 // try to clean up the old flags. After all, if the user replaces a German SIM
206 // with a French one, it is still possible that they are speaking German.
207 // So both French and German are reasonable suggestions.
208 if (simCountries.contains(li.getLocale().getCountry())) {
209 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
210 }
211 }
212 }
213
Mihai Nitac67b64f2016-02-05 14:27:55 -0800214 /*
215 * Show all the languages supported for a country in the suggested list.
216 * This is also handy for devices without SIM (tablets).
217 */
218 private static void addSuggestedLocalesForRegion(Locale locale) {
219 if (locale == null) {
220 return;
221 }
222 final String country = locale.getCountry();
223 if (country.isEmpty()) {
224 return;
225 }
226
227 for (LocaleInfo li : sLocaleCache.values()) {
228 if (country.equals(li.getLocale().getCountry())) {
229 // We don't need to differentiate between manual and SIM suggestions
230 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
231 }
232 }
233 }
234
Mihai Nita1808ff72016-01-12 08:53:54 -0800235 public static void fillCache(Context context) {
236 if (sFullyInitialized) {
237 return;
238 }
239
240 Set<String> simCountries = getSimCountries(context);
241
242 for (String localeId : LocalePicker.getSupportedLocales(context)) {
243 if (localeId.isEmpty()) {
244 throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
245 }
246 LocaleInfo li = new LocaleInfo(localeId);
247 if (simCountries.contains(li.getLocale().getCountry())) {
248 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
249 }
250 sLocaleCache.put(li.getId(), li);
251 final Locale parent = li.getParent();
252 if (parent != null) {
253 String parentId = parent.toLanguageTag();
254 if (!sLocaleCache.containsKey(parentId)) {
255 sLocaleCache.put(parentId, new LocaleInfo(parent));
256 }
257 }
258 }
259
260 boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
261 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
262 for (String localeId : LocalePicker.getPseudoLocales()) {
263 LocaleInfo li = getLocaleInfo(Locale.forLanguageTag(localeId));
264 if (isInDeveloperMode) {
265 li.setTranslated(true);
266 li.mIsPseudo = true;
267 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
268 } else {
269 sLocaleCache.remove(li.getId());
270 }
271 }
272
273 // TODO: See if we can reuse what LocaleList.matchScore does
274 final HashSet<String> localizedLocales = new HashSet<>();
275 for (String localeId : LocalePicker.getSystemAssetLocales()) {
276 LocaleInfo li = new LocaleInfo(localeId);
Mihai Nita86235d42016-04-01 11:50:59 -0700277 final String country = li.getLocale().getCountry();
278 // All this is to figure out if we should suggest a country
279 if (!country.isEmpty()) {
280 LocaleInfo cachedLocale = null;
281 if (sLocaleCache.containsKey(li.getId())) { // the simple case, e.g. fr-CH
282 cachedLocale = sLocaleCache.get(li.getId());
283 } else { // e.g. zh-TW localized, zh-Hant-TW in cache
284 final String langScriptCtry = li.getLangScriptKey() + "-" + country;
285 if (sLocaleCache.containsKey(langScriptCtry)) {
286 cachedLocale = sLocaleCache.get(langScriptCtry);
287 }
288 }
289 if (cachedLocale != null) {
290 cachedLocale.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CFG;
291 }
292 }
Mihai Nita1808ff72016-01-12 08:53:54 -0800293 localizedLocales.add(li.getLangScriptKey());
294 }
295
296 for (LocaleInfo li : sLocaleCache.values()) {
297 li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
298 }
299
Mihai Nitac67b64f2016-02-05 14:27:55 -0800300 addSuggestedLocalesForRegion(Locale.getDefault());
301
Mihai Nita1808ff72016-01-12 08:53:54 -0800302 sFullyInitialized = true;
303 }
304
305 private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
306 if (ignorables.contains(li.getId())) return 0;
307 if (li.mIsPseudo) return 2;
308 if (translatedOnly && !li.isTranslated()) return 0;
309 if (li.getParent() != null) return 2;
310 return 0;
311 }
312
313 /**
314 * Returns a list of locales for language or region selection.
315 * If the parent is null, then it is the language list.
Mihai Nita137b96e2016-01-25 11:31:15 -0800316 * 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 -0800317 * Example: if the parent is "ar", then the region list will contain all Arabic locales.
318 * (this is not language based, but language-script, so that it works for zh-Hant and so on.
319 */
Mihai Nita137b96e2016-01-25 11:31:15 -0800320 public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
Mihai Nita1808ff72016-01-12 08:53:54 -0800321 LocaleInfo parent, boolean translatedOnly) {
322 fillCache(context);
323 String parentId = parent == null ? null : parent.getId();
324
325 HashSet<LocaleInfo> result = new HashSet<>();
326 for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
327 int level = getLevel(ignorables, li, translatedOnly);
328 if (level == 2) {
329 if (parent != null) { // region selection
330 if (parentId.equals(li.getParent().toLanguageTag())) {
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800331 result.add(li);
Mihai Nita1808ff72016-01-12 08:53:54 -0800332 }
333 } else { // language selection
334 if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
335 result.add(li);
336 } else {
337 result.add(getLocaleInfo(li.getParent()));
338 }
339 }
340 }
341 }
342 return result;
343 }
344
345 public static LocaleInfo getLocaleInfo(Locale locale) {
346 String id = locale.toLanguageTag();
347 LocaleInfo result;
348 if (!sLocaleCache.containsKey(id)) {
349 result = new LocaleInfo(locale);
350 sLocaleCache.put(id, result);
351 } else {
352 result = sLocaleCache.get(id);
353 }
354 return result;
355 }
356}