LocaleStore.java revision 86235d497007ad17ddec7e659fb0e0c36b010745
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
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 = 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
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) {
108            if (!mIsTranslated) { // Never suggest an untranslated locale
109                return false;
110            }
111            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
149        String getLabel(boolean countryMode) {
150            if (countryMode) {
151                return getFullCountryNameNative();
152            } else {
153                return getFullNameNative();
154            }
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
186    /*
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
214    /*
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
235    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);
277            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            }
293            localizedLocales.add(li.getLangScriptKey());
294        }
295
296        for (LocaleInfo li : sLocaleCache.values()) {
297            li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
298        }
299
300        addSuggestedLocalesForRegion(Locale.getDefault());
301
302        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.
316     * If it is not null, then the list will contain all the locales that belong to that parent.
317     * 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     */
320    public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
321            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())) {
331                        result.add(li);
332                    }
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}
357