• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.UnsupportedAppUsage;
20 import android.content.Context;
21 import android.os.LocaleList;
22 import android.provider.Settings;
23 import android.telephony.TelephonyManager;
24 
25 import java.io.Serializable;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.IllformedLocaleException;
29 import java.util.Locale;
30 import java.util.Set;
31 
32 public class LocaleStore {
33     private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
34     private static boolean sFullyInitialized = false;
35 
36     public static class LocaleInfo implements Serializable {
37         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;
40 
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 
LocaleInfo(Locale locale)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 
LocaleInfo(String localeId)65         private LocaleInfo(String localeId) {
66             this(Locale.forLanguageTag(localeId));
67         }
68 
getParent(Locale locale)69         private static Locale getParent(Locale locale) {
70             if (locale.getCountry().isEmpty()) {
71                 return null;
72             }
73             return new Locale.Builder()
74                     .setLocale(locale)
75                     .setRegion("")
76                     .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
77                     .build();
78         }
79 
80         @Override
toString()81         public String toString() {
82             return mId;
83         }
84 
85         @UnsupportedAppUsage
getLocale()86         public Locale getLocale() {
87             return mLocale;
88         }
89 
90         @UnsupportedAppUsage
getParent()91         public Locale getParent() {
92             return mParent;
93         }
94 
95         @UnsupportedAppUsage
getId()96         public String getId() {
97             return mId;
98         }
99 
isTranslated()100         public boolean isTranslated() {
101             return mIsTranslated;
102         }
103 
setTranslated(boolean isTranslated)104         public void setTranslated(boolean isTranslated) {
105             mIsTranslated = isTranslated;
106         }
107 
isSuggested()108         /* package */ boolean isSuggested() {
109             if (!mIsTranslated) { // Never suggest an untranslated locale
110                 return false;
111             }
112             return mSuggestionFlags != SUGGESTION_TYPE_NONE;
113         }
114 
isSuggestionOfType(int suggestionMask)115         private boolean isSuggestionOfType(int suggestionMask) {
116             if (!mIsTranslated) { // Never suggest an untranslated locale
117                 return false;
118             }
119             return (mSuggestionFlags & suggestionMask) == suggestionMask;
120         }
121 
122         @UnsupportedAppUsage
getFullNameNative()123         public String getFullNameNative() {
124             if (mFullNameNative == null) {
125                 mFullNameNative =
126                         LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */);
127             }
128             return mFullNameNative;
129         }
130 
getFullCountryNameNative()131         String getFullCountryNameNative() {
132             if (mFullCountryNameNative == null) {
133                 mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale);
134             }
135             return mFullCountryNameNative;
136         }
137 
getFullCountryNameInUiLanguage()138         String getFullCountryNameInUiLanguage() {
139             // We don't cache the UI name because the default locale keeps changing
140             return LocaleHelper.getDisplayCountry(mLocale);
141         }
142 
143         /** 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          */
148         @UnsupportedAppUsage
getFullNameInUiLanguage()149         public String getFullNameInUiLanguage() {
150             // We don't cache the UI name because the default locale keeps changing
151             return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
152         }
153 
getLangScriptKey()154         private String getLangScriptKey() {
155             if (mLangScriptKey == null) {
156                 Locale baseLocale = new Locale.Builder()
157                     .setLocale(mLocale)
158                     .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
159                     .build();
160                 Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(baseLocale));
161                 mLangScriptKey =
162                         (parentWithScript == null)
163                         ? mLocale.toLanguageTag()
164                         : parentWithScript.toLanguageTag();
165             }
166             return mLangScriptKey;
167         }
168 
getLabel(boolean countryMode)169         String getLabel(boolean countryMode) {
170             if (countryMode) {
171                 return getFullCountryNameNative();
172             } else {
173                 return getFullNameNative();
174             }
175         }
176 
getContentDescription(boolean countryMode)177         String getContentDescription(boolean countryMode) {
178             if (countryMode) {
179                 return getFullCountryNameInUiLanguage();
180             } else {
181                 return getFullNameInUiLanguage();
182             }
183         }
184 
getChecked()185         public boolean getChecked() {
186             return mIsChecked;
187         }
188 
setChecked(boolean checked)189         public void setChecked(boolean checked) {
190             mIsChecked = checked;
191         }
192     }
193 
getSimCountries(Context context)194     private static Set<String> getSimCountries(Context context) {
195         Set<String> result = new HashSet<>();
196 
197         TelephonyManager tm = TelephonyManager.from(context);
198 
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 
214     /*
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      */
updateSimCountries(Context context)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 
242     /*
243      * Show all the languages supported for a country in the suggested list.
244      * This is also handy for devices without SIM (tablets).
245      */
addSuggestedLocalesForRegion(Locale locale)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 
263     @UnsupportedAppUsage
fillCache(Context context)264     public static void fillCache(Context context) {
265         if (sFullyInitialized) {
266             return;
267         }
268 
269         Set<String> simCountries = getSimCountries(context);
270 
271         final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
272                 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
273         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);
278 
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 
290             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 
303         // 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);
307             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             }
323             localizedLocales.add(li.getLangScriptKey());
324         }
325 
326         for (LocaleInfo li : sLocaleCache.values()) {
327             li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
328         }
329 
330         addSuggestedLocalesForRegion(Locale.getDefault());
331 
332         sFullyInitialized = true;
333     }
334 
getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly)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.
346      * If it is not null, then the list will contain all the locales that belong to that parent.
347      * 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      */
350     @UnsupportedAppUsage
getLevelLocales(Context context, Set<String> ignorables, LocaleInfo parent, boolean translatedOnly)351     public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
352             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())) {
362                         result.add(li);
363                     }
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 
376     @UnsupportedAppUsage
getLocaleInfo(Locale locale)377     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 }
389