LocaleStore.java revision c67b64fda4324d366da18014791406f32f48d025
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 = 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) {
107            if (!mIsTranslated) { // Never suggest an untranslated locale
108                return false;
109            }
110            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
148        String getLabel() {
149            if (getParent() == null || this.isSuggestionOfType(SUGGESTION_TYPE_SIM)) {
150                return getFullNameNative();
151            } else {
152                return getFullCountryNameNative();
153            }
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
185    /*
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
213    /*
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
234    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
283        addSuggestedLocalesForRegion(Locale.getDefault());
284
285        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.
299     * If it is not null, then the list will contain all the locales that belong to that parent.
300     * 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     */
303    public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
304            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())) {
314                        if (!li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
315                            result.add(li);
316                        }
317                    }
318                } else { // language selection
319                    if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
320                        result.add(li);
321                    } else {
322                        result.add(getLocaleInfo(li.getParent()));
323                    }
324                }
325            }
326        }
327        return result;
328    }
329
330    public static LocaleInfo getLocaleInfo(Locale locale) {
331        String id = locale.toLanguageTag();
332        LocaleInfo result;
333        if (!sLocaleCache.containsKey(id)) {
334            result = new LocaleInfo(locale);
335            sLocaleCache.put(id, result);
336        } else {
337            result = sLocaleCache.get(id);
338        }
339        return result;
340    }
341}
342