summaryrefslogtreecommitdiff
path: root/src/com/android/localepicker/LocaleStore.java
blob: c799bd3fb83a8e954920c97043b75e71835dc65d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.localepicker;

import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Resources;
import android.icu.util.ULocale;
import android.os.LocaleList;
import android.provider.Settings;
import android.telephony.TelephonyManager;

import androidx.annotation.VisibleForTesting;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

public class LocaleStore {
    private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
    private static boolean sFullyInitialized = false;

    public static class LocaleInfo implements Serializable {
        @VisibleForTesting static final int SUGGESTION_TYPE_NONE = 0;
        @VisibleForTesting static final int SUGGESTION_TYPE_SIM = 1 << 0;
        @VisibleForTesting static final int SUGGESTION_TYPE_CFG = 1 << 1;

        private final Locale mLocale;
        private final Locale mParent;
        private final String mId;
        private boolean mIsTranslated;
        private boolean mIsPseudo;
        private boolean mIsChecked; // Used by the LocaleListEditor to mark entries for deletion
        // Combination of flags for various reasons to show a locale as a suggestion.
        // Can be SIM, location, etc.
        @VisibleForTesting int mSuggestionFlags;

        private String mFullNameNative;
        private String mFullCountryNameNative;
        private String mLangScriptKey;

        private LocaleInfo(Locale locale) {
            this.mLocale = locale;
            this.mId = locale.toLanguageTag();
            this.mParent = getParent(locale);
            this.mIsChecked = false;
            this.mSuggestionFlags = SUGGESTION_TYPE_NONE;
            this.mIsTranslated = false;
            this.mIsPseudo = false;
        }

        private LocaleInfo(String localeId) {
            this(Locale.forLanguageTag(localeId));
        }

        private static Locale getParent(Locale locale) {
            if (locale.getCountry().isEmpty()) {
                return null;
            }
            return new Locale.Builder()
                    .setLocale(locale)
                    .setRegion("")
                    .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
                    .build();
        }

        @Override
        public String toString() {
            return mId;
        }

        public Locale getLocale() {
            return mLocale;
        }

        public Locale getParent() {
            return mParent;
        }

        public String getId() {
            return mId;
        }

        public boolean isTranslated() {
            return mIsTranslated;
        }

        public void setTranslated(boolean isTranslated) {
            mIsTranslated = isTranslated;
        }

        /* package */ boolean isSuggested() {
            if (!mIsTranslated) { // Never suggest an untranslated locale
                return false;
            }
            return mSuggestionFlags != SUGGESTION_TYPE_NONE;
        }

        private boolean isSuggestionOfType(int suggestionMask) {
            if (!mIsTranslated) { // Never suggest an untranslated locale
                return false;
            }
            return (mSuggestionFlags & suggestionMask) == suggestionMask;
        }

        public String getFullNameNative() {
            if (mFullNameNative == null) {
                mFullNameNative =
                        LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */);
            }
            return mFullNameNative;
        }

        String getFullCountryNameNative() {
            if (mFullCountryNameNative == null) {
                mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale);
            }
            return mFullCountryNameNative;
        }

        String getFullCountryNameInUiLanguage() {
            // We don't cache the UI name because the default locale keeps changing
            return LocaleHelper.getDisplayCountry(mLocale);
        }

        /** Returns the name of the locale in the language of the UI.
         * It is used for search, but never shown.
         * For instance German will show as "Deutsch" in the list, but we will also search for
         * "allemand" if the system UI is in French.
         */
        public String getFullNameInUiLanguage() {
            // We don't cache the UI name because the default locale keeps changing
            return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
        }

        private String getLangScriptKey() {
            if (mLangScriptKey == null) {
                Locale baseLocale = new Locale.Builder()
                    .setLocale(mLocale)
                    .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
                    .build();
                Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(baseLocale));
                mLangScriptKey =
                        (parentWithScript == null)
                        ? mLocale.toLanguageTag()
                        : parentWithScript.toLanguageTag();
            }
            return mLangScriptKey;
        }

        String getLabel(boolean countryMode) {
            if (countryMode) {
                return getFullCountryNameNative();
            } else {
                return getFullNameNative();
            }
        }

        String getContentDescription(boolean countryMode) {
            if (countryMode) {
                return getFullCountryNameInUiLanguage();
            } else {
                return getFullNameInUiLanguage();
            }
        }

        public boolean getChecked() {
            return mIsChecked;
        }

        public void setChecked(boolean checked) {
            mIsChecked = checked;
        }
    }

    private static Set<String> getSimCountries(Context context) {
        Set<String> result = new HashSet<>();

        TelephonyManager tm = context.getSystemService(TelephonyManager.class);

        if (tm != null) {
            String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
            if (!iso.isEmpty()) {
                result.add(iso);
            }

            iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
            if (!iso.isEmpty()) {
                result.add(iso);
            }
        }

        return result;
    }

    /*
     * This method is added for SetupWizard, to force an update of the suggested locales
     * when the SIM is initialized.
     *
     * <p>When the device is freshly started, it sometimes gets to the language selection
     * before the SIM is properly initialized.
     * So at the time the cache is filled, the info from the SIM might not be available.
     * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events.
     * SetupWizard will call this function when that happens.</p>
     *
     * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code.
     * The user might change the SIM or might cross border and connect to a network
     * in a different country, without restarting the Settings application or the phone.</p>
     */
    public static void updateSimCountries(Context context) {
        Set<String> simCountries = getSimCountries(context);

        for (LocaleInfo li : sLocaleCache.values()) {
            // This method sets the suggestion flags for the (new) SIM locales, but it does not
            // try to clean up the old flags. After all, if the user replaces a German SIM
            // with a French one, it is still possible that they are speaking German.
            // So both French and German are reasonable suggestions.
            if (simCountries.contains(li.getLocale().getCountry())) {
                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
            }
        }
    }

    /*
     * Show all the languages supported for a country in the suggested list.
     * This is also handy for devices without SIM (tablets).
     */
    private static void addSuggestedLocalesForRegion(Locale locale) {
        if (locale == null) {
            return;
        }
        final String country = locale.getCountry();
        if (country.isEmpty()) {
            return;
        }

        for (LocaleInfo li : sLocaleCache.values()) {
            if (country.equals(li.getLocale().getCountry())) {
                // We don't need to differentiate between manual and SIM suggestions
                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
            }
        }
    }

    public static void fillCache(Context context) {
        if (sFullyInitialized) {
            return;
        }

        Set<String> simCountries = getSimCountries(context);

        final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
                Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
        ActivityManager activityManager = context.getSystemService(ActivityManager.class);
        for (Locale locale : activityManager.getSupportedLocales()) {
            if (locale == null) {
                throw new NullPointerException("Bad locale entry in locale_config.xml");
            }
            LocaleInfo li = new LocaleInfo(locale);

            if (LocaleList.isPseudoLocale(ULocale.forLocale(li.getLocale()))) {
                if (isInDeveloperMode) {
                    li.setTranslated(true);
                    li.mIsPseudo = true;
                    li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
                } else {
                    // Do not display pseudolocales unless in development mode.
                    continue;
                }
            }

            if (simCountries.contains(li.getLocale().getCountry())) {
                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
            }
            sLocaleCache.put(li.getId(), li);
            final Locale parent = li.getParent();
            if (parent != null) {
                String parentId = parent.toLanguageTag();
                if (!sLocaleCache.containsKey(parentId)) {
                    sLocaleCache.put(parentId, new LocaleInfo(parent));
                }
            }
        }

        // TODO: See if we can reuse what LocaleList.matchScore does
        final HashSet<String> localizedLocales = new HashSet<>();
        for (String localeId : Resources.getSystem().getAssets().getLocales()) {
            LocaleInfo li = new LocaleInfo(localeId);
            final String country = li.getLocale().getCountry();
            // All this is to figure out if we should suggest a country
            if (!country.isEmpty()) {
                LocaleInfo cachedLocale = null;
                if (sLocaleCache.containsKey(li.getId())) { // the simple case, e.g. fr-CH
                    cachedLocale = sLocaleCache.get(li.getId());
                } else { // e.g. zh-TW localized, zh-Hant-TW in cache
                    final String langScriptCtry = li.getLangScriptKey() + "-" + country;
                    if (sLocaleCache.containsKey(langScriptCtry)) {
                        cachedLocale = sLocaleCache.get(langScriptCtry);
                    }
                }
                if (cachedLocale != null) {
                    cachedLocale.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CFG;
                }
            }
            localizedLocales.add(li.getLangScriptKey());
        }

        for (LocaleInfo li : sLocaleCache.values()) {
            li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
        }

        addSuggestedLocalesForRegion(Locale.getDefault());

        sFullyInitialized = true;
    }

    private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
        if (ignorables.contains(li.getId())) return 0;
        if (li.mIsPseudo) return 2;
        if (translatedOnly && !li.isTranslated()) return 0;
        if (li.getParent() != null) return 2;
        return 0;
    }

    /**
     * Returns a list of locales for language or region selection.
     * If the parent is null, then it is the language list.
     * If it is not null, then the list will contain all the locales that belong to that parent.
     * Example: if the parent is "ar", then the region list will contain all Arabic locales.
     * (this is not language based, but language-script, so that it works for zh-Hant and so on.
     */
    public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
            LocaleInfo parent, boolean translatedOnly) {
        fillCache(context);
        String parentId = parent == null ? null : parent.getId();

        HashSet<LocaleInfo> result = new HashSet<>();
        ArrayList<LocaleStore.LocaleInfo> localeInfos = new ArrayList<>(sLocaleCache.values());
        for (LocaleStore.LocaleInfo li : localeInfos) {
            int level = getLevel(ignorables, li, translatedOnly);
            if (level == 2) {
                if (parent != null) { // region selection
                    if (parentId.equals(li.getParent().toLanguageTag())) {
                        result.add(li);
                    }
                } else { // language selection
                    if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
                        result.add(li);
                    } else {
                        result.add(getLocaleInfo(li.getParent()));
                    }
                }
            }
        }
        return result;
    }

    public static LocaleInfo getLocaleInfo(Locale locale) {
        String id = locale.toLanguageTag();
        LocaleInfo result;
        if (!sLocaleCache.containsKey(id)) {
            result = new LocaleInfo(locale);
            sLocaleCache.put(id, result);
        } else {
            result = sLocaleCache.get(id);
        }
        return result;
    }
}