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