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

Commit 758a3446 authored by Neil Fuller's avatar Neil Fuller
Browse files

Move time zone lookup logic

Move time zone lookup logic into a separate TimeZoneLookupHelper which
can be independently tested and improved. Tests for all methods there
have been added / migrated & improved.

This also allows the lookup behavior to be mocked for NitzStateMachineTest
which has been adjusted accordingly. An additional test has been added
to demonstrate.

TimeStampedValue has been moved to a top-level class under util so
it can be used more widely and from tests.

Test: atest FrameworksTelephonyTests
Bug: 63743683
Change-Id: I2ff15b1355d5ba690198c5943bcb0faed17d8210
Merged-In: I2ff15b1355d5ba690198c5943bcb0faed17d8210
(cherry picked from commit a0f09cee)
parent d28dd923
Loading
Loading
Loading
Loading
+4 −44
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import android.telephony.Rlog;
import com.android.internal.annotations.VisibleForTesting;

import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

/**
@@ -35,7 +34,6 @@ import java.util.TimeZone;
@VisibleForTesting(visibility = PACKAGE)
public final class NitzData {
    private static final String LOG_TAG = ServiceStateTracker.LOG_TAG;
    private static final int MS_PER_HOUR = 60 * 60 * 1000;
    private static final int MS_PER_QUARTER_HOUR = 15 * 60 * 1000;

    /* Time stamp after 19 January 2038 is not supported under 32 bit */
@@ -53,7 +51,7 @@ public final class NitzData {
    private final TimeZone mEmulatorHostTimeZone;

    private NitzData(String originalString, int zoneOffsetMillis, Integer dstOffsetMillis,
            long utcTimeMillis, TimeZone timeZone) {
            long utcTimeMillis, TimeZone emulatorHostTimeZone) {
        if (originalString == null) {
            throw new NullPointerException("originalString==null");
        }
@@ -61,7 +59,7 @@ public final class NitzData {
        this.mZoneOffset = zoneOffsetMillis;
        this.mDstOffset = dstOffsetMillis;
        this.mCurrentTimeMillis = utcTimeMillis;
        this.mEmulatorHostTimeZone = timeZone;
        this.mEmulatorHostTimeZone = emulatorHostTimeZone;
    }

    /**
@@ -139,9 +137,9 @@ public final class NitzData {

    /** A method for use in tests to create NitzData instances. */
    public static NitzData createForTests(int zoneOffsetMillis, Integer dstOffsetMillis,
            long utcTimeMillis, TimeZone timeZone) {
            long utcTimeMillis, TimeZone emulatorHostTimeZone) {
        return new NitzData("Test data", zoneOffsetMillis, dstOffsetMillis, utcTimeMillis,
                timeZone);
                emulatorHostTimeZone);
    }

    /**
@@ -187,44 +185,6 @@ public final class NitzData {
        return mEmulatorHostTimeZone;
    }

    /**
     * Using information present in the supplied {@link NitzData} object, guess the time zone.
     * Because multiple time zones can have the same offset / DST state at a given time this process
     * is error prone; an arbitrary match is returned when there are multiple candidates. The
     * algorithm can also return a non-exact match by assuming that the DST information provided by
     * NITZ is incorrect. This method can return {@code null} if no time zones are found.
     */
    public static TimeZone guessTimeZone(NitzData nitzData) {
        int offset = nitzData.getLocalOffsetMillis();
        boolean dst = nitzData.isDst();
        long when = nitzData.getCurrentTimeInMillis();
        TimeZone guess = findTimeZone(offset, dst, when);
        if (guess == null) {
            // Couldn't find a proper timezone.  Perhaps the DST data is wrong.
            guess = findTimeZone(offset, !dst, when);
        }
        return guess;
    }

    private static TimeZone findTimeZone(int offset, boolean dst, long when) {
        int rawOffset = offset;
        if (dst) {
            rawOffset -= MS_PER_HOUR;
        }
        String[] zones = TimeZone.getAvailableIDs(rawOffset);
        TimeZone guess = null;
        Date d = new Date(when);
        for (String zone : zones) {
            TimeZone tz = TimeZone.getTimeZone(zone);
            if (tz.getOffset(when) == offset && tz.inDaylightTime(d) == dst) {
                guess = tz;
                break;
            }
        }

        return guess;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
+42 −100
Original line number Diff line number Diff line
@@ -30,12 +30,11 @@ import android.util.TimeUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.telephony.util.TimeStampedValue;
import com.android.internal.util.IndentingPrintWriter;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import java.util.TimeZone;

/**
@@ -117,63 +116,9 @@ public class NitzStateMachine {
        }
    }

    /** A pair containing a value and an associated time stamp. */
    private static class TimeStampedValue<T> {
        /** The value. */
        final T mValue;

        /**
         * The value of {@link SystemClock#elapsedRealtime} or equivalent when value was
         * determined.
         */
        final long mElapsedRealtime;

        TimeStampedValue(T value, long elapsedRealtime) {
            this.mValue = value;
            this.mElapsedRealtime = elapsedRealtime;
        }

        @Override
        public String toString() {
            return "TimeStampedValue{"
                    + "mValue=" + mValue
                    + ", mElapsedRealtime=" + mElapsedRealtime
                    + '}';
        }
    }

    private static final String LOG_TAG = ServiceStateTracker.LOG_TAG;
    private static final boolean DBG = ServiceStateTracker.DBG;

    /**
     * List of ISO codes for countries that can have an offset of
     * GMT+0 when not in daylight savings time.  This ignores some
     * small places such as the Canary Islands (Spain) and
     * Danmarkshavn (Denmark).  The list must be sorted by code.
     */
    private static final String[] GMT_COUNTRY_CODES = {
            "bf", // Burkina Faso
            "ci", // Cote d'Ivoire
            "eh", // Western Sahara
            "fo", // Faroe Islands, Denmark
            "gb", // United Kingdom of Great Britain and Northern Ireland
            "gh", // Ghana
            "gm", // Gambia
            "gn", // Guinea
            "gw", // Guinea Bissau
            "ie", // Ireland
            "lr", // Liberia
            "is", // Iceland
            "ma", // Morocco
            "ml", // Mali
            "mr", // Mauritania
            "pt", // Portugal
            "sl", // Sierra Leone
            "sn", // Senegal
            "st", // Sao Tome and Principe
            "tg", // Togo
    };

    // Time detection state.

    /**
@@ -212,6 +157,7 @@ public class NitzStateMachine {
    private final GsmCdmaPhone mPhone;
    private final DeviceState mDeviceState;
    private final TimeServiceHelper mTimeServiceHelper;
    private final TimeZoneLookupHelper mTimeZoneLookupHelper;
    /** Wake lock used while setting time of day. */
    private final PowerManager.WakeLock mWakeLock;
    private static final String WAKELOCK_TAG = "NitzStateMachine";
@@ -219,12 +165,13 @@ public class NitzStateMachine {
    public NitzStateMachine(GsmCdmaPhone phone) {
        this(phone,
                TelephonyComponentFactory.getInstance().makeTimeServiceHelper(phone.getContext()),
                new DeviceState(phone));
                new DeviceState(phone),
                new TimeZoneLookupHelper());
    }

    @VisibleForTesting
    public NitzStateMachine(GsmCdmaPhone phone, TimeServiceHelper timeServiceHelper,
            DeviceState deviceState) {
            DeviceState deviceState, TimeZoneLookupHelper timeZoneLookupHelper) {
        mPhone = phone;

        Context context = phone.getContext();
@@ -233,6 +180,7 @@ public class NitzStateMachine {
        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);

        mDeviceState = deviceState;
        mTimeZoneLookupHelper = timeZoneLookupHelper;
        mTimeServiceHelper = timeServiceHelper;
        mTimeServiceHelper.setListener(new TimeServiceHelper.Listener() {
            @Override
@@ -266,23 +214,22 @@ public class NitzStateMachine {
                    + " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
                    + " mNitzData=" + mNitzData
                    + " iso-cc='" + isoCountryCode
                    + "' iso-cc-idx=" + Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode));
                    + "' countryUsesUtc=" + mTimeZoneLookupHelper.countryUsesUtc(isoCountryCode));
        }
        TimeZone zone;
        String zoneId;
        if ("".equals(isoCountryCode) && mNeedFixZoneAfterNitz) {
            // Country code not found.  This is likely a test network.
            // Get a TimeZone based only on the NITZ parameters (best guess).

            // mNeedFixZoneAfterNitz is only set to true when mNitzData is set so there's no need to
            // check mNitzData == null.
            zone = NitzData.guessTimeZone(mNitzData);
            zoneId = mTimeZoneLookupHelper.guessZoneIdByNitz(mNitzData);
            if (DBG) {
                Rlog.d(LOG_TAG, "fixTimeZone(): guessNitzTimeZone returned "
                        + (zone == null ? zone : zone.getID()));
                Rlog.d(LOG_TAG, "fixTimeZone(): guessNitzTimeZone returned " + zoneId);
            }
        } else if ((mNitzData == null || nitzOffsetMightBeBogus(mNitzData))
                && isTimeZoneSettingInitialized
                && (Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode) < 0)) {
                && !mTimeZoneLookupHelper.countryUsesUtc(isoCountryCode)) {

            // This case means that (1) the device received no NITZ signal yet or received an NITZ
            // signal that looks bogus due to having a zero offset from UTC, (2) the device has a
@@ -290,7 +237,8 @@ public class NitzStateMachine {
            // zero offset. This is interpreted as being NITZ incorrectly reporting a local time and
            // not a UTC time. The zone is left as the current device's zone setting, and the time
            // may be adjusted by assuming the current zone setting is correct.
            zone = TimeZone.getDefault();
            TimeZone zone = TimeZone.getDefault();
            zoneId = zone.getID();

            // Note that mNeedFixZoneAfterNitz => (implies) { mNitzData != null }. Therefore, if
            // mNitzData == null, mNeedFixZoneAfterNitz cannot be true. The code in this section
@@ -330,15 +278,14 @@ public class NitzStateMachine {
        } else if (mNitzData == null) {
            // The use of 1/1/1970 UTC is unusual but consistent with historical behavior when
            // it wasn't possible to detect whether a previous NITZ signal had been saved.
            zone = TimeUtils.getTimeZone(0 /* offset */, false /* dst */, 0 /* when */,
                    isoCountryCode);
            zoneId = mTimeZoneLookupHelper.guessZoneIdByInstantOffsetDstCountry(
                    0 /* when */, 0 /* offset */, false /* dst */, isoCountryCode);
            if (DBG) {
                Rlog.d(LOG_TAG, "fixTimeZone: No cached NITZ data available, using only country"
                        + " code. zone=" + zone);
                        + " code. zone=" + zoneId);
            }
        } else {
            zone = TimeUtils.getTimeZone(mNitzData.getLocalOffsetMillis(), mNitzData.isDst(),
                    mNitzData.getCurrentTimeInMillis(), isoCountryCode);
            zoneId = mTimeZoneLookupHelper.guessZoneIdByNitzCountry(mNitzData, isoCountryCode);
            if (DBG) {
                Rlog.d(LOG_TAG, "fixTimeZone: using getTimeZone(off, dst, time, iso)");
            }
@@ -348,18 +295,18 @@ public class NitzStateMachine {
                + " mNitzData=" + mNitzData
                + " iso-cc=" + isoCountryCode
                + " mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz
                + " zone=" + (zone != null ? zone.getID() : "NULL");
                + " zoneId=" + zoneId;
        mTimeZoneLog.log(tmpLog);

        if (zone != null) {
            Rlog.d(LOG_TAG, "fixTimeZone: zone != null zone.getID=" + zone.getID());
        if (zoneId != null) {
            Rlog.d(LOG_TAG, "fixTimeZone: zone != null zoneId=" + zoneId);
            if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
                setAndBroadcastNetworkSetTimeZone(zone.getID());
                setAndBroadcastNetworkSetTimeZone(zoneId);
            } else {
                Rlog.d(LOG_TAG, "fixTimeZone: skip changing zone as getAutoTimeZone was false");
            }
            if (mNeedFixZoneAfterNitz) {
                saveNitzTimeZone(zone.getID());
                saveNitzTimeZone(zoneId);
            }
        } else {
            Rlog.d(LOG_TAG, "fixTimeZone: zone == null, do nothing for zone");
@@ -389,27 +336,22 @@ public class NitzStateMachine {
    private void setTimeZoneFromNitz(NitzData newNitzData, long nitzReceiveTime) {
        try {
            String iso = mDeviceState.getNetworkCountryIsoForPhone();
            TimeZone zone;
            String zoneId;
            if (newNitzData.getEmulatorHostTimeZone() != null) {
                zone = newNitzData.getEmulatorHostTimeZone();
                zoneId = newNitzData.getEmulatorHostTimeZone().getID();
            } else {
                if (!mGotCountryCode) {
                    zone = null;
                    zoneId = null;
                } else if (iso != null && iso.length() > 0) {
                    zone = TimeUtils.getTimeZone(
                            newNitzData.getLocalOffsetMillis(),
                            newNitzData.isDst(),
                            newNitzData.getCurrentTimeInMillis(),
                            iso);
                    zoneId = mTimeZoneLookupHelper.guessZoneIdByNitzCountry(newNitzData, iso);
                } else {
                    // We don't have a valid iso country code.  This is
                    // most likely because we're on a test network that's
                    // using a bogus MCC (eg, "001"), so get a TimeZone
                    // based only on the NITZ parameters.
                    zone = NitzData.guessTimeZone(newNitzData);
                    zoneId = mTimeZoneLookupHelper.guessZoneIdByNitz(newNitzData);
                    if (DBG) {
                        Rlog.d(LOG_TAG, "setTimeFromNITZ(): guessNitzTimeZone returned "
                                + (zone == null ? zone : zone.getID()));
                        Rlog.d(LOG_TAG, "setTimeZoneFromNitz(): findByNitz returned " + zoneId);
                    }
                }
            }
@@ -425,7 +367,7 @@ public class NitzStateMachine {
                previousUtcOffset = mNitzData.getLocalOffsetMillis();
                previousIsDst = mNitzData.isDst();
            }
            if ((zone == null)
            if ((zoneId == null)
                    || (newNitzData.getLocalOffsetMillis() != previousUtcOffset)
                    || (newNitzData.isDst() != previousIsDst)) {
                // We got the time before the country or the zone has changed
@@ -437,7 +379,7 @@ public class NitzStateMachine {

            String tmpLog = "NITZ: newNitzData=" + newNitzData
                    + " nitzReceiveTime=" + nitzReceiveTime
                    + " zone=" + (zone != null ? zone.getID() : "NULL")
                    + " zoneId=" + zoneId
                    + " iso=" + iso + " mGotCountryCode=" + mGotCountryCode
                    + " mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz
                    + " isTimeZoneDetectionEnabled()="
@@ -447,12 +389,12 @@ public class NitzStateMachine {
            }
            mTimeZoneLog.log(tmpLog);

            if (zone != null) {
            if (zoneId != null) {
                if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
                    setAndBroadcastNetworkSetTimeZone(zone.getID());
                    setAndBroadcastNetworkSetTimeZone(zoneId);
                }
                mNitzTimeZoneDetectionSuccessful = true;
                saveNitzTimeZone(zone.getID());
                saveNitzTimeZone(zoneId);
            }
        } catch (RuntimeException ex) {
            Rlog.e(LOG_TAG, "NITZ: Processing NITZ data " + newNitzData + " ex=" + ex);
@@ -642,27 +584,27 @@ public class NitzStateMachine {
    }

    /**
     * Update time zone by network country code, works on countries which only have one time zone.
     * Update time zone by network country code, works well on countries which only have one time
     * zone or multiple zones with the same offset.
     *
     * @param iso Country code from network MCC
     */
    public void updateTimeZoneByNetworkCountryCode(String iso) {
        List<String> uniqueZoneIds = TimeUtils.getTimeZoneIdsWithUniqueOffsets(iso);
        if (uniqueZoneIds.size() == 1) {
            String zoneId = uniqueZoneIds.get(0);
        String zoneId = mTimeZoneLookupHelper.guessZoneIdByCountry(
                iso, mDeviceState.currentTimeMillis());
        if (zoneId != null) {
            if (DBG) {
                Rlog.d(LOG_TAG, "updateTimeZoneByNetworkCountryCode: no nitz but one TZ for iso-cc="
                        + iso
                        + " with zone.getID=" + zoneId);
                Rlog.d(LOG_TAG, "updateTimeZoneByNetworkCountryCode: set time zone=" + zoneId
                        + " iso=" + iso);
            }

            mTimeZoneLog.log("updateTimeZoneByNetworkCountryCode: set time zone=" + zoneId
                    + " iso=" + iso);
            setAndBroadcastNetworkSetTimeZone(zoneId);
        } else {
            if (DBG) {
                Rlog.d(LOG_TAG,
                        "updateTimeZoneByNetworkCountryCode: there are " + uniqueZoneIds.size()
                                + " unique offsets for iso-cc='" + iso
                        "updateTimeZoneByNetworkCountryCode: no good zone for iso-cc='" + iso
                                + "', do nothing");
            }
        }
+1 −1
Original line number Diff line number Diff line
@@ -3092,7 +3092,7 @@ public class ServiceStateTracker extends Handler {
            if (lastNitzData == null) {
                tzone = null;
            } else {
                tzone = NitzData.guessTimeZone(lastNitzData);
                tzone = TimeZoneLookupHelper.guessZoneByNitzStatic(lastNitzData);
                if (ServiceStateTracker.DBG) {
                    log("fixUnknownMcc(): guessNitzTimeZone returned "
                            + (tzone == null ? tzone : tzone.getID()));
+173 −0
Original line number Diff line number Diff line
/*
 * Copyright 2017 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;

import android.util.TimeUtils;

import libcore.util.CountryTimeZones;
import libcore.util.TimeZoneFinder;

import java.util.Arrays;
import java.util.Date;
import java.util.TimeZone;

/**
 * An interface to various time zone lookup behaviors.
 */
// Non-final to allow mocking.
public class TimeZoneLookupHelper {
    private static final int MS_PER_HOUR = 60 * 60 * 1000;

    /**
     * List of ISO codes for countries that can have an offset of
     * GMT+0 when not in daylight savings time.  This ignores some
     * small places such as the Canary Islands (Spain) and
     * Danmarkshavn (Denmark).  The list must be sorted by code.
     */
    private static final String[] GMT_COUNTRY_CODES = {
            "bf", // Burkina Faso
            "ci", // Cote d'Ivoire
            "eh", // Western Sahara
            "fo", // Faroe Islands, Denmark
            "gb", // United Kingdom of Great Britain and Northern Ireland
            "gh", // Ghana
            "gm", // Gambia
            "gn", // Guinea
            "gw", // Guinea Bissau
            "ie", // Ireland
            "lr", // Liberia
            "is", // Iceland
            "ma", // Morocco
            "ml", // Mali
            "mr", // Mauritania
            "pt", // Portugal
            "sl", // Sierra Leone
            "sn", // Senegal
            "st", // Sao Tome and Principe
            "tg", // Togo
    };

    public TimeZoneLookupHelper() {}

    /**
     * Finds a time zone ID that fits the supplied NITZ and country information.
     *
     * <p><em>Note:</em> When there are multiple matching zones then one of the matching candidates
     * will be returned. If the current device default zone matches it will be returned in
     * preference to other candidates. This method can return {@code null} if no matching time
     * zones are found.
     */
    public String guessZoneIdByNitzCountry(NitzData nitzData, String isoCountryCode) {
        return guessZoneIdByInstantOffsetDstCountry(
                nitzData.getCurrentTimeInMillis(),
                nitzData.getLocalOffsetMillis(),
                nitzData.isDst(),
                isoCountryCode);
    }

    /**
     * Finds a time zone ID that fits the supplied time / offset and country information.
     *
     * <p><em>Note:</em> When there are multiple matching zones then one of the matching candidates
     * will be returned. If the current device default zone matches it will be returned in
     * preference to other candidates. This method can return {@code null} if no matching time
     * zones are found.
     */
    public String guessZoneIdByInstantOffsetDstCountry(
            long timeMillis, int utcOffsetMillis, boolean isDst, String isoCountryCode) {
        TimeZone timeZone =
                TimeUtils.getTimeZone(utcOffsetMillis, isDst, timeMillis, isoCountryCode);
        return timeZone == null ? null : timeZone.getID();
    }

    /**
     * Finds a time zone ID using only information present in the supplied {@link NitzData} object.
     *
     * <p><em>Note:</em> Because multiple time zones can have the same offset / DST state at a given
     * time this process is error prone; an arbitrary match is returned when there are multiple
     * candidates. The algorithm can also return a non-exact match by assuming that the DST
     * information provided by NITZ is incorrect. This method can return {@code null} if no matching
     * time zones are found.
     */
    public String guessZoneIdByNitz(NitzData nitzData) {
        TimeZone zone = guessZoneByNitzStatic(nitzData);
        return zone == null ? null : zone.getID();
    }

    /**
     * Returns a time zone ID for the country if possible. For counties that use a single time zone
     * this will provide a good choice. For countries with multiple time zones, a time zone is
     * returned if all time zones used in the country currently have the same offset (currently ==
     * according to the device's current system clock time). If this is not the case then
     * {@code null} can be returned.
     */
    public String guessZoneIdByCountry(String isoCountryCode, long whenMillis) {
        CountryTimeZones countryTimeZones =
                TimeZoneFinder.getInstance().lookupCountryTimeZones(isoCountryCode);
        if (countryTimeZones == null) {
            // Unknown country code.
            return null;
        }

        if (countryTimeZones.isDefaultOkForCountryTimeZoneDetection(whenMillis)) {
            return countryTimeZones.getDefaultTimeZoneId();
        }
        return null;
    }

    /** Static method for use by {@link ServiceStateTracker}. */
    static TimeZone guessZoneByNitzStatic(NitzData nitzData) {
        int utcOffsetMillis = nitzData.getLocalOffsetMillis();
        boolean isDst = nitzData.isDst();
        long timeMillis = nitzData.getCurrentTimeInMillis();

        TimeZone guess = guessByInstantOffsetDst(timeMillis, utcOffsetMillis, isDst);
        if (guess == null) {
            // Couldn't find a proper timezone.  Perhaps the DST data is wrong.
            guess = guessByInstantOffsetDst(timeMillis, utcOffsetMillis, !isDst);
        }
        return guess;
    }

    private static TimeZone guessByInstantOffsetDst(long timeMillis, int utcOffsetMillis,
            boolean isDst) {
        int rawOffset = utcOffsetMillis;
        if (isDst) {
            rawOffset -= MS_PER_HOUR;
        }
        String[] zones = TimeZone.getAvailableIDs(rawOffset);
        TimeZone guess = null;
        Date d = new Date(timeMillis);
        for (String zone : zones) {
            TimeZone tz = TimeZone.getTimeZone(zone);
            if (tz.getOffset(timeMillis) == utcOffsetMillis && tz.inDaylightTime(d) == isDst) {
                guess = tz;
                break;
            }
        }

        return guess;
    }

    /**
     * Returns {@code true} if the supplied (lower-case) ISO country code is for a country known to
     * use a raw offset of zero from UTC.
     */
    public boolean countryUsesUtc(String isoCountryCode) {
        return Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode) >= 0;
    }
}
+73 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading