Loading src/java/com/android/internal/telephony/LocaleTracker.java +108 −26 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ import android.text.TextUtils; import android.util.LocalLog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.MccTable.MccMnc; import com.android.internal.util.IndentingPrintWriter; import java.io.FileDescriptor; Loading Loading @@ -282,23 +283,16 @@ public class LocaleTracker extends Handler { private String getMccFromCellInfo() { String selectedMcc = null; if (mCellInfoList != null) { Map<String, Integer> countryCodeMap = new HashMap<>(); Map<String, Integer> mccMap = new HashMap<>(); int maxCount = 0; for (CellInfo cellInfo : mCellInfoList) { String mcc = null; if (cellInfo instanceof CellInfoGsm) { mcc = ((CellInfoGsm) cellInfo).getCellIdentity().getMccString(); } else if (cellInfo instanceof CellInfoLte) { mcc = ((CellInfoLte) cellInfo).getCellIdentity().getMccString(); } else if (cellInfo instanceof CellInfoWcdma) { mcc = ((CellInfoWcdma) cellInfo).getCellIdentity().getMccString(); } String mcc = getNetworkMcc(cellInfo); if (mcc != null) { int count = 1; if (countryCodeMap.containsKey(mcc)) { count = countryCodeMap.get(mcc) + 1; if (mccMap.containsKey(mcc)) { count = mccMap.get(mcc) + 1; } countryCodeMap.put(mcc, count); mccMap.put(mcc, count); // This is unlikely, but if MCC from cell info looks different, we choose the // MCC that occurs most. if (count > maxCount) { Loading @@ -311,6 +305,67 @@ public class LocaleTracker extends Handler { return selectedMcc; } /** * Get the most frequent MCC + MNC combination with the specified MCC using cell tower * information. If no one combination is more frequent than any other an arbitrary MCC + MNC is * returned with the matching MCC. The MNC value returned can be null if it is not provided by * the cell tower information. * * @param mccToMatch the MCC to match * @return a matching {@link MccMnc}. Null if the information is not available. */ @Nullable private MccMnc getMccMncFromCellInfo(String mccToMatch) { MccMnc selectedMccMnc = null; if (mCellInfoList != null) { Map<MccMnc, Integer> mccMncMap = new HashMap<>(); int maxCount = 0; for (CellInfo cellInfo : mCellInfoList) { String mcc = getNetworkMcc(cellInfo); if (Objects.equals(mcc, mccToMatch)) { String mnc = getNetworkMnc(cellInfo); MccMnc mccMnc = new MccMnc(mcc, mnc); int count = 1; if (mccMncMap.containsKey(mccMnc)) { count = mccMncMap.get(mccMnc) + 1; } mccMncMap.put(mccMnc, count); // This is unlikely, but if MCC from cell info looks different, we choose the // MCC that occurs most. if (count > maxCount) { maxCount = count; selectedMccMnc = mccMnc; } } } } return selectedMccMnc; } private static String getNetworkMcc(CellInfo cellInfo) { String mccString = null; if (cellInfo instanceof CellInfoGsm) { mccString = ((CellInfoGsm) cellInfo).getCellIdentity().getMccString(); } else if (cellInfo instanceof CellInfoLte) { mccString = ((CellInfoLte) cellInfo).getCellIdentity().getMccString(); } else if (cellInfo instanceof CellInfoWcdma) { mccString = ((CellInfoWcdma) cellInfo).getCellIdentity().getMccString(); } return mccString; } private static String getNetworkMnc(CellInfo cellInfo) { String mccString = null; if (cellInfo instanceof CellInfoGsm) { mccString = ((CellInfoGsm) cellInfo).getCellIdentity().getMncString(); } else if (cellInfo instanceof CellInfoLte) { mccString = ((CellInfoLte) cellInfo).getCellIdentity().getMncString(); } else if (cellInfo instanceof CellInfoWcdma) { mccString = ((CellInfoWcdma) cellInfo).getCellIdentity().getMncString(); } return mccString; } /** * Called when SIM card state changed. Only when we absolutely know the SIM is absent, we get * cell info from the network. Other SIM states like NOT_READY might be just a transitioning Loading Loading @@ -457,15 +512,23 @@ public class LocaleTracker extends Handler { String countryIso = getCarrierCountry(); String countryIsoDebugInfo = "getCarrierCountry()"; // For time zone detection we want the best geographical match we can get, which may differ // from the countryIso. String timeZoneCountryIso = null; String timeZoneCountryIsoDebugInfo = null; if (!TextUtils.isEmpty(mOperatorNumeric)) { try { String mcc = mOperatorNumeric.substring(0, 3); countryIso = MccTable.countryCodeForMcc(mcc); MccMnc mccMnc = MccMnc.fromOperatorNumeric(mOperatorNumeric); if (mccMnc != null) { countryIso = MccTable.countryCodeForMcc(mccMnc.mcc); countryIsoDebugInfo = "OperatorNumeric(" + mOperatorNumeric + "): MccTable.countryCodeForMcc(\"" + mcc + "\")"; } catch (StringIndexOutOfBoundsException ex) { + "): MccTable.countryCodeForMcc(\"" + mccMnc.mcc + "\")"; timeZoneCountryIso = MccTable.geoCountryCodeForMccMnc(mccMnc); timeZoneCountryIsoDebugInfo = "OperatorNumeric: MccTable.geoCountryCodeForMccMnc(" + mccMnc + ")"; } else { loge("updateLocale: Can't get country from operator numeric. mOperatorNumeric = " + mOperatorNumeric + ". ex=" + ex); + mOperatorNumeric); } } Loading @@ -475,19 +538,27 @@ public class LocaleTracker extends Handler { String mcc = getMccFromCellInfo(); countryIso = MccTable.countryCodeForMcc(mcc); countryIsoDebugInfo = "CellInfo: MccTable.countryCodeForMcc(\"" + mcc + "\")"; MccMnc mccMnc = getMccMncFromCellInfo(mcc); if (mccMnc != null) { timeZoneCountryIso = MccTable.geoCountryCodeForMccMnc(mccMnc); timeZoneCountryIsoDebugInfo = "CellInfo: MccTable.geoCountryCodeForMccMnc(" + mccMnc + ")"; } } if (mCountryOverride != null) { countryIso = mCountryOverride; countryIsoDebugInfo = "mCountryOverride = \"" + mCountryOverride + "\""; log("Override current country to " + mCountryOverride); timeZoneCountryIso = countryIso; timeZoneCountryIsoDebugInfo = countryIsoDebugInfo; } log("updateLocale: countryIso = " + countryIso + ", countryIsoDebugInfo = " + countryIsoDebugInfo); if (!Objects.equals(countryIso, mCurrentCountryIso)) { String msg = "updateLocale: Change the current country to \"" + countryIso + "\", countryIsoDebugInfo = " + countryIsoDebugInfo String msg = "updateLocale: Change the current country to \"" + countryIso + "\"" + ", countryIsoDebugInfo = " + countryIsoDebugInfo + ", mCellInfoList = " + mCellInfoList; log(msg); mLocalLog.log(msg); Loading @@ -502,19 +573,30 @@ public class LocaleTracker extends Handler { mPhone.getContext().sendBroadcast(intent); } // For a test cell, the NitzStateMachine requires handleCountryDetected("") to pass // compliance tests. http://b/142840879 // Pass the geographical country information to the telephony time zone detection code. boolean isTestMcc = false; if (!TextUtils.isEmpty(mOperatorNumeric)) { // For a test cell (MCC 001), the NitzStateMachine requires handleCountryDetected("") in // order to pass compliance tests. http://b/142840879 if (mOperatorNumeric.startsWith("001")) { isTestMcc = true; countryIso = ""; timeZoneCountryIso = ""; timeZoneCountryIsoDebugInfo = "Test cell: " + mOperatorNumeric; } } if (TextUtils.isEmpty(countryIso) && !isTestMcc) { if (timeZoneCountryIso == null) { // After this timeZoneCountryIso may still be null. timeZoneCountryIso = countryIso; timeZoneCountryIsoDebugInfo = "Defaulted: " + countryIsoDebugInfo; } log("updateLocale: timeZoneCountryIso = " + timeZoneCountryIso + ", timeZoneCountryIsoDebugInfo = " + timeZoneCountryIsoDebugInfo); if (TextUtils.isEmpty(timeZoneCountryIso) && !isTestMcc) { mNitzStateMachine.handleCountryUnavailable(); } else { mNitzStateMachine.handleCountryDetected(countryIso); mNitzStateMachine.handleCountryDetected(timeZoneCountryIso); } } Loading src/java/com/android/internal/telephony/MccTable.java +137 −9 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.internal.telephony; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; import android.app.ActivityManager; import android.content.Context; Loading @@ -27,9 +29,15 @@ import android.os.SystemProperties; import android.text.TextUtils; import android.util.Slog; 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 libcore.timezone.TelephonyLookup; import libcore.timezone.TelephonyNetwork; import libcore.timezone.TelephonyNetworkFinder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; Loading @@ -37,6 +45,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; /** * Mobile Country Code Loading @@ -46,6 +55,9 @@ import java.util.Map; public final class MccTable { static final String LOG_TAG = "MccTable"; @GuardedBy("MccTable.class") private static TelephonyNetworkFinder sTelephonyNetworkFinder; static ArrayList<MccEntry> sTable; static class MccEntry implements Comparable<MccEntry> { Loading @@ -58,11 +70,11 @@ public final class MccTable { final String mIso; final int mSmallestDigitsMnc; MccEntry(int mnc, String iso, int smallestDigitsMCC) { MccEntry(int mcc, String iso, int smallestDigitsMCC) { if (iso == null) { throw new NullPointerException(); } mMcc = mnc; mMcc = mcc; mIso = iso; mSmallestDigitsMnc = smallestDigitsMCC; } Loading @@ -73,6 +85,77 @@ public final class MccTable { } } /** * A combination of MCC and MNC. The MNC is optional and may be null. * * @hide */ @VisibleForTesting public static class MccMnc { @NonNull public final String mcc; @Nullable public final String mnc; /** * Splits the supplied String in two: the first three characters are treated as the MCC, * the remaining characters are treated as the MNC. */ @Nullable public static MccMnc fromOperatorNumeric(@NonNull String operatorNumeric) { Objects.requireNonNull(operatorNumeric); String mcc; try { mcc = operatorNumeric.substring(0, 3); } catch (StringIndexOutOfBoundsException e) { return null; } String mnc; try { mnc = operatorNumeric.substring(3); } catch (StringIndexOutOfBoundsException e) { mnc = null; } return new MccMnc(mcc, mnc); } /** * Creates an MccMnc using the supplied values. */ public MccMnc(@NonNull String mcc, @Nullable String mnc) { this.mcc = Objects.requireNonNull(mcc); this.mnc = mnc; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } MccMnc mccMnc = (MccMnc) o; return mcc.equals(mccMnc.mcc) && Objects.equals(mnc, mccMnc.mnc); } @Override public int hashCode() { return Objects.hash(mcc, mnc); } @Override public String toString() { return "MccMnc{" + "mcc='" + mcc + '\'' + ", mnc='" + mnc + '\'' + '}'; } } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "There is no alternative for {@code MccTable.entryForMcc}, " + "but it was included in hidden APIs due to a static analysis false positive " Loading @@ -91,11 +174,11 @@ public final class MccTable { } /** * Given a GSM Mobile Country Code, returns * an ISO two-character country code if available. * Returns "" if unavailable. * Given a GSM Mobile Country Code, returns a lower-case ISO 3166 alpha-2 country code if * available. Returns empty string if unavailable. */ @UnsupportedAppUsage @NonNull public static String countryCodeForMcc(int mcc) { MccEntry entry = entryForMcc(mcc); Loading @@ -107,11 +190,11 @@ public final class MccTable { } /** * Given a GSM Mobile Country Code, returns * an ISO two-character country code if available. * Returns empty string if unavailable. * Given a GSM Mobile Country Code, returns a lower-case ISO 3166 alpha-2 country code if * available. Returns empty string if unavailable. */ public static String countryCodeForMcc(String mcc) { @NonNull public static String countryCodeForMcc(@NonNull String mcc) { try { return countryCodeForMcc(Integer.parseInt(mcc)); } catch (NumberFormatException ex) { Loading @@ -119,6 +202,51 @@ public final class MccTable { } } /** * Given a combination of MCC and MNC, returns a lower case ISO 3166 alpha-2 country code for * the device's geographical location. * * <p>This can give a better geographical result than {@link #countryCodeForMcc(String)} * (which provides the official "which country is the MCC assigned to?" answer) for cases when * MNC is also available: Sometimes an MCC can be used by multiple countries and the MNC can * help distinguish, or the MCC assigned to a country isn't used for geopolitical reasons. * When the geographical country is needed (e.g. time zone detection) this version can provide * more pragmatic results than the official MCC-only answer. This method falls back to calling * {@link #countryCodeForMcc(int)} if no special MCC+MNC cases are found. * Returns empty string if no code can be determined. */ @NonNull public static String geoCountryCodeForMccMnc(@NonNull MccMnc mccMnc) { String countryCode = null; if (mccMnc.mnc != null) { countryCode = countryCodeForMccMncNoFallback(mccMnc); } if (TextUtils.isEmpty(countryCode)) { // Try the MCC-only fallback. countryCode = MccTable.countryCodeForMcc(mccMnc.mcc); } return countryCode; } @Nullable private static String countryCodeForMccMncNoFallback(MccMnc mccMnc) { synchronized (MccTable.class) { if (sTelephonyNetworkFinder == null) { sTelephonyNetworkFinder = TelephonyLookup.getInstance().getTelephonyNetworkFinder(); } } if (sTelephonyNetworkFinder == null) { // This should not happen under normal circumstances, only when the data is missing. return null; } TelephonyNetwork network = sTelephonyNetworkFinder.findNetworkByMccMnc(mccMnc.mcc, mccMnc.mnc); if (network == null) { return null; } return network.getCountryIsoCode(); } /** * Given a GSM Mobile Country Code, returns * an ISO 2-3 character language code if available. Loading tests/telephonytests/src/com/android/internal/telephony/MccTableTest.java +42 −20 Original line number Diff line number Diff line Loading @@ -16,33 +16,51 @@ package com.android.internal.telephony; import android.test.AndroidTestCase; import static org.junit.Assert.assertEquals; import android.content.Context; import android.test.suitebuilder.annotation.SmallTest; import org.junit.Ignore; import androidx.test.InstrumentationRegistry; import com.android.internal.telephony.MccTable.MccMnc; import org.junit.Test; import java.util.Locale; // TODO try using InstrumentationRegistry.getContext() instead of the default // AndroidTestCase context public class MccTableTest extends AndroidTestCase { private final static String LOG_TAG = "GSM"; public class MccTableTest { @SmallTest @Test public void testCountryCodeForMcc() throws Exception { checkMccLookupWithNoMnc("lu", 270); checkMccLookupWithNoMnc("gr", 202); checkMccLookupWithNoMnc("fk", 750); checkMccLookupWithNoMnc("mg", 646); checkMccLookupWithNoMnc("us", 314); checkMccLookupWithNoMnc("", 300); // mcc not defined, hence default checkMccLookupWithNoMnc("", 0); // mcc not defined, hence default checkMccLookupWithNoMnc("", 2000); // mcc not defined, hence default } private void checkMccLookupWithNoMnc(String expectedCountryIsoCode, int mcc) { assertEquals(expectedCountryIsoCode, MccTable.countryCodeForMcc(mcc)); assertEquals(expectedCountryIsoCode, MccTable.countryCodeForMcc(mcc)); assertEquals(expectedCountryIsoCode, MccTable.countryCodeForMcc("" + mcc)); assertEquals(expectedCountryIsoCode, MccTable.geoCountryCodeForMccMnc(new MccMnc("" + mcc, "999"))); } @SmallTest @Ignore public void testCountryCode() throws Exception { assertEquals("lu", MccTable.countryCodeForMcc(270)); assertEquals("gr", MccTable.countryCodeForMcc(202)); assertEquals("fk", MccTable.countryCodeForMcc(750)); assertEquals("mg", MccTable.countryCodeForMcc(646)); assertEquals("us", MccTable.countryCodeForMcc(314)); assertEquals("", MccTable.countryCodeForMcc(300)); // mcc not defined, hence default assertEquals("", MccTable.countryCodeForMcc(0)); // mcc not defined, hence default assertEquals("", MccTable.countryCodeForMcc(2000)); // mcc not defined, hence default @Test public void testGeoCountryCodeForMccMnc() throws Exception { // This test is possibly fragile as this data is configurable. assertEquals("gu", MccTable.geoCountryCodeForMccMnc(new MccMnc("310", "370"))); } @SmallTest @Ignore @Test public void testLang() throws Exception { assertEquals("en", MccTable.defaultLanguageForMcc(311)); assertEquals("de", MccTable.defaultLanguageForMcc(232)); Loading @@ -54,7 +72,7 @@ public class MccTableTest extends AndroidTestCase { } @SmallTest @Ignore @Test public void testLang_India() throws Exception { assertEquals("en", MccTable.defaultLanguageForMcc(404)); assertEquals("en", MccTable.defaultLanguageForMcc(405)); Loading @@ -62,7 +80,7 @@ public class MccTableTest extends AndroidTestCase { } @SmallTest @Ignore @Test public void testLocale() throws Exception { assertEquals(Locale.forLanguageTag("en-CA"), MccTable.getLocaleFromMcc(getContext(), 302, null)); Loading @@ -78,8 +96,12 @@ public class MccTableTest extends AndroidTestCase { MccTable.getLocaleFromMcc(getContext(), 466, null)); } private Context getContext() { return InstrumentationRegistry.getContext(); } @SmallTest @Ignore @Test public void testSmDigits() throws Exception { assertEquals(3, MccTable.smallestDigitsMccForMnc(312)); assertEquals(2, MccTable.smallestDigitsMccForMnc(430)); Loading Loading
src/java/com/android/internal/telephony/LocaleTracker.java +108 −26 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ import android.text.TextUtils; import android.util.LocalLog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.MccTable.MccMnc; import com.android.internal.util.IndentingPrintWriter; import java.io.FileDescriptor; Loading Loading @@ -282,23 +283,16 @@ public class LocaleTracker extends Handler { private String getMccFromCellInfo() { String selectedMcc = null; if (mCellInfoList != null) { Map<String, Integer> countryCodeMap = new HashMap<>(); Map<String, Integer> mccMap = new HashMap<>(); int maxCount = 0; for (CellInfo cellInfo : mCellInfoList) { String mcc = null; if (cellInfo instanceof CellInfoGsm) { mcc = ((CellInfoGsm) cellInfo).getCellIdentity().getMccString(); } else if (cellInfo instanceof CellInfoLte) { mcc = ((CellInfoLte) cellInfo).getCellIdentity().getMccString(); } else if (cellInfo instanceof CellInfoWcdma) { mcc = ((CellInfoWcdma) cellInfo).getCellIdentity().getMccString(); } String mcc = getNetworkMcc(cellInfo); if (mcc != null) { int count = 1; if (countryCodeMap.containsKey(mcc)) { count = countryCodeMap.get(mcc) + 1; if (mccMap.containsKey(mcc)) { count = mccMap.get(mcc) + 1; } countryCodeMap.put(mcc, count); mccMap.put(mcc, count); // This is unlikely, but if MCC from cell info looks different, we choose the // MCC that occurs most. if (count > maxCount) { Loading @@ -311,6 +305,67 @@ public class LocaleTracker extends Handler { return selectedMcc; } /** * Get the most frequent MCC + MNC combination with the specified MCC using cell tower * information. If no one combination is more frequent than any other an arbitrary MCC + MNC is * returned with the matching MCC. The MNC value returned can be null if it is not provided by * the cell tower information. * * @param mccToMatch the MCC to match * @return a matching {@link MccMnc}. Null if the information is not available. */ @Nullable private MccMnc getMccMncFromCellInfo(String mccToMatch) { MccMnc selectedMccMnc = null; if (mCellInfoList != null) { Map<MccMnc, Integer> mccMncMap = new HashMap<>(); int maxCount = 0; for (CellInfo cellInfo : mCellInfoList) { String mcc = getNetworkMcc(cellInfo); if (Objects.equals(mcc, mccToMatch)) { String mnc = getNetworkMnc(cellInfo); MccMnc mccMnc = new MccMnc(mcc, mnc); int count = 1; if (mccMncMap.containsKey(mccMnc)) { count = mccMncMap.get(mccMnc) + 1; } mccMncMap.put(mccMnc, count); // This is unlikely, but if MCC from cell info looks different, we choose the // MCC that occurs most. if (count > maxCount) { maxCount = count; selectedMccMnc = mccMnc; } } } } return selectedMccMnc; } private static String getNetworkMcc(CellInfo cellInfo) { String mccString = null; if (cellInfo instanceof CellInfoGsm) { mccString = ((CellInfoGsm) cellInfo).getCellIdentity().getMccString(); } else if (cellInfo instanceof CellInfoLte) { mccString = ((CellInfoLte) cellInfo).getCellIdentity().getMccString(); } else if (cellInfo instanceof CellInfoWcdma) { mccString = ((CellInfoWcdma) cellInfo).getCellIdentity().getMccString(); } return mccString; } private static String getNetworkMnc(CellInfo cellInfo) { String mccString = null; if (cellInfo instanceof CellInfoGsm) { mccString = ((CellInfoGsm) cellInfo).getCellIdentity().getMncString(); } else if (cellInfo instanceof CellInfoLte) { mccString = ((CellInfoLte) cellInfo).getCellIdentity().getMncString(); } else if (cellInfo instanceof CellInfoWcdma) { mccString = ((CellInfoWcdma) cellInfo).getCellIdentity().getMncString(); } return mccString; } /** * Called when SIM card state changed. Only when we absolutely know the SIM is absent, we get * cell info from the network. Other SIM states like NOT_READY might be just a transitioning Loading Loading @@ -457,15 +512,23 @@ public class LocaleTracker extends Handler { String countryIso = getCarrierCountry(); String countryIsoDebugInfo = "getCarrierCountry()"; // For time zone detection we want the best geographical match we can get, which may differ // from the countryIso. String timeZoneCountryIso = null; String timeZoneCountryIsoDebugInfo = null; if (!TextUtils.isEmpty(mOperatorNumeric)) { try { String mcc = mOperatorNumeric.substring(0, 3); countryIso = MccTable.countryCodeForMcc(mcc); MccMnc mccMnc = MccMnc.fromOperatorNumeric(mOperatorNumeric); if (mccMnc != null) { countryIso = MccTable.countryCodeForMcc(mccMnc.mcc); countryIsoDebugInfo = "OperatorNumeric(" + mOperatorNumeric + "): MccTable.countryCodeForMcc(\"" + mcc + "\")"; } catch (StringIndexOutOfBoundsException ex) { + "): MccTable.countryCodeForMcc(\"" + mccMnc.mcc + "\")"; timeZoneCountryIso = MccTable.geoCountryCodeForMccMnc(mccMnc); timeZoneCountryIsoDebugInfo = "OperatorNumeric: MccTable.geoCountryCodeForMccMnc(" + mccMnc + ")"; } else { loge("updateLocale: Can't get country from operator numeric. mOperatorNumeric = " + mOperatorNumeric + ". ex=" + ex); + mOperatorNumeric); } } Loading @@ -475,19 +538,27 @@ public class LocaleTracker extends Handler { String mcc = getMccFromCellInfo(); countryIso = MccTable.countryCodeForMcc(mcc); countryIsoDebugInfo = "CellInfo: MccTable.countryCodeForMcc(\"" + mcc + "\")"; MccMnc mccMnc = getMccMncFromCellInfo(mcc); if (mccMnc != null) { timeZoneCountryIso = MccTable.geoCountryCodeForMccMnc(mccMnc); timeZoneCountryIsoDebugInfo = "CellInfo: MccTable.geoCountryCodeForMccMnc(" + mccMnc + ")"; } } if (mCountryOverride != null) { countryIso = mCountryOverride; countryIsoDebugInfo = "mCountryOverride = \"" + mCountryOverride + "\""; log("Override current country to " + mCountryOverride); timeZoneCountryIso = countryIso; timeZoneCountryIsoDebugInfo = countryIsoDebugInfo; } log("updateLocale: countryIso = " + countryIso + ", countryIsoDebugInfo = " + countryIsoDebugInfo); if (!Objects.equals(countryIso, mCurrentCountryIso)) { String msg = "updateLocale: Change the current country to \"" + countryIso + "\", countryIsoDebugInfo = " + countryIsoDebugInfo String msg = "updateLocale: Change the current country to \"" + countryIso + "\"" + ", countryIsoDebugInfo = " + countryIsoDebugInfo + ", mCellInfoList = " + mCellInfoList; log(msg); mLocalLog.log(msg); Loading @@ -502,19 +573,30 @@ public class LocaleTracker extends Handler { mPhone.getContext().sendBroadcast(intent); } // For a test cell, the NitzStateMachine requires handleCountryDetected("") to pass // compliance tests. http://b/142840879 // Pass the geographical country information to the telephony time zone detection code. boolean isTestMcc = false; if (!TextUtils.isEmpty(mOperatorNumeric)) { // For a test cell (MCC 001), the NitzStateMachine requires handleCountryDetected("") in // order to pass compliance tests. http://b/142840879 if (mOperatorNumeric.startsWith("001")) { isTestMcc = true; countryIso = ""; timeZoneCountryIso = ""; timeZoneCountryIsoDebugInfo = "Test cell: " + mOperatorNumeric; } } if (TextUtils.isEmpty(countryIso) && !isTestMcc) { if (timeZoneCountryIso == null) { // After this timeZoneCountryIso may still be null. timeZoneCountryIso = countryIso; timeZoneCountryIsoDebugInfo = "Defaulted: " + countryIsoDebugInfo; } log("updateLocale: timeZoneCountryIso = " + timeZoneCountryIso + ", timeZoneCountryIsoDebugInfo = " + timeZoneCountryIsoDebugInfo); if (TextUtils.isEmpty(timeZoneCountryIso) && !isTestMcc) { mNitzStateMachine.handleCountryUnavailable(); } else { mNitzStateMachine.handleCountryDetected(countryIso); mNitzStateMachine.handleCountryDetected(timeZoneCountryIso); } } Loading
src/java/com/android/internal/telephony/MccTable.java +137 −9 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.internal.telephony; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; import android.app.ActivityManager; import android.content.Context; Loading @@ -27,9 +29,15 @@ import android.os.SystemProperties; import android.text.TextUtils; import android.util.Slog; 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 libcore.timezone.TelephonyLookup; import libcore.timezone.TelephonyNetwork; import libcore.timezone.TelephonyNetworkFinder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; Loading @@ -37,6 +45,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; /** * Mobile Country Code Loading @@ -46,6 +55,9 @@ import java.util.Map; public final class MccTable { static final String LOG_TAG = "MccTable"; @GuardedBy("MccTable.class") private static TelephonyNetworkFinder sTelephonyNetworkFinder; static ArrayList<MccEntry> sTable; static class MccEntry implements Comparable<MccEntry> { Loading @@ -58,11 +70,11 @@ public final class MccTable { final String mIso; final int mSmallestDigitsMnc; MccEntry(int mnc, String iso, int smallestDigitsMCC) { MccEntry(int mcc, String iso, int smallestDigitsMCC) { if (iso == null) { throw new NullPointerException(); } mMcc = mnc; mMcc = mcc; mIso = iso; mSmallestDigitsMnc = smallestDigitsMCC; } Loading @@ -73,6 +85,77 @@ public final class MccTable { } } /** * A combination of MCC and MNC. The MNC is optional and may be null. * * @hide */ @VisibleForTesting public static class MccMnc { @NonNull public final String mcc; @Nullable public final String mnc; /** * Splits the supplied String in two: the first three characters are treated as the MCC, * the remaining characters are treated as the MNC. */ @Nullable public static MccMnc fromOperatorNumeric(@NonNull String operatorNumeric) { Objects.requireNonNull(operatorNumeric); String mcc; try { mcc = operatorNumeric.substring(0, 3); } catch (StringIndexOutOfBoundsException e) { return null; } String mnc; try { mnc = operatorNumeric.substring(3); } catch (StringIndexOutOfBoundsException e) { mnc = null; } return new MccMnc(mcc, mnc); } /** * Creates an MccMnc using the supplied values. */ public MccMnc(@NonNull String mcc, @Nullable String mnc) { this.mcc = Objects.requireNonNull(mcc); this.mnc = mnc; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } MccMnc mccMnc = (MccMnc) o; return mcc.equals(mccMnc.mcc) && Objects.equals(mnc, mccMnc.mnc); } @Override public int hashCode() { return Objects.hash(mcc, mnc); } @Override public String toString() { return "MccMnc{" + "mcc='" + mcc + '\'' + ", mnc='" + mnc + '\'' + '}'; } } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "There is no alternative for {@code MccTable.entryForMcc}, " + "but it was included in hidden APIs due to a static analysis false positive " Loading @@ -91,11 +174,11 @@ public final class MccTable { } /** * Given a GSM Mobile Country Code, returns * an ISO two-character country code if available. * Returns "" if unavailable. * Given a GSM Mobile Country Code, returns a lower-case ISO 3166 alpha-2 country code if * available. Returns empty string if unavailable. */ @UnsupportedAppUsage @NonNull public static String countryCodeForMcc(int mcc) { MccEntry entry = entryForMcc(mcc); Loading @@ -107,11 +190,11 @@ public final class MccTable { } /** * Given a GSM Mobile Country Code, returns * an ISO two-character country code if available. * Returns empty string if unavailable. * Given a GSM Mobile Country Code, returns a lower-case ISO 3166 alpha-2 country code if * available. Returns empty string if unavailable. */ public static String countryCodeForMcc(String mcc) { @NonNull public static String countryCodeForMcc(@NonNull String mcc) { try { return countryCodeForMcc(Integer.parseInt(mcc)); } catch (NumberFormatException ex) { Loading @@ -119,6 +202,51 @@ public final class MccTable { } } /** * Given a combination of MCC and MNC, returns a lower case ISO 3166 alpha-2 country code for * the device's geographical location. * * <p>This can give a better geographical result than {@link #countryCodeForMcc(String)} * (which provides the official "which country is the MCC assigned to?" answer) for cases when * MNC is also available: Sometimes an MCC can be used by multiple countries and the MNC can * help distinguish, or the MCC assigned to a country isn't used for geopolitical reasons. * When the geographical country is needed (e.g. time zone detection) this version can provide * more pragmatic results than the official MCC-only answer. This method falls back to calling * {@link #countryCodeForMcc(int)} if no special MCC+MNC cases are found. * Returns empty string if no code can be determined. */ @NonNull public static String geoCountryCodeForMccMnc(@NonNull MccMnc mccMnc) { String countryCode = null; if (mccMnc.mnc != null) { countryCode = countryCodeForMccMncNoFallback(mccMnc); } if (TextUtils.isEmpty(countryCode)) { // Try the MCC-only fallback. countryCode = MccTable.countryCodeForMcc(mccMnc.mcc); } return countryCode; } @Nullable private static String countryCodeForMccMncNoFallback(MccMnc mccMnc) { synchronized (MccTable.class) { if (sTelephonyNetworkFinder == null) { sTelephonyNetworkFinder = TelephonyLookup.getInstance().getTelephonyNetworkFinder(); } } if (sTelephonyNetworkFinder == null) { // This should not happen under normal circumstances, only when the data is missing. return null; } TelephonyNetwork network = sTelephonyNetworkFinder.findNetworkByMccMnc(mccMnc.mcc, mccMnc.mnc); if (network == null) { return null; } return network.getCountryIsoCode(); } /** * Given a GSM Mobile Country Code, returns * an ISO 2-3 character language code if available. Loading
tests/telephonytests/src/com/android/internal/telephony/MccTableTest.java +42 −20 Original line number Diff line number Diff line Loading @@ -16,33 +16,51 @@ package com.android.internal.telephony; import android.test.AndroidTestCase; import static org.junit.Assert.assertEquals; import android.content.Context; import android.test.suitebuilder.annotation.SmallTest; import org.junit.Ignore; import androidx.test.InstrumentationRegistry; import com.android.internal.telephony.MccTable.MccMnc; import org.junit.Test; import java.util.Locale; // TODO try using InstrumentationRegistry.getContext() instead of the default // AndroidTestCase context public class MccTableTest extends AndroidTestCase { private final static String LOG_TAG = "GSM"; public class MccTableTest { @SmallTest @Test public void testCountryCodeForMcc() throws Exception { checkMccLookupWithNoMnc("lu", 270); checkMccLookupWithNoMnc("gr", 202); checkMccLookupWithNoMnc("fk", 750); checkMccLookupWithNoMnc("mg", 646); checkMccLookupWithNoMnc("us", 314); checkMccLookupWithNoMnc("", 300); // mcc not defined, hence default checkMccLookupWithNoMnc("", 0); // mcc not defined, hence default checkMccLookupWithNoMnc("", 2000); // mcc not defined, hence default } private void checkMccLookupWithNoMnc(String expectedCountryIsoCode, int mcc) { assertEquals(expectedCountryIsoCode, MccTable.countryCodeForMcc(mcc)); assertEquals(expectedCountryIsoCode, MccTable.countryCodeForMcc(mcc)); assertEquals(expectedCountryIsoCode, MccTable.countryCodeForMcc("" + mcc)); assertEquals(expectedCountryIsoCode, MccTable.geoCountryCodeForMccMnc(new MccMnc("" + mcc, "999"))); } @SmallTest @Ignore public void testCountryCode() throws Exception { assertEquals("lu", MccTable.countryCodeForMcc(270)); assertEquals("gr", MccTable.countryCodeForMcc(202)); assertEquals("fk", MccTable.countryCodeForMcc(750)); assertEquals("mg", MccTable.countryCodeForMcc(646)); assertEquals("us", MccTable.countryCodeForMcc(314)); assertEquals("", MccTable.countryCodeForMcc(300)); // mcc not defined, hence default assertEquals("", MccTable.countryCodeForMcc(0)); // mcc not defined, hence default assertEquals("", MccTable.countryCodeForMcc(2000)); // mcc not defined, hence default @Test public void testGeoCountryCodeForMccMnc() throws Exception { // This test is possibly fragile as this data is configurable. assertEquals("gu", MccTable.geoCountryCodeForMccMnc(new MccMnc("310", "370"))); } @SmallTest @Ignore @Test public void testLang() throws Exception { assertEquals("en", MccTable.defaultLanguageForMcc(311)); assertEquals("de", MccTable.defaultLanguageForMcc(232)); Loading @@ -54,7 +72,7 @@ public class MccTableTest extends AndroidTestCase { } @SmallTest @Ignore @Test public void testLang_India() throws Exception { assertEquals("en", MccTable.defaultLanguageForMcc(404)); assertEquals("en", MccTable.defaultLanguageForMcc(405)); Loading @@ -62,7 +80,7 @@ public class MccTableTest extends AndroidTestCase { } @SmallTest @Ignore @Test public void testLocale() throws Exception { assertEquals(Locale.forLanguageTag("en-CA"), MccTable.getLocaleFromMcc(getContext(), 302, null)); Loading @@ -78,8 +96,12 @@ public class MccTableTest extends AndroidTestCase { MccTable.getLocaleFromMcc(getContext(), 466, null)); } private Context getContext() { return InstrumentationRegistry.getContext(); } @SmallTest @Ignore @Test public void testSmDigits() throws Exception { assertEquals(3, MccTable.smallestDigitsMccForMnc(312)); assertEquals(2, MccTable.smallestDigitsMccForMnc(430)); Loading