blob: 064b10dd85ef0538a6e898aaa67586c6b4a049e2 [file] [log] [blame]
/*
* 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.text.TextUtils;
import com.android.internal.telephony.MccTable;
import com.android.telephony.Rlog;
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;
}
}