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        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         */
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
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
155        String getLabel(boolean countryMode) {
156            if (countryMode) {
157                return getFullCountryNameNative();
158            } else {
159                return getFullNameNative();
160            }
161        }
162
163        String getContentDescription(boolean countryMode) {
164            if (countryMode) {
165                return getFullCountryNameInUiLanguage();
166            } else {
167                return getFullNameInUiLanguage();
168            }
169        }
170
171        public boolean getChecked() {
172            return mIsChecked;
173        }
174
175        public void setChecked(boolean checked) {
176            mIsChecked = checked;
177        }
178    }
179
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     */
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     */
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
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
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     */
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
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