1 /*
2  * Copyright (C) 2019 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 
17 package com.android.internal.telephony.util;
18 
19 import android.content.Context;
20 import android.icu.util.ULocale;
21 import android.text.TextUtils;
22 
23 import com.android.internal.telephony.MccTable;
24 import com.android.telephony.Rlog;
25 
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.List;
29 import java.util.Locale;
30 
31 /**
32  * This class provides various util functions about Locale.
33  */
34 public class LocaleUtils {
35 
36     private static final String LOG_TAG = "LocaleUtils";
37 
38     /**
39      * Get Locale based on the MCC of the SIM.
40      *
41      * @param context Context to act on.
42      * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA)
43      * @param simLanguage (nullable) the language from the SIM records (if present).
44      *
45      * @return locale for the mcc or null if none
46      */
getLocaleFromMcc(Context context, int mcc, String simLanguage)47     public static Locale getLocaleFromMcc(Context context, int mcc, String simLanguage) {
48         boolean hasSimLanguage = !TextUtils.isEmpty(simLanguage);
49         String language = hasSimLanguage ? simLanguage : defaultLanguageForMcc(mcc);
50         String country = MccTable.countryCodeForMcc(mcc);
51 
52         Rlog.d(LOG_TAG, "getLocaleFromMcc(" + language + ", " + country + ", " + mcc);
53         final Locale locale = getLocaleForLanguageCountry(context, language, country);
54 
55         // If we couldn't find a locale that matches the SIM language, give it a go again
56         // with the "likely" language for the given country.
57         if (locale == null && hasSimLanguage) {
58             language = defaultLanguageForMcc(mcc);
59             Rlog.d(LOG_TAG, "[retry ] getLocaleFromMcc(" + language + ", " + country + ", " + mcc);
60             return getLocaleForLanguageCountry(context, language, country);
61         }
62 
63         return locale;
64     }
65 
66     /**
67      * Return Locale for the language and country or null if no good match.
68      *
69      * @param context Context to act on.
70      * @param language Two character language code desired
71      * @param country Two character country code desired
72      *
73      * @return Locale or null if no appropriate value
74      */
getLocaleForLanguageCountry(Context context, String language, String country)75     private static Locale getLocaleForLanguageCountry(Context context, String language,
76                                                       String country) {
77         if (language == null) {
78             Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: skipping no language");
79             return null; // no match possible
80         }
81         if (country == null) {
82             country = ""; // The Locale constructor throws if passed null.
83         }
84 
85         final Locale target = new Locale(language, country);
86         try {
87             String[] localeArray = context.getAssets().getLocales();
88             List<String> locales = new ArrayList<>(Arrays.asList(localeArray));
89 
90             // Even in developer mode, you don't want the pseudolocales.
91             locales.remove("ar-XB");
92             locales.remove("en-XA");
93 
94             List<Locale> languageMatches = new ArrayList<>();
95             for (String locale : locales) {
96                 final Locale l = Locale.forLanguageTag(locale.replace('_', '-'));
97 
98                 // Only consider locales with both language and country.
99                 if (l == null || "und".equals(l.getLanguage())
100                         || l.getLanguage().isEmpty() || l.getCountry().isEmpty()) {
101                     continue;
102                 }
103                 if (l.getLanguage().equals(target.getLanguage())) {
104                     // If we got a perfect match, we're done.
105                     if (l.getCountry().equals(target.getCountry())) {
106                         Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got perfect match: "
107                                 + l.toLanguageTag());
108                         return l;
109                     }
110 
111                     // We've only matched the language, not the country.
112                     languageMatches.add(l);
113                 }
114             }
115 
116             if (languageMatches.isEmpty()) {
117                 Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: no locales for language " + language);
118                 return null;
119             }
120 
121             Locale bestMatch = lookupFallback(target, languageMatches);
122             if (bestMatch != null) {
123                 Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got a fallback match: "
124                         + bestMatch.toLanguageTag());
125                 return bestMatch;
126             } else {
127                 // If a locale is "translated", it is selectable in setup wizard, and can therefore
128                 // be considered a valid result for this method.
129                 if (!TextUtils.isEmpty(target.getCountry())) {
130                     if (isTranslated(context, target)) {
131                         Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: "
132                                 + "target locale is translated: " + target);
133                         return target;
134                     }
135                 }
136 
137                 // Somewhat arbitrarily take the first locale for the language,
138                 // unless we get a perfect match later. Note that these come back in no
139                 // particular order, so there's no reason to think the first match is
140                 // a particularly good match.
141                 Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got language-only match: "
142                         + language);
143                 return languageMatches.get(0);
144             }
145         } catch (Exception e) {
146             Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: exception", e);
147         }
148 
149         return null;
150     }
151 
152     /**
153      * Given a GSM Mobile Country Code, returns
154      * an ISO 2-3 character language code if available.
155      * Returns null if unavailable.
156      */
defaultLanguageForMcc(int mcc)157     public static String defaultLanguageForMcc(int mcc) {
158         MccTable.MccEntry entry = MccTable.entryForMcc(mcc);
159         if (entry == null) {
160             Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): no country for mcc");
161             return null;
162         }
163 
164         final String country = entry.mIso;
165 
166         // Choose English as the default language for India.
167         if ("in".equals(country)) {
168             return "en";
169         }
170 
171         // Ask CLDR for the language this country uses...
172         ULocale likelyLocale = ULocale.addLikelySubtags(new ULocale("und", country));
173         String likelyLanguage = likelyLocale.getLanguage();
174         Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): country " + country + " uses "
175                 + likelyLanguage);
176         return likelyLanguage;
177     }
178 
isTranslated(Context context, Locale targetLocale)179     private static boolean isTranslated(Context context, Locale targetLocale) {
180         ULocale fullTargetLocale = ULocale.addLikelySubtags(ULocale.forLocale(targetLocale));
181         String language = fullTargetLocale.getLanguage();
182         String script = fullTargetLocale.getScript();
183 
184         for (String localeId : context.getAssets().getLocales()) {
185             ULocale fullLocale = ULocale.addLikelySubtags(new ULocale(localeId));
186             if (language.equals(fullLocale.getLanguage())
187                     && script.equals(fullLocale.getScript())) {
188                 return true;
189             }
190         }
191         return false;
192     }
193 
194     /**
195      * Finds a suitable locale among {@code candidates} to use as the fallback locale for
196      * {@code target}. This looks through the list of {@link MccTable#FALLBACKS},
197      * and follows the chain until a locale in {@code candidates} is found.
198      * This function assumes that {@code target} is not in {@code candidates}.
199      *
200      * TODO: This should really follow the CLDR chain of parent locales! That might be a bit
201      * of a problem because we don't really have an en-001 locale on android.
202      *
203      * @return The fallback locale or {@code null} if there is no suitable fallback defined in the
204      *         lookup.
205      */
lookupFallback(Locale target, List<Locale> candidates)206     private static Locale lookupFallback(Locale target, List<Locale> candidates) {
207         Locale fallback = target;
208         while ((fallback = MccTable.FALLBACKS.get(fallback)) != null) {
209             if (candidates.contains(fallback)) {
210                 return fallback;
211             }
212         }
213 
214         return null;
215     }
216 }
217