blob: c4e6675c2c92263e11cc68d9c118e081667626df [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 {
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) {
Mihai Nitac67b64f2016-02-05 14:27:55 -0800107 if (!mIsTranslated) { // Never suggest an untranslated locale
108 return false;
109 }
Mihai Nita1808ff72016-01-12 08:53:54 -0800110 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
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800148 String getLabel(boolean countryMode) {
149 if (countryMode) {
Mihai Nita1808ff72016-01-12 08:53:54 -0800150 return getFullCountryNameNative();
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800151 } else {
152 return getFullNameNative();
Mihai Nita1808ff72016-01-12 08:53:54 -0800153 }
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
Mihai Nita137b96e2016-01-25 11:31:15 -0800185 /*
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
Mihai Nitac67b64f2016-02-05 14:27:55 -0800213 /*
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
Mihai Nita1808ff72016-01-12 08:53:54 -0800234 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
Mihai Nitac67b64f2016-02-05 14:27:55 -0800283 addSuggestedLocalesForRegion(Locale.getDefault());
284
Mihai Nita1808ff72016-01-12 08:53:54 -0800285 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.
Mihai Nita137b96e2016-01-25 11:31:15 -0800299 * 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 -0800300 * 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 */
Mihai Nita137b96e2016-01-25 11:31:15 -0800303 public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
Mihai Nita1808ff72016-01-12 08:53:54 -0800304 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())) {
Mihai Nitaf1f39cf2016-02-29 14:42:12 -0800314 result.add(li);
Mihai Nita1808ff72016-01-12 08:53:54 -0800315 }
316 } else { // language selection
317 if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
318 result.add(li);
319 } else {
320 result.add(getLocaleInfo(li.getParent()));
321 }
322 }
323 }
324 }
325 return result;
326 }
327
328 public static LocaleInfo getLocaleInfo(Locale locale) {
329 String id = locale.toLanguageTag();
330 LocaleInfo result;
331 if (!sLocaleCache.containsKey(id)) {
332 result = new LocaleInfo(locale);
333 sLocaleCache.put(id, result);
334 } else {
335 result = sLocaleCache.get(id);
336 }
337 return result;
338 }
339}