Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b7d711b7 authored by Neil Fuller's avatar Neil Fuller Committed by android-build-merger
Browse files

Merge "Improve time zone detection by leveraging MNC info" am: 06755a77 am: 72d17e7c

am: ca4d6b6f

Change-Id: I50f955082d899d8015d48af48b0072b42f1804a2
parents 8a637382 ca4d6b6f
Loading
Loading
Loading
Loading
+108 −26
Original line number Diff line number Diff line
@@ -43,6 +43,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;
@@ -286,23 +287,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) {
@@ -315,6 +309,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
@@ -461,15 +516,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);
            }
        }

@@ -479,19 +542,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);
@@ -512,19 +583,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);
        }
    }

+137 −9
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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
@@ -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> {
@@ -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;
        }
@@ -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 "
@@ -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);

@@ -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) {
@@ -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.
+42 −20
Original line number Diff line number Diff line
@@ -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));
@@ -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));
@@ -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));
@@ -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));