Loading src/java/com/android/internal/telephony/MccTable.java +8 −188 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import android.annotation.UnsupportedAppUsage; import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; import android.icu.util.ULocale; import android.os.Build; import android.os.RemoteException; import android.os.SystemProperties; Loading @@ -31,8 +30,6 @@ import android.text.TextUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.LocaleStore; import com.android.internal.app.LocaleStore.LocaleInfo; import com.android.internal.telephony.util.TelephonyUtils; import libcore.timezone.TelephonyLookup; Loading @@ -40,10 +37,8 @@ import libcore.timezone.TelephonyNetwork; import libcore.timezone.TelephonyNetworkFinder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; Loading @@ -61,14 +56,18 @@ public final class MccTable { static ArrayList<MccEntry> sTable; static class MccEntry implements Comparable<MccEntry> { /** * Container class for mcc and iso. This class implements compareTo so that it can be sorted * by mcc. */ public static class MccEntry implements Comparable<MccEntry> { final int mMcc; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "There is no alternative for {@code MccTable.MccEntry.mIso}, " + "but it was included in hidden APIs due to a static analysis false " + "positive and has been made greylist-max-q. Please file a bug if you " + "still require this API.") final String mIso; public final String mIso; final int mSmallestDigitsMnc; MccEntry(int mcc, String iso, int smallestDigitsMCC) { Loading Loading @@ -162,7 +161,7 @@ public final class MccTable { + "but it was included in hidden APIs due to a static analysis false positive " + "and has been made greylist-max-q. Please file a bug if you still require " + "this API.") private static MccEntry entryForMcc(int mcc) { public static MccEntry entryForMcc(int mcc) { MccEntry m = new MccEntry(mcc, "", 0); int index = Collections.binarySearch(sTable, m); Loading Loading @@ -248,37 +247,6 @@ public final class MccTable { return network.getCountryIsoCode(); } /** * Given a GSM Mobile Country Code, returns * an ISO 2-3 character language code if available. * Returns null if unavailable. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "There is no alternative for {@code MccTable.defaultLanguageForMcc" + "}, but it was included in hidden APIs due to a static analysis false " + "positive and has been made greylist-max-q. Please file a bug if you still " + "require this API.") public static String defaultLanguageForMcc(int mcc) { MccEntry entry = entryForMcc(mcc); if (entry == null) { Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): no country for mcc"); return null; } final String country = entry.mIso; // Choose English as the default language for India. if ("in".equals(country)) { return "en"; } // Ask CLDR for the language this country uses... ULocale likelyLocale = ULocale.addLikelySubtags(new ULocale("und", country)); String likelyLanguage = likelyLocale.getLanguage(); Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): country " + country + " uses " + likelyLanguage); return likelyLanguage; } /** * Given a GSM Mobile Country Code, returns Loading Loading @@ -354,161 +322,13 @@ public final class MccTable { /** * Maps a given locale to a fallback locale that approximates it. This is a hack. */ private static final Map<Locale, Locale> FALLBACKS = new HashMap<Locale, Locale>(); public static final Map<Locale, Locale> FALLBACKS = new HashMap<Locale, Locale>(); static { // If we have English (without a country) explicitly prioritize en_US. http://b/28998094 FALLBACKS.put(Locale.ENGLISH, Locale.US); } /** * Finds a suitable locale among {@code candidates} to use as the fallback locale for * {@code target}. This looks through the list of {@link #FALLBACKS}, and follows the chain * until a locale in {@code candidates} is found. * This function assumes that {@code target} is not in {@code candidates}. * * TODO: This should really follow the CLDR chain of parent locales! That might be a bit * of a problem because we don't really have an en-001 locale on android. * * @return The fallback locale or {@code null} if there is no suitable fallback defined in the * lookup. */ private static Locale lookupFallback(Locale target, List<Locale> candidates) { Locale fallback = target; while ((fallback = FALLBACKS.get(fallback)) != null) { if (candidates.contains(fallback)) { return fallback; } } return null; } /** * Return Locale for the language and country or null if no good match. * * @param context Context to act on. * @param language Two character language code desired * @param country Two character country code desired * * @return Locale or null if no appropriate value */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "There is no alternative for {@code MccTable" + ".getLocaleForLanguageCountry}, but it was included in hidden APIs due to a " + "static analysis false positive and has been made greylist-max-q. Please " + "file a bug if you still require this API.") private static Locale getLocaleForLanguageCountry(Context context, String language, String country) { if (language == null) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: skipping no language"); return null; // no match possible } if (country == null) { country = ""; // The Locale constructor throws if passed null. } final Locale target = new Locale(language, country); try { String[] localeArray = context.getAssets().getLocales(); List<String> locales = new ArrayList<>(Arrays.asList(localeArray)); // Even in developer mode, you don't want the pseudolocales. locales.remove("ar-XB"); locales.remove("en-XA"); List<Locale> languageMatches = new ArrayList<>(); for (String locale : locales) { final Locale l = Locale.forLanguageTag(locale.replace('_', '-')); // Only consider locales with both language and country. if (l == null || "und".equals(l.getLanguage()) || l.getLanguage().isEmpty() || l.getCountry().isEmpty()) { continue; } if (l.getLanguage().equals(target.getLanguage())) { // If we got a perfect match, we're done. if (l.getCountry().equals(target.getCountry())) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got perfect match: " + l.toLanguageTag()); return l; } // We've only matched the language, not the country. languageMatches.add(l); } } if (languageMatches.isEmpty()) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: no locales for language " + language); return null; } Locale bestMatch = lookupFallback(target, languageMatches); if (bestMatch != null) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got a fallback match: " + bestMatch.toLanguageTag()); return bestMatch; } else { // Ask {@link LocaleStore} whether this locale is considered "translated". // LocaleStore has a broader definition of translated than just the asset locales // above: a locale is "translated" if it has translation assets, or another locale // with the same language and script has translation assets. // If a locale is "translated", it is selectable in setup wizard, and can therefore // be considerd a valid result for this method. if (!TextUtils.isEmpty(target.getCountry())) { LocaleStore.fillCache(context); LocaleInfo targetInfo = LocaleStore.getLocaleInfo(target); if (targetInfo.isTranslated()) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: " + "target locale is translated: " + target); return target; } } // Somewhat arbitrarily take the first locale for the language, // unless we get a perfect match later. Note that these come back in no // particular order, so there's no reason to think the first match is // a particularly good match. Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got language-only match: " + language); return languageMatches.get(0); } } catch (Exception e) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: exception", e); } return null; } /** * Get Locale based on the MCC of the SIM. * * @param context Context to act on. * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA) * @param simLanguage (nullable) the language from the SIM records (if present). * * @return locale for the mcc or null if none */ public static Locale getLocaleFromMcc(Context context, int mcc, String simLanguage) { boolean hasSimLanguage = !TextUtils.isEmpty(simLanguage); String language = hasSimLanguage ? simLanguage : MccTable.defaultLanguageForMcc(mcc); String country = MccTable.countryCodeForMcc(mcc); Rlog.d(LOG_TAG, "getLocaleFromMcc(" + language + ", " + country + ", " + mcc); final Locale locale = getLocaleForLanguageCountry(context, language, country); // If we couldn't find a locale that matches the SIM language, give it a go again // with the "likely" language for the given country. if (locale == null && hasSimLanguage) { language = MccTable.defaultLanguageForMcc(mcc); Rlog.d(LOG_TAG, "[retry ] getLocaleFromMcc(" + language + ", " + country + ", " + mcc); return getLocaleForLanguageCountry(context, language, country); } return locale; } static { sTable = new ArrayList<MccEntry>(240); Loading src/java/com/android/internal/telephony/util/LocaleUtils.java 0 → 100644 +216 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 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.internal.telephony.util; import android.content.Context; import android.icu.util.ULocale; import android.telephony.Rlog; import android.text.TextUtils; import com.android.internal.telephony.MccTable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; /** * This class provides various util functions about Locale. */ public class LocaleUtils { private static final String LOG_TAG = "LocaleUtils"; /** * Get Locale based on the MCC of the SIM. * * @param context Context to act on. * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA) * @param simLanguage (nullable) the language from the SIM records (if present). * * @return locale for the mcc or null if none */ public static Locale getLocaleFromMcc(Context context, int mcc, String simLanguage) { boolean hasSimLanguage = !TextUtils.isEmpty(simLanguage); String language = hasSimLanguage ? simLanguage : defaultLanguageForMcc(mcc); String country = MccTable.countryCodeForMcc(mcc); Rlog.d(LOG_TAG, "getLocaleFromMcc(" + language + ", " + country + ", " + mcc); final Locale locale = getLocaleForLanguageCountry(context, language, country); // If we couldn't find a locale that matches the SIM language, give it a go again // with the "likely" language for the given country. if (locale == null && hasSimLanguage) { language = defaultLanguageForMcc(mcc); Rlog.d(LOG_TAG, "[retry ] getLocaleFromMcc(" + language + ", " + country + ", " + mcc); return getLocaleForLanguageCountry(context, language, country); } return locale; } /** * Return Locale for the language and country or null if no good match. * * @param context Context to act on. * @param language Two character language code desired * @param country Two character country code desired * * @return Locale or null if no appropriate value */ private static Locale getLocaleForLanguageCountry(Context context, String language, String country) { if (language == null) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: skipping no language"); return null; // no match possible } if (country == null) { country = ""; // The Locale constructor throws if passed null. } final Locale target = new Locale(language, country); try { String[] localeArray = context.getAssets().getLocales(); List<String> locales = new ArrayList<>(Arrays.asList(localeArray)); // Even in developer mode, you don't want the pseudolocales. locales.remove("ar-XB"); locales.remove("en-XA"); List<Locale> languageMatches = new ArrayList<>(); for (String locale : locales) { final Locale l = Locale.forLanguageTag(locale.replace('_', '-')); // Only consider locales with both language and country. if (l == null || "und".equals(l.getLanguage()) || l.getLanguage().isEmpty() || l.getCountry().isEmpty()) { continue; } if (l.getLanguage().equals(target.getLanguage())) { // If we got a perfect match, we're done. if (l.getCountry().equals(target.getCountry())) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got perfect match: " + l.toLanguageTag()); return l; } // We've only matched the language, not the country. languageMatches.add(l); } } if (languageMatches.isEmpty()) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: no locales for language " + language); return null; } Locale bestMatch = lookupFallback(target, languageMatches); if (bestMatch != null) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got a fallback match: " + bestMatch.toLanguageTag()); return bestMatch; } else { // If a locale is "translated", it is selectable in setup wizard, and can therefore // be considered a valid result for this method. if (!TextUtils.isEmpty(target.getCountry())) { if (isTranslated(context, target)) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: " + "target locale is translated: " + target); return target; } } // Somewhat arbitrarily take the first locale for the language, // unless we get a perfect match later. Note that these come back in no // particular order, so there's no reason to think the first match is // a particularly good match. Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got language-only match: " + language); return languageMatches.get(0); } } catch (Exception e) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: exception", e); } return null; } /** * Given a GSM Mobile Country Code, returns * an ISO 2-3 character language code if available. * Returns null if unavailable. */ public static String defaultLanguageForMcc(int mcc) { MccTable.MccEntry entry = MccTable.entryForMcc(mcc); if (entry == null) { Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): no country for mcc"); return null; } final String country = entry.mIso; // Choose English as the default language for India. if ("in".equals(country)) { return "en"; } // Ask CLDR for the language this country uses... ULocale likelyLocale = ULocale.addLikelySubtags(new ULocale("und", country)); String likelyLanguage = likelyLocale.getLanguage(); Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): country " + country + " uses " + likelyLanguage); return likelyLanguage; } private static boolean isTranslated(Context context, Locale targetLocale) { ULocale fullTargetLocale = ULocale.addLikelySubtags(ULocale.forLocale(targetLocale)); String language = fullTargetLocale.getLanguage(); String script = fullTargetLocale.getScript(); for (String localeId : context.getAssets().getLocales()) { ULocale fullLocale = ULocale.addLikelySubtags(new ULocale(localeId)); if (language.equals(fullLocale.getLanguage()) && script.equals(fullLocale.getScript())) { return true; } } return false; } /** * Finds a suitable locale among {@code candidates} to use as the fallback locale for * {@code target}. This looks through the list of {@link MccTable#FALLBACKS}, * and follows the chain until a locale in {@code candidates} is found. * This function assumes that {@code target} is not in {@code candidates}. * * TODO: This should really follow the CLDR chain of parent locales! That might be a bit * of a problem because we don't really have an en-001 locale on android. * * @return The fallback locale or {@code null} if there is no suitable fallback defined in the * lookup. */ private static Locale lookupFallback(Locale target, List<Locale> candidates) { Locale fallback = target; while ((fallback = MccTable.FALLBACKS.get(fallback)) != null) { if (candidates.contains(fallback)) { return fallback; } } return null; } } tests/telephonytests/src/com/android/internal/telephony/MccTableTest.java +25 −19 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.test.suitebuilder.annotation.SmallTest; import androidx.test.InstrumentationRegistry; import com.android.internal.telephony.MccTable.MccMnc; import com.android.internal.telephony.util.LocaleUtils; import org.junit.Test; Loading Loading @@ -62,38 +63,40 @@ public class MccTableTest { @SmallTest @Test public void testLang() throws Exception { assertEquals("en", MccTable.defaultLanguageForMcc(311)); assertEquals("de", MccTable.defaultLanguageForMcc(232)); assertEquals("cs", MccTable.defaultLanguageForMcc(230)); assertEquals("nl", MccTable.defaultLanguageForMcc(204)); assertEquals("is", MccTable.defaultLanguageForMcc(274)); assertEquals(null, MccTable.defaultLanguageForMcc(0)); // mcc not defined, hence default assertEquals(null, MccTable.defaultLanguageForMcc(2000)); // mcc not defined, hence default assertEquals("en", LocaleUtils.defaultLanguageForMcc(311)); assertEquals("de", LocaleUtils.defaultLanguageForMcc(232)); assertEquals("cs", LocaleUtils.defaultLanguageForMcc(230)); assertEquals("nl", LocaleUtils.defaultLanguageForMcc(204)); assertEquals("is", LocaleUtils.defaultLanguageForMcc(274)); // mcc not defined, hence default assertEquals(null, LocaleUtils.defaultLanguageForMcc(0)); // mcc not defined, hence default assertEquals(null, LocaleUtils.defaultLanguageForMcc(2000)); } @SmallTest @Test public void testLang_India() throws Exception { assertEquals("en", MccTable.defaultLanguageForMcc(404)); assertEquals("en", MccTable.defaultLanguageForMcc(405)); assertEquals("en", MccTable.defaultLanguageForMcc(406)); assertEquals("en", LocaleUtils.defaultLanguageForMcc(404)); assertEquals("en", LocaleUtils.defaultLanguageForMcc(405)); assertEquals("en", LocaleUtils.defaultLanguageForMcc(406)); } @SmallTest @Test public void testLocale() throws Exception { assertEquals(Locale.forLanguageTag("en-CA"), MccTable.getLocaleFromMcc(getContext(), 302, null)); LocaleUtils.getLocaleFromMcc(getContext(), 302, null)); assertEquals(Locale.forLanguageTag("en-GB"), MccTable.getLocaleFromMcc(getContext(), 234, null)); LocaleUtils.getLocaleFromMcc(getContext(), 234, null)); assertEquals(Locale.forLanguageTag("en-US"), MccTable.getLocaleFromMcc(getContext(), 0, "en")); LocaleUtils.getLocaleFromMcc(getContext(), 0, "en")); assertEquals(Locale.forLanguageTag("zh-HK"), MccTable.getLocaleFromMcc(getContext(), 454, null)); LocaleUtils.getLocaleFromMcc(getContext(), 454, null)); assertEquals(Locale.forLanguageTag("en-HK"), MccTable.getLocaleFromMcc(getContext(), 454, "en")); LocaleUtils.getLocaleFromMcc(getContext(), 454, "en")); assertEquals(Locale.forLanguageTag("zh-TW"), MccTable.getLocaleFromMcc(getContext(), 466, null)); LocaleUtils.getLocaleFromMcc(getContext(), 466, null)); } private Context getContext() { Loading @@ -107,8 +110,11 @@ public class MccTableTest { assertEquals(2, MccTable.smallestDigitsMccForMnc(430)); assertEquals(3, MccTable.smallestDigitsMccForMnc(365)); assertEquals(2, MccTable.smallestDigitsMccForMnc(536)); assertEquals(2, MccTable.smallestDigitsMccForMnc(352)); // sd not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(0)); // mcc not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(2000)); // mcc not defined, hence default // sd not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(352)); // mcc not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(0)); // mcc not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(2000)); } } Loading
src/java/com/android/internal/telephony/MccTable.java +8 −188 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import android.annotation.UnsupportedAppUsage; import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; import android.icu.util.ULocale; import android.os.Build; import android.os.RemoteException; import android.os.SystemProperties; Loading @@ -31,8 +30,6 @@ import android.text.TextUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.LocaleStore; import com.android.internal.app.LocaleStore.LocaleInfo; import com.android.internal.telephony.util.TelephonyUtils; import libcore.timezone.TelephonyLookup; Loading @@ -40,10 +37,8 @@ import libcore.timezone.TelephonyNetwork; import libcore.timezone.TelephonyNetworkFinder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; Loading @@ -61,14 +56,18 @@ public final class MccTable { static ArrayList<MccEntry> sTable; static class MccEntry implements Comparable<MccEntry> { /** * Container class for mcc and iso. This class implements compareTo so that it can be sorted * by mcc. */ public static class MccEntry implements Comparable<MccEntry> { final int mMcc; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "There is no alternative for {@code MccTable.MccEntry.mIso}, " + "but it was included in hidden APIs due to a static analysis false " + "positive and has been made greylist-max-q. Please file a bug if you " + "still require this API.") final String mIso; public final String mIso; final int mSmallestDigitsMnc; MccEntry(int mcc, String iso, int smallestDigitsMCC) { Loading Loading @@ -162,7 +161,7 @@ public final class MccTable { + "but it was included in hidden APIs due to a static analysis false positive " + "and has been made greylist-max-q. Please file a bug if you still require " + "this API.") private static MccEntry entryForMcc(int mcc) { public static MccEntry entryForMcc(int mcc) { MccEntry m = new MccEntry(mcc, "", 0); int index = Collections.binarySearch(sTable, m); Loading Loading @@ -248,37 +247,6 @@ public final class MccTable { return network.getCountryIsoCode(); } /** * Given a GSM Mobile Country Code, returns * an ISO 2-3 character language code if available. * Returns null if unavailable. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "There is no alternative for {@code MccTable.defaultLanguageForMcc" + "}, but it was included in hidden APIs due to a static analysis false " + "positive and has been made greylist-max-q. Please file a bug if you still " + "require this API.") public static String defaultLanguageForMcc(int mcc) { MccEntry entry = entryForMcc(mcc); if (entry == null) { Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): no country for mcc"); return null; } final String country = entry.mIso; // Choose English as the default language for India. if ("in".equals(country)) { return "en"; } // Ask CLDR for the language this country uses... ULocale likelyLocale = ULocale.addLikelySubtags(new ULocale("und", country)); String likelyLanguage = likelyLocale.getLanguage(); Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): country " + country + " uses " + likelyLanguage); return likelyLanguage; } /** * Given a GSM Mobile Country Code, returns Loading Loading @@ -354,161 +322,13 @@ public final class MccTable { /** * Maps a given locale to a fallback locale that approximates it. This is a hack. */ private static final Map<Locale, Locale> FALLBACKS = new HashMap<Locale, Locale>(); public static final Map<Locale, Locale> FALLBACKS = new HashMap<Locale, Locale>(); static { // If we have English (without a country) explicitly prioritize en_US. http://b/28998094 FALLBACKS.put(Locale.ENGLISH, Locale.US); } /** * Finds a suitable locale among {@code candidates} to use as the fallback locale for * {@code target}. This looks through the list of {@link #FALLBACKS}, and follows the chain * until a locale in {@code candidates} is found. * This function assumes that {@code target} is not in {@code candidates}. * * TODO: This should really follow the CLDR chain of parent locales! That might be a bit * of a problem because we don't really have an en-001 locale on android. * * @return The fallback locale or {@code null} if there is no suitable fallback defined in the * lookup. */ private static Locale lookupFallback(Locale target, List<Locale> candidates) { Locale fallback = target; while ((fallback = FALLBACKS.get(fallback)) != null) { if (candidates.contains(fallback)) { return fallback; } } return null; } /** * Return Locale for the language and country or null if no good match. * * @param context Context to act on. * @param language Two character language code desired * @param country Two character country code desired * * @return Locale or null if no appropriate value */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "There is no alternative for {@code MccTable" + ".getLocaleForLanguageCountry}, but it was included in hidden APIs due to a " + "static analysis false positive and has been made greylist-max-q. Please " + "file a bug if you still require this API.") private static Locale getLocaleForLanguageCountry(Context context, String language, String country) { if (language == null) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: skipping no language"); return null; // no match possible } if (country == null) { country = ""; // The Locale constructor throws if passed null. } final Locale target = new Locale(language, country); try { String[] localeArray = context.getAssets().getLocales(); List<String> locales = new ArrayList<>(Arrays.asList(localeArray)); // Even in developer mode, you don't want the pseudolocales. locales.remove("ar-XB"); locales.remove("en-XA"); List<Locale> languageMatches = new ArrayList<>(); for (String locale : locales) { final Locale l = Locale.forLanguageTag(locale.replace('_', '-')); // Only consider locales with both language and country. if (l == null || "und".equals(l.getLanguage()) || l.getLanguage().isEmpty() || l.getCountry().isEmpty()) { continue; } if (l.getLanguage().equals(target.getLanguage())) { // If we got a perfect match, we're done. if (l.getCountry().equals(target.getCountry())) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got perfect match: " + l.toLanguageTag()); return l; } // We've only matched the language, not the country. languageMatches.add(l); } } if (languageMatches.isEmpty()) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: no locales for language " + language); return null; } Locale bestMatch = lookupFallback(target, languageMatches); if (bestMatch != null) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got a fallback match: " + bestMatch.toLanguageTag()); return bestMatch; } else { // Ask {@link LocaleStore} whether this locale is considered "translated". // LocaleStore has a broader definition of translated than just the asset locales // above: a locale is "translated" if it has translation assets, or another locale // with the same language and script has translation assets. // If a locale is "translated", it is selectable in setup wizard, and can therefore // be considerd a valid result for this method. if (!TextUtils.isEmpty(target.getCountry())) { LocaleStore.fillCache(context); LocaleInfo targetInfo = LocaleStore.getLocaleInfo(target); if (targetInfo.isTranslated()) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: " + "target locale is translated: " + target); return target; } } // Somewhat arbitrarily take the first locale for the language, // unless we get a perfect match later. Note that these come back in no // particular order, so there's no reason to think the first match is // a particularly good match. Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got language-only match: " + language); return languageMatches.get(0); } } catch (Exception e) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: exception", e); } return null; } /** * Get Locale based on the MCC of the SIM. * * @param context Context to act on. * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA) * @param simLanguage (nullable) the language from the SIM records (if present). * * @return locale for the mcc or null if none */ public static Locale getLocaleFromMcc(Context context, int mcc, String simLanguage) { boolean hasSimLanguage = !TextUtils.isEmpty(simLanguage); String language = hasSimLanguage ? simLanguage : MccTable.defaultLanguageForMcc(mcc); String country = MccTable.countryCodeForMcc(mcc); Rlog.d(LOG_TAG, "getLocaleFromMcc(" + language + ", " + country + ", " + mcc); final Locale locale = getLocaleForLanguageCountry(context, language, country); // If we couldn't find a locale that matches the SIM language, give it a go again // with the "likely" language for the given country. if (locale == null && hasSimLanguage) { language = MccTable.defaultLanguageForMcc(mcc); Rlog.d(LOG_TAG, "[retry ] getLocaleFromMcc(" + language + ", " + country + ", " + mcc); return getLocaleForLanguageCountry(context, language, country); } return locale; } static { sTable = new ArrayList<MccEntry>(240); Loading
src/java/com/android/internal/telephony/util/LocaleUtils.java 0 → 100644 +216 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 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.internal.telephony.util; import android.content.Context; import android.icu.util.ULocale; import android.telephony.Rlog; import android.text.TextUtils; import com.android.internal.telephony.MccTable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; /** * This class provides various util functions about Locale. */ public class LocaleUtils { private static final String LOG_TAG = "LocaleUtils"; /** * Get Locale based on the MCC of the SIM. * * @param context Context to act on. * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA) * @param simLanguage (nullable) the language from the SIM records (if present). * * @return locale for the mcc or null if none */ public static Locale getLocaleFromMcc(Context context, int mcc, String simLanguage) { boolean hasSimLanguage = !TextUtils.isEmpty(simLanguage); String language = hasSimLanguage ? simLanguage : defaultLanguageForMcc(mcc); String country = MccTable.countryCodeForMcc(mcc); Rlog.d(LOG_TAG, "getLocaleFromMcc(" + language + ", " + country + ", " + mcc); final Locale locale = getLocaleForLanguageCountry(context, language, country); // If we couldn't find a locale that matches the SIM language, give it a go again // with the "likely" language for the given country. if (locale == null && hasSimLanguage) { language = defaultLanguageForMcc(mcc); Rlog.d(LOG_TAG, "[retry ] getLocaleFromMcc(" + language + ", " + country + ", " + mcc); return getLocaleForLanguageCountry(context, language, country); } return locale; } /** * Return Locale for the language and country or null if no good match. * * @param context Context to act on. * @param language Two character language code desired * @param country Two character country code desired * * @return Locale or null if no appropriate value */ private static Locale getLocaleForLanguageCountry(Context context, String language, String country) { if (language == null) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: skipping no language"); return null; // no match possible } if (country == null) { country = ""; // The Locale constructor throws if passed null. } final Locale target = new Locale(language, country); try { String[] localeArray = context.getAssets().getLocales(); List<String> locales = new ArrayList<>(Arrays.asList(localeArray)); // Even in developer mode, you don't want the pseudolocales. locales.remove("ar-XB"); locales.remove("en-XA"); List<Locale> languageMatches = new ArrayList<>(); for (String locale : locales) { final Locale l = Locale.forLanguageTag(locale.replace('_', '-')); // Only consider locales with both language and country. if (l == null || "und".equals(l.getLanguage()) || l.getLanguage().isEmpty() || l.getCountry().isEmpty()) { continue; } if (l.getLanguage().equals(target.getLanguage())) { // If we got a perfect match, we're done. if (l.getCountry().equals(target.getCountry())) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got perfect match: " + l.toLanguageTag()); return l; } // We've only matched the language, not the country. languageMatches.add(l); } } if (languageMatches.isEmpty()) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: no locales for language " + language); return null; } Locale bestMatch = lookupFallback(target, languageMatches); if (bestMatch != null) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got a fallback match: " + bestMatch.toLanguageTag()); return bestMatch; } else { // If a locale is "translated", it is selectable in setup wizard, and can therefore // be considered a valid result for this method. if (!TextUtils.isEmpty(target.getCountry())) { if (isTranslated(context, target)) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: " + "target locale is translated: " + target); return target; } } // Somewhat arbitrarily take the first locale for the language, // unless we get a perfect match later. Note that these come back in no // particular order, so there's no reason to think the first match is // a particularly good match. Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: got language-only match: " + language); return languageMatches.get(0); } } catch (Exception e) { Rlog.d(LOG_TAG, "getLocaleForLanguageCountry: exception", e); } return null; } /** * Given a GSM Mobile Country Code, returns * an ISO 2-3 character language code if available. * Returns null if unavailable. */ public static String defaultLanguageForMcc(int mcc) { MccTable.MccEntry entry = MccTable.entryForMcc(mcc); if (entry == null) { Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): no country for mcc"); return null; } final String country = entry.mIso; // Choose English as the default language for India. if ("in".equals(country)) { return "en"; } // Ask CLDR for the language this country uses... ULocale likelyLocale = ULocale.addLikelySubtags(new ULocale("und", country)); String likelyLanguage = likelyLocale.getLanguage(); Rlog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): country " + country + " uses " + likelyLanguage); return likelyLanguage; } private static boolean isTranslated(Context context, Locale targetLocale) { ULocale fullTargetLocale = ULocale.addLikelySubtags(ULocale.forLocale(targetLocale)); String language = fullTargetLocale.getLanguage(); String script = fullTargetLocale.getScript(); for (String localeId : context.getAssets().getLocales()) { ULocale fullLocale = ULocale.addLikelySubtags(new ULocale(localeId)); if (language.equals(fullLocale.getLanguage()) && script.equals(fullLocale.getScript())) { return true; } } return false; } /** * Finds a suitable locale among {@code candidates} to use as the fallback locale for * {@code target}. This looks through the list of {@link MccTable#FALLBACKS}, * and follows the chain until a locale in {@code candidates} is found. * This function assumes that {@code target} is not in {@code candidates}. * * TODO: This should really follow the CLDR chain of parent locales! That might be a bit * of a problem because we don't really have an en-001 locale on android. * * @return The fallback locale or {@code null} if there is no suitable fallback defined in the * lookup. */ private static Locale lookupFallback(Locale target, List<Locale> candidates) { Locale fallback = target; while ((fallback = MccTable.FALLBACKS.get(fallback)) != null) { if (candidates.contains(fallback)) { return fallback; } } return null; } }
tests/telephonytests/src/com/android/internal/telephony/MccTableTest.java +25 −19 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.test.suitebuilder.annotation.SmallTest; import androidx.test.InstrumentationRegistry; import com.android.internal.telephony.MccTable.MccMnc; import com.android.internal.telephony.util.LocaleUtils; import org.junit.Test; Loading Loading @@ -62,38 +63,40 @@ public class MccTableTest { @SmallTest @Test public void testLang() throws Exception { assertEquals("en", MccTable.defaultLanguageForMcc(311)); assertEquals("de", MccTable.defaultLanguageForMcc(232)); assertEquals("cs", MccTable.defaultLanguageForMcc(230)); assertEquals("nl", MccTable.defaultLanguageForMcc(204)); assertEquals("is", MccTable.defaultLanguageForMcc(274)); assertEquals(null, MccTable.defaultLanguageForMcc(0)); // mcc not defined, hence default assertEquals(null, MccTable.defaultLanguageForMcc(2000)); // mcc not defined, hence default assertEquals("en", LocaleUtils.defaultLanguageForMcc(311)); assertEquals("de", LocaleUtils.defaultLanguageForMcc(232)); assertEquals("cs", LocaleUtils.defaultLanguageForMcc(230)); assertEquals("nl", LocaleUtils.defaultLanguageForMcc(204)); assertEquals("is", LocaleUtils.defaultLanguageForMcc(274)); // mcc not defined, hence default assertEquals(null, LocaleUtils.defaultLanguageForMcc(0)); // mcc not defined, hence default assertEquals(null, LocaleUtils.defaultLanguageForMcc(2000)); } @SmallTest @Test public void testLang_India() throws Exception { assertEquals("en", MccTable.defaultLanguageForMcc(404)); assertEquals("en", MccTable.defaultLanguageForMcc(405)); assertEquals("en", MccTable.defaultLanguageForMcc(406)); assertEquals("en", LocaleUtils.defaultLanguageForMcc(404)); assertEquals("en", LocaleUtils.defaultLanguageForMcc(405)); assertEquals("en", LocaleUtils.defaultLanguageForMcc(406)); } @SmallTest @Test public void testLocale() throws Exception { assertEquals(Locale.forLanguageTag("en-CA"), MccTable.getLocaleFromMcc(getContext(), 302, null)); LocaleUtils.getLocaleFromMcc(getContext(), 302, null)); assertEquals(Locale.forLanguageTag("en-GB"), MccTable.getLocaleFromMcc(getContext(), 234, null)); LocaleUtils.getLocaleFromMcc(getContext(), 234, null)); assertEquals(Locale.forLanguageTag("en-US"), MccTable.getLocaleFromMcc(getContext(), 0, "en")); LocaleUtils.getLocaleFromMcc(getContext(), 0, "en")); assertEquals(Locale.forLanguageTag("zh-HK"), MccTable.getLocaleFromMcc(getContext(), 454, null)); LocaleUtils.getLocaleFromMcc(getContext(), 454, null)); assertEquals(Locale.forLanguageTag("en-HK"), MccTable.getLocaleFromMcc(getContext(), 454, "en")); LocaleUtils.getLocaleFromMcc(getContext(), 454, "en")); assertEquals(Locale.forLanguageTag("zh-TW"), MccTable.getLocaleFromMcc(getContext(), 466, null)); LocaleUtils.getLocaleFromMcc(getContext(), 466, null)); } private Context getContext() { Loading @@ -107,8 +110,11 @@ public class MccTableTest { assertEquals(2, MccTable.smallestDigitsMccForMnc(430)); assertEquals(3, MccTable.smallestDigitsMccForMnc(365)); assertEquals(2, MccTable.smallestDigitsMccForMnc(536)); assertEquals(2, MccTable.smallestDigitsMccForMnc(352)); // sd not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(0)); // mcc not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(2000)); // mcc not defined, hence default // sd not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(352)); // mcc not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(0)); // mcc not defined, hence default assertEquals(2, MccTable.smallestDigitsMccForMnc(2000)); } }