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