LocaleStore.java revision 137b96e3a41edc19a53f7218ef3e53adbfcfd340
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            return (mSuggestionFlags & suggestionMask) == suggestionMask;
108        }
109
110        public String getFullNameNative() {
111            if (mFullNameNative == null) {
112                mFullNameNative =
113                        LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */);
114            }
115            return mFullNameNative;
116        }
117
118        String getFullCountryNameNative() {
119            if (mFullCountryNameNative == null) {
120                mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale);
121            }
122            return mFullCountryNameNative;
123        }
124
125        /** Returns the name of the locale in the language of the UI.
126         * It is used for search, but never shown.
127         * For instance German will show as "Deutsch" in the list, but we will also search for
128         * "allemand" if the system UI is in French.
129         */
130        public String getFullNameInUiLanguage() {
131            return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
132        }
133
134        private String getLangScriptKey() {
135            if (mLangScriptKey == null) {
136                Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(mLocale));
137                mLangScriptKey =
138                        (parentWithScript == null)
139                        ? mLocale.toLanguageTag()
140                        : parentWithScript.toLanguageTag();
141            }
142            return mLangScriptKey;
143        }
144
145        String getLabel() {
146            if (getParent() == null || this.isSuggestionOfType(SUGGESTION_TYPE_SIM)) {
147                return getFullNameNative();
148            } else {
149                return getFullCountryNameNative();
150            }
151        }
152
153        public boolean getChecked() {
154            return mIsChecked;
155        }
156
157        public void setChecked(boolean checked) {
158            mIsChecked = checked;
159        }
160    }
161
162    private static Set<String> getSimCountries(Context context) {
163        Set<String> result = new HashSet<>();
164
165        TelephonyManager tm = TelephonyManager.from(context);
166
167        if (tm != null) {
168            String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
169            if (!iso.isEmpty()) {
170                result.add(iso);
171            }
172
173            iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
174            if (!iso.isEmpty()) {
175                result.add(iso);
176            }
177        }
178
179        return result;
180    }
181
182    /*
183     * This method is added for SetupWizard, to force an update of the suggested locales
184     * when the SIM is initialized.
185     *
186     * <p>When the device is freshly started, it sometimes gets to the language selection
187     * before the SIM is properly initialized.
188     * So at the time the cache is filled, the info from the SIM might not be available.
189     * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events.
190     * SetupWizard will call this function when that happens.</p>
191     *
192     * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code.
193     * The user might change the SIM or might cross border and connect to a network
194     * in a different country, without restarting the Settings application or the phone.</p>
195     */
196    public static void updateSimCountries(Context context) {
197        Set<String> simCountries = getSimCountries(context);
198
199        for (LocaleInfo li : sLocaleCache.values()) {
200            // This method sets the suggestion flags for the (new) SIM locales, but it does not
201            // try to clean up the old flags. After all, if the user replaces a German SIM
202            // with a French one, it is still possible that they are speaking German.
203            // So both French and German are reasonable suggestions.
204            if (simCountries.contains(li.getLocale().getCountry())) {
205                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
206            }
207        }
208    }
209
210    public static void fillCache(Context context) {
211        if (sFullyInitialized) {
212            return;
213        }
214
215        Set<String> simCountries = getSimCountries(context);
216
217        for (String localeId : LocalePicker.getSupportedLocales(context)) {
218            if (localeId.isEmpty()) {
219                throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
220            }
221            LocaleInfo li = new LocaleInfo(localeId);
222            if (simCountries.contains(li.getLocale().getCountry())) {
223                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
224            }
225            sLocaleCache.put(li.getId(), li);
226            final Locale parent = li.getParent();
227            if (parent != null) {
228                String parentId = parent.toLanguageTag();
229                if (!sLocaleCache.containsKey(parentId)) {
230                    sLocaleCache.put(parentId, new LocaleInfo(parent));
231                }
232            }
233        }
234
235        boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
236                Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
237        for (String localeId : LocalePicker.getPseudoLocales()) {
238            LocaleInfo li = getLocaleInfo(Locale.forLanguageTag(localeId));
239            if (isInDeveloperMode) {
240                li.setTranslated(true);
241                li.mIsPseudo = true;
242                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
243            } else {
244                sLocaleCache.remove(li.getId());
245            }
246        }
247
248        // TODO: See if we can reuse what LocaleList.matchScore does
249        final HashSet<String> localizedLocales = new HashSet<>();
250        for (String localeId : LocalePicker.getSystemAssetLocales()) {
251            LocaleInfo li = new LocaleInfo(localeId);
252            localizedLocales.add(li.getLangScriptKey());
253        }
254
255        for (LocaleInfo li : sLocaleCache.values()) {
256            li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
257        }
258
259        sFullyInitialized = true;
260    }
261
262    private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
263        if (ignorables.contains(li.getId())) return 0;
264        if (li.mIsPseudo) return 2;
265        if (translatedOnly && !li.isTranslated()) return 0;
266        if (li.getParent() != null) return 2;
267        return 0;
268    }
269
270    /**
271     * Returns a list of locales for language or region selection.
272     * If the parent is null, then it is the language list.
273     * If it is not null, then the list will contain all the locales that belong to that parent.
274     * Example: if the parent is "ar", then the region list will contain all Arabic locales.
275     * (this is not language based, but language-script, so that it works for zh-Hant and so on.
276     */
277    public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
278            LocaleInfo parent, boolean translatedOnly) {
279        fillCache(context);
280        String parentId = parent == null ? null : parent.getId();
281
282        HashSet<LocaleInfo> result = new HashSet<>();
283        for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
284            int level = getLevel(ignorables, li, translatedOnly);
285            if (level == 2) {
286                if (parent != null) { // region selection
287                    if (parentId.equals(li.getParent().toLanguageTag())) {
288                        if (!li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
289                            result.add(li);
290                        }
291                    }
292                } else { // language selection
293                    if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
294                        result.add(li);
295                    } else {
296                        result.add(getLocaleInfo(li.getParent()));
297                    }
298                }
299            }
300        }
301        return result;
302    }
303
304    public static LocaleInfo getLocaleInfo(Locale locale) {
305        String id = locale.toLanguageTag();
306        LocaleInfo result;
307        if (!sLocaleCache.containsKey(id)) {
308            result = new LocaleInfo(locale);
309            sLocaleCache.put(id, result);
310        } else {
311            result = sLocaleCache.get(id);
312        }
313        return result;
314    }
315}
316