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

Commit 06755a77 authored by Neil Fuller's avatar Neil Fuller Committed by Gerrit Code Review
Browse files

Merge "Improve time zone detection by leveraging MNC info"

parents 5dcca67d f8cc2cb8
Loading
Loading
Loading
Loading
+108 −26
Original line number Diff line number Diff line
@@ -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;
@@ -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) {
@@ -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
@@ -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);
            }
        }

@@ -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);
@@ -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);
        }
    }

+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));