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 
17 package com.android.internal.app;
18 
19 import android.content.Context;
20 import android.os.LocaleList;
21 import android.provider.Settings;
22 import android.telephony.TelephonyManager;
23 
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.IllformedLocaleException;
27 import java.util.Locale;
28 import java.util.Set;
29 
30 public class LocaleStore {
31     private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
32     private static boolean sFullyInitialized = false;
33 
34     public static class LocaleInfo {
35         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;
38 
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 
LocaleInfo(Locale locale)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 
LocaleInfo(String localeId)63         private LocaleInfo(String localeId) {
64             this(Locale.forLanguageTag(localeId));
65         }
66 
getParent(Locale locale)67         private static Locale getParent(Locale locale) {
68             if (locale.getCountry().isEmpty()) {
69                 return null;
70             }
71             return new Locale.Builder()
72                     .setLocale(locale)
73                     .setRegion("")
74                     .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
75                     .build();
76         }
77 
78         @Override
toString()79         public String toString() {
80             return mId;
81         }
82 
getLocale()83         public Locale getLocale() {
84             return mLocale;
85         }
86 
getParent()87         public Locale getParent() {
88             return mParent;
89         }
90 
getId()91         public String getId() {
92             return mId;
93         }
94 
isTranslated()95         public boolean isTranslated() {
96             return mIsTranslated;
97         }
98 
setTranslated(boolean isTranslated)99         public void setTranslated(boolean isTranslated) {
100             mIsTranslated = isTranslated;
101         }
102 
isSuggested()103         /* package */ boolean isSuggested() {
104             if (!mIsTranslated) { // Never suggest an untranslated locale
105                 return false;
106             }
107             return mSuggestionFlags != SUGGESTION_TYPE_NONE;
108         }
109 
isSuggestionOfType(int suggestionMask)110         private boolean isSuggestionOfType(int suggestionMask) {
111             if (!mIsTranslated) { // Never suggest an untranslated locale
112                 return false;
113             }
114             return (mSuggestionFlags & suggestionMask) == suggestionMask;
115         }
116 
getFullNameNative()117         public String getFullNameNative() {
118             if (mFullNameNative == null) {
119                 mFullNameNative =
120                         LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */);
121             }
122             return mFullNameNative;
123         }
124 
getFullCountryNameNative()125         String getFullCountryNameNative() {
126             if (mFullCountryNameNative == null) {
127                 mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale);
128             }
129             return mFullCountryNameNative;
130         }
131 
getFullCountryNameInUiLanguage()132         String getFullCountryNameInUiLanguage() {
133             // We don't cache the UI name because the default locale keeps changing
134             return LocaleHelper.getDisplayCountry(mLocale);
135         }
136 
137         /** 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          */
getFullNameInUiLanguage()142         public String getFullNameInUiLanguage() {
143             // We don't cache the UI name because the default locale keeps changing
144             return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
145         }
146 
getLangScriptKey()147         private String getLangScriptKey() {
148             if (mLangScriptKey == null) {
149                 Locale baseLocale = new Locale.Builder()
150                     .setLocale(mLocale)
151                     .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
152                     .build();
153                 Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(baseLocale));
154                 mLangScriptKey =
155                         (parentWithScript == null)
156                         ? mLocale.toLanguageTag()
157                         : parentWithScript.toLanguageTag();
158             }
159             return mLangScriptKey;
160         }
161 
getLabel(boolean countryMode)162         String getLabel(boolean countryMode) {
163             if (countryMode) {
164                 return getFullCountryNameNative();
165             } else {
166                 return getFullNameNative();
167             }
168         }
169 
getContentDescription(boolean countryMode)170         String getContentDescription(boolean countryMode) {
171             if (countryMode) {
172                 return getFullCountryNameInUiLanguage();
173             } else {
174                 return getFullNameInUiLanguage();
175             }
176         }
177 
getChecked()178         public boolean getChecked() {
179             return mIsChecked;
180         }
181 
setChecked(boolean checked)182         public void setChecked(boolean checked) {
183             mIsChecked = checked;
184         }
185     }
186 
getSimCountries(Context context)187     private static Set<String> getSimCountries(Context context) {
188         Set<String> result = new HashSet<>();
189 
190         TelephonyManager tm = TelephonyManager.from(context);
191 
192         if (tm != null) {
193             String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
194             if (!iso.isEmpty()) {
195                 result.add(iso);
196             }
197 
198             iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
199             if (!iso.isEmpty()) {
200                 result.add(iso);
201             }
202         }
203 
204         return result;
205     }
206 
207     /*
208      * This method is added for SetupWizard, to force an update of the suggested locales
209      * when the SIM is initialized.
210      *
211      * <p>When the device is freshly started, it sometimes gets to the language selection
212      * before the SIM is properly initialized.
213      * So at the time the cache is filled, the info from the SIM might not be available.
214      * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events.
215      * SetupWizard will call this function when that happens.</p>
216      *
217      * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code.
218      * The user might change the SIM or might cross border and connect to a network
219      * in a different country, without restarting the Settings application or the phone.</p>
220      */
updateSimCountries(Context context)221     public static void updateSimCountries(Context context) {
222         Set<String> simCountries = getSimCountries(context);
223 
224         for (LocaleInfo li : sLocaleCache.values()) {
225             // This method sets the suggestion flags for the (new) SIM locales, but it does not
226             // try to clean up the old flags. After all, if the user replaces a German SIM
227             // with a French one, it is still possible that they are speaking German.
228             // So both French and German are reasonable suggestions.
229             if (simCountries.contains(li.getLocale().getCountry())) {
230                 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
231             }
232         }
233     }
234 
235     /*
236      * Show all the languages supported for a country in the suggested list.
237      * This is also handy for devices without SIM (tablets).
238      */
addSuggestedLocalesForRegion(Locale locale)239     private static void addSuggestedLocalesForRegion(Locale locale) {
240         if (locale == null) {
241             return;
242         }
243         final String country = locale.getCountry();
244         if (country.isEmpty()) {
245             return;
246         }
247 
248         for (LocaleInfo li : sLocaleCache.values()) {
249             if (country.equals(li.getLocale().getCountry())) {
250                 // We don't need to differentiate between manual and SIM suggestions
251                 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
252             }
253         }
254     }
255 
fillCache(Context context)256     public static void fillCache(Context context) {
257         if (sFullyInitialized) {
258             return;
259         }
260 
261         Set<String> simCountries = getSimCountries(context);
262 
263         final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
264                 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
265         for (String localeId : LocalePicker.getSupportedLocales(context)) {
266             if (localeId.isEmpty()) {
267                 throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
268             }
269             LocaleInfo li = new LocaleInfo(localeId);
270 
271             if (LocaleList.isPseudoLocale(li.getLocale())) {
272                 if (isInDeveloperMode) {
273                     li.setTranslated(true);
274                     li.mIsPseudo = true;
275                     li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
276                 } else {
277                     // Do not display pseudolocales unless in development mode.
278                     continue;
279                 }
280             }
281 
282             if (simCountries.contains(li.getLocale().getCountry())) {
283                 li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
284             }
285             sLocaleCache.put(li.getId(), li);
286             final Locale parent = li.getParent();
287             if (parent != null) {
288                 String parentId = parent.toLanguageTag();
289                 if (!sLocaleCache.containsKey(parentId)) {
290                     sLocaleCache.put(parentId, new LocaleInfo(parent));
291                 }
292             }
293         }
294 
295         // TODO: See if we can reuse what LocaleList.matchScore does
296         final HashSet<String> localizedLocales = new HashSet<>();
297         for (String localeId : LocalePicker.getSystemAssetLocales()) {
298             LocaleInfo li = new LocaleInfo(localeId);
299             final String country = li.getLocale().getCountry();
300             // All this is to figure out if we should suggest a country
301             if (!country.isEmpty()) {
302                 LocaleInfo cachedLocale = null;
303                 if (sLocaleCache.containsKey(li.getId())) { // the simple case, e.g. fr-CH
304                     cachedLocale = sLocaleCache.get(li.getId());
305                 } else { // e.g. zh-TW localized, zh-Hant-TW in cache
306                     final String langScriptCtry = li.getLangScriptKey() + "-" + country;
307                     if (sLocaleCache.containsKey(langScriptCtry)) {
308                         cachedLocale = sLocaleCache.get(langScriptCtry);
309                     }
310                 }
311                 if (cachedLocale != null) {
312                     cachedLocale.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CFG;
313                 }
314             }
315             localizedLocales.add(li.getLangScriptKey());
316         }
317 
318         for (LocaleInfo li : sLocaleCache.values()) {
319             li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
320         }
321 
322         addSuggestedLocalesForRegion(Locale.getDefault());
323 
324         sFullyInitialized = true;
325     }
326 
getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly)327     private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
328         if (ignorables.contains(li.getId())) return 0;
329         if (li.mIsPseudo) return 2;
330         if (translatedOnly && !li.isTranslated()) return 0;
331         if (li.getParent() != null) return 2;
332         return 0;
333     }
334 
335     /**
336      * Returns a list of locales for language or region selection.
337      * If the parent is null, then it is the language list.
338      * If it is not null, then the list will contain all the locales that belong to that parent.
339      * Example: if the parent is "ar", then the region list will contain all Arabic locales.
340      * (this is not language based, but language-script, so that it works for zh-Hant and so on.
341      */
getLevelLocales(Context context, Set<String> ignorables, LocaleInfo parent, boolean translatedOnly)342     public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
343             LocaleInfo parent, boolean translatedOnly) {
344         fillCache(context);
345         String parentId = parent == null ? null : parent.getId();
346 
347         HashSet<LocaleInfo> result = new HashSet<>();
348         for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
349             int level = getLevel(ignorables, li, translatedOnly);
350             if (level == 2) {
351                 if (parent != null) { // region selection
352                     if (parentId.equals(li.getParent().toLanguageTag())) {
353                         result.add(li);
354                     }
355                 } else { // language selection
356                     if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
357                         result.add(li);
358                     } else {
359                         result.add(getLocaleInfo(li.getParent()));
360                     }
361                 }
362             }
363         }
364         return result;
365     }
366 
getLocaleInfo(Locale locale)367     public static LocaleInfo getLocaleInfo(Locale locale) {
368         String id = locale.toLanguageTag();
369         LocaleInfo result;
370         if (!sLocaleCache.containsKey(id)) {
371             result = new LocaleInfo(locale);
372             sLocaleCache.put(id, result);
373         } else {
374             result = sLocaleCache.get(id);
375         }
376         return result;
377     }
378 }
379