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

Commit 1fef4399 authored by Neil Fuller's avatar Neil Fuller
Browse files

Reduce duplication of time zone detection logic

Over the a series of prior commits the time zone detection behavior
when receiving ...

(a) an NITZ signal then a country code
and
(b) a country code and then an NITZ signal

... has been nudged closer together. The two paths are now
effectively identical.

Now it is possible to deduplicate the two paths. This change
merges parts of handleNetworkCountryCodeSet() with
handleTimeZoneFromNitz() into a new private
updateTimeZoneFromCountryAndNitz() method that shares common
NITZ + country code time zone detection logic.

The logging has generally been tidied as part of this change.

More tests have been added to cover the cases when the device
is on a test network.

Bug: 78217059
Test: atest  FrameworksTelephonyTests:com.android.internal.telephony.NitzStateMachineTest
Change-Id: If485f441dd766f2140ce248b793b3a8770f00404
parent 171a84b6
Loading
Loading
Loading
Loading
+130 −170
Original line number Diff line number Diff line
@@ -119,10 +119,12 @@ public class NitzStateMachine {
    private TimeStampedValue<NitzData> mLatestNitzSignal;

    /**
     * Sometimes we get the NITZ time before we know what country we are in.  We can find the time
     * zone once we know the country by using {@link #mLatestNitzSignal}.
     * Records whether the device should have a country code available via
     * {@link DeviceState#getNetworkCountryIsoForPhone()}. Before this an NITZ signal
     * received is (almost always) not enough to determine time zone. On test networks the country
     * code should be available but can still be an empty string but this flag indicates that the
     * information available is unlikely to improve.
     */
    private boolean mNeedCountryCodeForNitz = false;
    private boolean mGotCountryCode = false;

    /**
@@ -198,95 +200,146 @@ public class NitzStateMachine {
     *     probably hasn't
     */
    public void handleNetworkCountryCodeSet(boolean countryChanged) {
        boolean hadCountryCode = mGotCountryCode;
        mGotCountryCode = true;

        String isoCountryCode = mDeviceState.getNetworkCountryIsoForPhone();
        if (!TextUtils.isEmpty(isoCountryCode) && !mNitzTimeZoneDetectionSuccessful) {
            updateTimeZoneByNetworkCountryCode(isoCountryCode);
            updateTimeZoneFromNetworkCountryCode(isoCountryCode);
        }

        if (countryChanged || mNeedCountryCodeForNitz) {
        if (mLatestNitzSignal != null && (countryChanged || !hadCountryCode)) {
            updateTimeZoneFromCountryAndNitz();
        }
    }

    private void updateTimeZoneFromCountryAndNitz() {
        String isoCountryCode = mDeviceState.getNetworkCountryIsoForPhone();
        TimeStampedValue<NitzData> nitzSignal = mLatestNitzSignal;

        // TimeZone.getDefault() returns a default zone (GMT) even when time zone have never
        // been set which makes it difficult to tell if it's what the user / time zone detection
        // has chosen. isTimeZoneSettingInitialized() tells us whether the time zone of the
        // device has ever been explicit set by the user or code.
        final boolean isTimeZoneSettingInitialized =
                mTimeServiceHelper.isTimeZoneSettingInitialized();

        if (DBG) {
                Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet:"
            Rlog.d(LOG_TAG, "updateTimeZoneFromCountryAndNitz:"
                    + " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
                        + " mLatestNitzSignal=" + mLatestNitzSignal
                    + " nitzSignal=" + nitzSignal
                    + " isoCountryCode=" + isoCountryCode);
        }

        try {
            NitzData nitzData = nitzSignal.mValue;

            String zoneId;
            if (TextUtils.isEmpty(isoCountryCode) && mNeedCountryCodeForNitz) {
                // Country code not found.  This is likely a test network.
                // Get a TimeZone based only on the NITZ parameters (best guess).

                // mNeedCountryCodeForNitz is only set to true after an NITZ signal has been
                // received and so mLatestNitzSign must be non-null.
                OffsetResult lookupResult =
                        mTimeZoneLookupHelper.lookupByNitz(mLatestNitzSignal.mValue);
            if (nitzData.getEmulatorHostTimeZone() != null) {
                zoneId = nitzData.getEmulatorHostTimeZone().getID();
            } else if (!mGotCountryCode) {
                // We don't have a country code so we won't try to look up the time zone.
                zoneId = null;
            } else if (TextUtils.isEmpty(isoCountryCode)) {
                // We have a country code but it's empty. This is most likely because we're on a
                // test network that's using a bogus MCC (eg, "001"). Obtain a TimeZone based only
                // on the NITZ parameters: it's only going to be correct in a few cases but it
                // should at least have the correct offset.
                OffsetResult lookupResult = mTimeZoneLookupHelper.lookupByNitz(nitzData);
                String logMsg = "updateTimeZoneFromCountryAndNitz: lookupByNitz returned"
                        + " lookupResult=" + lookupResult;
                if (DBG) {
                    Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: guessZoneIdByNitz() returned"
                            + " lookupResult=" + lookupResult);
                    Rlog.d(LOG_TAG, logMsg);
                }
                // We log this in the time zone log because it has been a source of bugs.
                mTimeZoneLog.log(logMsg);

                zoneId = lookupResult != null ? lookupResult.zoneId : null;
            } else if (mLatestNitzSignal == null) {
                zoneId = null;
                if (DBG) {
                    Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: No cached NITZ data available,"
                    Rlog.d(LOG_TAG,
                            "updateTimeZoneFromCountryAndNitz: No cached NITZ data available,"
                                    + " not setting zone");
                }
            } else { // mLatestNitzSignal != null
                if (isNitzSignalOffsetInfoBogus(mLatestNitzSignal, isoCountryCode)) {
                    String logMsg = "handleNetworkCountryCodeSet: Stored NITZ looks bogus, "
                zoneId = null;
            } else if (isNitzSignalOffsetInfoBogus(nitzSignal, isoCountryCode)) {
                String logMsg = "updateTimeZoneFromCountryAndNitz: Received NITZ looks bogus, "
                        + " isoCountryCode=" + isoCountryCode
                            + " mLatestNitzSignal=" + mLatestNitzSignal;
                        + " nitzSignal=" + nitzSignal;
                if (DBG) {
                    Rlog.d(LOG_TAG, logMsg);
                }
                // We log this in the time zone log because it has been a source of bugs.
                mTimeZoneLog.log(logMsg);

                    // No zone can be determined.
                zoneId = null;
            } else {
                    NitzData nitzData = mLatestNitzSignal.mValue;
                    OffsetResult lookupResult =
                            mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, isoCountryCode);
                OffsetResult lookupResult = mTimeZoneLookupHelper.lookupByNitzCountry(
                        nitzData, isoCountryCode);
                if (DBG) {
                        Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: using"
                                + " guessZoneIdByNitzCountry(nitzData, isoCountryCode),"
                    Rlog.d(LOG_TAG, "updateTimeZoneFromCountryAndNitz: using"
                            + " lookupByNitzCountry(nitzData, isoCountryCode),"
                            + " nitzData=" + nitzData
                            + " isoCountryCode=" + isoCountryCode
                            + " lookupResult=" + lookupResult);
                }
                zoneId = lookupResult != null ? lookupResult.zoneId : null;
            }
            }
            final String tmpLog = "handleNetworkCountryCodeSet:"

            // Log the action taken to the dedicated time zone log.
            final String tmpLog = "updateTimeZoneFromCountryAndNitz:"
                    + " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
                    + " mLatestNitzSignal=" + mLatestNitzSignal
                    + " isoCountryCode=" + isoCountryCode
                    + " mNeedCountryCodeForNitz=" + mNeedCountryCodeForNitz
                    + " zoneId=" + zoneId;
                    + " nitzSignal=" + nitzSignal
                    + " zoneId=" + zoneId
                    + " isTimeZoneDetectionEnabled()="
                    + mTimeServiceHelper.isTimeZoneDetectionEnabled();
            mTimeZoneLog.log(tmpLog);

            // Set state as needed.
            if (zoneId != null) {
                Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: zoneId != null, zoneId=" + zoneId);
                if (DBG) {
                    Rlog.d(LOG_TAG, "updateTimeZoneFromCountryAndNitz: zoneId=" + zoneId);
                }
                if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
                    setAndBroadcastNetworkSetTimeZone(zoneId);
                } else {
                    Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: skip changing zone as"
                            + " isTimeZoneDetectionEnabled() is false");
                    if (DBG) {
                        Rlog.d(LOG_TAG, "updateTimeZoneFromCountryAndNitz: skip changing zone"
                                + " as isTimeZoneDetectionEnabled() is false");
                    }
                }
                mSavedTimeZoneId = zoneId;
                mNitzTimeZoneDetectionSuccessful = true;
            } else {
                Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: lookupResult == null, do nothing");
                if (DBG) {
                    Rlog.d(LOG_TAG,
                            "updateTimeZoneFromCountryAndNitz: zoneId == null, do nothing");
                }
            mNeedCountryCodeForNitz = false;
            }
        } catch (RuntimeException ex) {
            Rlog.e(LOG_TAG, "updateTimeZoneFromCountryAndNitz: Processing NITZ data"
                    + " nitzSignal=" + nitzSignal
                    + " isoCountryCode=" + isoCountryCode
                    + " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
                    + " ex=" + ex);
        }
    }

    /**
     * Returns true if the NITZ signal is definitely bogus, assuming that the country is correct.
     */
    private boolean isNitzSignalOffsetInfoBogus(
            TimeStampedValue<NitzData> nitzSignal, String isoCountryCode) {

        if (TextUtils.isEmpty(isoCountryCode)) {
            // We cannot say for sure.
            return false;
        }

        NitzData newNitzData = nitzSignal.mValue;
        boolean zeroOffsetNitz = newNitzData.getLocalOffsetMillis() == 0 && !newNitzData.isDst();
        return zeroOffsetNitz && !countryUsesUtc(isoCountryCode, nitzSignal);
    }

    private boolean countryUsesUtc(
@@ -327,111 +380,19 @@ public class NitzStateMachine {
        // Always store the latest NITZ signal received.
        mLatestNitzSignal = nitzSignal;

        handleTimeZoneFromNitz(nitzSignal);
        handleTimeFromNitz(nitzSignal);
    }

    /**
     * Returns true if the NITZ signal is definitely bogus, assuming that the country is correct.
     */
    private boolean isNitzSignalOffsetInfoBogus(
            TimeStampedValue<NitzData> nitzSignal, String isoCountryCode) {

        if (TextUtils.isEmpty(isoCountryCode)) {
            // We cannot say for sure.
            return false;
        }

        NitzData newNitzData = nitzSignal.mValue;
        boolean zeroOffsetNitz = newNitzData.getLocalOffsetMillis() == 0 && !newNitzData.isDst();
        return zeroOffsetNitz && !countryUsesUtc(isoCountryCode, nitzSignal);
    }

    private void handleTimeZoneFromNitz(TimeStampedValue<NitzData> nitzSignal) {
        try {
            NitzData newNitzData = nitzSignal.mValue;
            String isoCountryCode = mDeviceState.getNetworkCountryIsoForPhone();

            boolean needCountryCode;
            String zoneId;
            if (newNitzData.getEmulatorHostTimeZone() != null) {
                zoneId = newNitzData.getEmulatorHostTimeZone().getID();
                needCountryCode = false;
            } else {
                if (!mGotCountryCode) {
                    // We don't have a country code so we won't try to look up the time zone.
                    zoneId = null;
                    needCountryCode = true;
                } else if (TextUtils.isEmpty(isoCountryCode)) {
                    // We have a country code but it's empty. 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.
                    OffsetResult lookupResult = mTimeZoneLookupHelper.lookupByNitz(newNitzData);
                    if (DBG) {
                        Rlog.d(LOG_TAG, "handleTimeZoneFromNitz: guessZoneIdByNitz returned"
                                + " lookupResult=" + lookupResult);
                    }
                    zoneId = lookupResult != null ? lookupResult.zoneId : null;
                    needCountryCode = false;
                } else if (isNitzSignalOffsetInfoBogus(nitzSignal, isoCountryCode)) {
                    String logMsg = "handleNitzReceived: Received NITZ looks bogus, "
                            + " isoCountryCode=" + isoCountryCode
                            + " nitzSignal=" + nitzSignal;
                    if (DBG) {
                        Rlog.d(LOG_TAG, logMsg);
                    }
                    mTimeZoneLog.log(logMsg);

                    zoneId = null;
                    needCountryCode = false;
                } else {
                    OffsetResult lookupResult = mTimeZoneLookupHelper.lookupByNitzCountry(
                            newNitzData, isoCountryCode);
                    zoneId = lookupResult != null ? lookupResult.zoneId : null;
                    needCountryCode = false;
                }
        updateTimeZoneFromCountryAndNitz();
        updateTimeFromNitz();
    }

            // Set state as needed.
            if (needCountryCode) {
                mNeedCountryCodeForNitz = true;
            } else {
                mNeedCountryCodeForNitz = false;
                if (zoneId != null) {
                    if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
                        setAndBroadcastNetworkSetTimeZone(zoneId);
                    }
                    mSavedTimeZoneId = zoneId;
                    mNitzTimeZoneDetectionSuccessful = true;
                }
            }

            String tmpLog = "handleTimeZoneFromNitz: nitzSignal=" + nitzSignal
                    + " zoneId=" + zoneId
                    + " mGotCountryCode=" + mGotCountryCode
                    + " isoCountryCode=" + isoCountryCode
                    + " mNeedCountryCodeForNitz=" + mNeedCountryCodeForNitz
                    + " mNitzTimeZoneDetectionSuccessful=" + mNitzTimeZoneDetectionSuccessful
                    + " mSavedTimeZoneId=" + mSavedTimeZoneId
                    + " isTimeZoneDetectionEnabled()="
                    + mTimeServiceHelper.isTimeZoneDetectionEnabled();
            if (DBG) {
                Rlog.d(LOG_TAG, tmpLog);
            }
            mTimeZoneLog.log(tmpLog);
        } catch (RuntimeException ex) {
            Rlog.e(LOG_TAG, "handleTimeZoneFromNitz: Processing NITZ data"
                    + " nitzSignal=" + nitzSignal
                    + " ex=" + ex);
        }
    }

    private void handleTimeFromNitz(TimeStampedValue<NitzData> nitzSignal) {
    private void updateTimeFromNitz() {
        TimeStampedValue<NitzData> nitzSignal = mLatestNitzSignal;
        try {
            boolean ignoreNitz = mDeviceState.getIgnoreNitz();
            if (ignoreNitz) {
                if (DBG) {
                    Rlog.d(LOG_TAG,
                        "handleTimeFromNitz: Not setting clock because gsm.ignore-nitz is set");
                            "updateTimeFromNitz: Not setting clock because gsm.ignore-nitz is set");
                }
                return;
            }

@@ -445,7 +406,7 @@ public class NitzStateMachine {
                long millisSinceNitzReceived = elapsedRealtime - nitzSignal.mElapsedRealtime;
                if (millisSinceNitzReceived < 0 || millisSinceNitzReceived > Integer.MAX_VALUE) {
                    if (DBG) {
                        Rlog.d(LOG_TAG, "handleTimeFromNitz: not setting time, unexpected"
                        Rlog.d(LOG_TAG, "updateTimeFromNitz: not setting time, unexpected"
                                + " elapsedRealtime=" + elapsedRealtime
                                + " nitzSignal=" + nitzSignal);
                    }
@@ -458,7 +419,7 @@ public class NitzStateMachine {
                long gained = adjustedCurrentTimeMillis - mTimeServiceHelper.currentTimeMillis();

                if (mTimeServiceHelper.isTimeDetectionEnabled()) {
                    String logMsg = "handleTimeFromNitz:"
                    String logMsg = "updateTimeFromNitz:"
                            + " nitzSignal=" + nitzSignal
                            + " adjustedCurrentTimeMillis=" + adjustedCurrentTimeMillis
                            + " millisSinceNitzReceived= " + millisSinceNitzReceived
@@ -502,7 +463,7 @@ public class NitzStateMachine {
                mWakeLock.release();
            }
        } catch (RuntimeException ex) {
            Rlog.e(LOG_TAG, "handleTimeFromNitz: Processing NITZ data"
            Rlog.e(LOG_TAG, "updateTimeFromNitz: Processing NITZ data"
                    + " nitzSignal=" + nitzSignal
                    + " ex=" + ex);
        }
@@ -578,7 +539,6 @@ public class NitzStateMachine {
        pw.println(" mSavedTime=" + mSavedNitzTime);

        // Time Zone Detection State
        pw.println(" mNeedCountryCodeForNitz=" + mNeedCountryCodeForNitz);
        pw.println(" mLatestNitzSignal=" + mLatestNitzSignal);
        pw.println(" mGotCountryCode=" + mGotCountryCode);
        pw.println(" mSavedTimeZoneId=" + mSavedTimeZoneId);
@@ -610,13 +570,13 @@ public class NitzStateMachine {
     *
     * @param iso Country code from network MCC
     */
    private void updateTimeZoneByNetworkCountryCode(String iso) {
    private void updateTimeZoneFromNetworkCountryCode(String iso) {
        CountryResult lookupResult = mTimeZoneLookupHelper.lookupByCountry(
                iso, mTimeServiceHelper.currentTimeMillis());
        if (lookupResult != null && lookupResult.allZonesHaveSameOffset) {
            String logMsg = "updateTimeZoneByNetworkCountryCode: set time"
                    + " lookupResult=" + lookupResult
                    + " iso=" + iso;
            String logMsg = "updateTimeZoneFromNetworkCountryCode: tz result found"
                    + " iso=" + iso
                    + " lookupResult=" + lookupResult;
            if (DBG) {
                Rlog.d(LOG_TAG, logMsg);
            }
@@ -628,7 +588,7 @@ public class NitzStateMachine {
            mSavedTimeZoneId = zoneId;
        } else {
            if (DBG) {
                Rlog.d(LOG_TAG, "updateTimeZoneByNetworkCountryCode: no good zone for"
                Rlog.d(LOG_TAG, "updateTimeZoneFromNetworkCountryCode: no good zone for"
                        + " iso=" + iso
                        + " lookupResult=" + lookupResult);
            }
+119 −0
Original line number Diff line number Diff line
@@ -667,6 +667,125 @@ public class NitzStateMachineTest extends TelephonyTest {
        assertNull(mNitzStateMachine.getSavedTimeZoneId());
    }

    @Test
    public void test_emulatorNitzExtensionUsedForTimeZone() throws Exception {
        Scenario scenario = UNIQUE_US_ZONE_SCENARIO;
        Device device = new DeviceBuilder()
                .setClocksFromScenario(scenario)
                .setTimeDetectionEnabled(false)
                .setTimeZoneDetectionEnabled(true)
                .setTimeZoneSettingInitialized(true)
                .initialize();
        Script script = new Script(device);

        TimeStampedValue<NitzData> originalNitzSignal = scenario.getNitzSignal();

        // Create an NITZ signal with an explicit time zone (as can happen on emulators)
        NitzData originalNitzData = originalNitzSignal.mValue;
        // A time zone that is obviously not in the US, but it should not be questioned.
        String emulatorTimeZoneId = "Europe/London";
        NitzData emulatorNitzData = NitzData.createForTests(
                originalNitzData.getLocalOffsetMillis(),
                originalNitzData.getDstAdjustmentMillis(),
                originalNitzData.getCurrentTimeInMillis(),
                java.util.TimeZone.getTimeZone(emulatorTimeZoneId) /* emulatorHostTimeZone */);
        TimeStampedValue<NitzData> emulatorNitzSignal = new TimeStampedValue<>(
                emulatorNitzData, originalNitzSignal.mElapsedRealtime);

        // Simulate receiving the emulator NITZ signal.
        script.nitzReceived(emulatorNitzSignal)
                .verifyOnlyTimeZoneWasSetAndReset(emulatorTimeZoneId);

        // Check NitzStateMachine state.
        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
        assertEquals(emulatorNitzSignal.mValue, mNitzStateMachine.getCachedNitzData());
        assertEquals(emulatorTimeZoneId, mNitzStateMachine.getSavedTimeZoneId());
    }

    @Test
    public void test_emptyCountryStringUsTime_countryReceivedFirst() throws Exception {
        Scenario scenario = UNIQUE_US_ZONE_SCENARIO;
        Device device = new DeviceBuilder()
                .setClocksFromScenario(scenario)
                .setTimeDetectionEnabled(true)
                .setTimeZoneDetectionEnabled(true)
                .setTimeZoneSettingInitialized(true)
                .initialize();
        Script script = new Script(device);

        String expectedZoneId = checkNitzOnlyLookupIsAmbiguousAndReturnZoneId(scenario);

        // Nothing should be set. The country is not valid.
        script.countryReceived("").verifyNothingWasSetAndReset();

        // Check NitzStateMachine state.
        assertFalse(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
        assertNull(mNitzStateMachine.getCachedNitzData());
        assertNull(mNitzStateMachine.getSavedTimeZoneId());

        // Simulate receiving the NITZ signal.
        script.nitzReceived(scenario.getNitzSignal())
                .verifyTimeAndZoneSetAndReset(scenario.getActualTimeMillis(), expectedZoneId);

        // Check NitzStateMachine state.
        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
        assertEquals(expectedZoneId, mNitzStateMachine.getSavedTimeZoneId());
    }

    @Test
    public void test_emptyCountryStringUsTime_nitzReceivedFirst() throws Exception {
        Scenario scenario = UNIQUE_US_ZONE_SCENARIO;
        Device device = new DeviceBuilder()
                .setClocksFromScenario(scenario)
                .setTimeDetectionEnabled(true)
                .setTimeZoneDetectionEnabled(true)
                .setTimeZoneSettingInitialized(true)
                .initialize();
        Script script = new Script(device);

        String expectedZoneId = checkNitzOnlyLookupIsAmbiguousAndReturnZoneId(scenario);

        // Simulate receiving the NITZ signal.
        script.nitzReceived(scenario.getNitzSignal())
                .verifyOnlyTimeWasSetAndReset(scenario.getActualTimeMillis());

        // Check NitzStateMachine state.
        assertFalse(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
        assertNull(mNitzStateMachine.getSavedTimeZoneId());

        // The time zone should be set (but the country is not valid so it's unlikely to be
        // correct).
        script.countryReceived("").verifyOnlyTimeZoneWasSetAndReset(expectedZoneId);

        // Check NitzStateMachine state.
        assertTrue(mNitzStateMachine.getNitzTimeZoneDetectionSuccessful());
        assertEquals(scenario.getNitzSignal().mValue, mNitzStateMachine.getCachedNitzData());
        assertEquals(expectedZoneId, mNitzStateMachine.getSavedTimeZoneId());
    }

    /**
     * Asserts a test scenario has the properties we expect for NITZ-only lookup. There are
     * usually multiple zones that will share the same UTC offset so we get a low quality / low
     * confidence answer, but the zone we find should at least have the correct offset.
     */
    private String checkNitzOnlyLookupIsAmbiguousAndReturnZoneId(Scenario scenario) {
        OffsetResult result =
                mRealTimeZoneLookupHelper.lookupByNitz(scenario.getNitzSignal().mValue);
        String expectedZoneId = result.zoneId;
        // All our scenarios should return multiple matches. The only cases where this wouldn't be
        // true are places that use offsets like XX:15, XX:30 and XX:45.
        assertFalse(result.isOnlyMatch);
        assertSameOffset(scenario.getActualTimeMillis(), expectedZoneId, scenario.getTimeZoneId());
        return expectedZoneId;
    }

    private static void assertSameOffset(long timeMillis, String zoneId1, String zoneId2) {
        assertEquals(TimeZone.getTimeZone(zoneId1).getOffset(timeMillis),
                TimeZone.getTimeZone(zoneId2).getOffset(timeMillis));
    }

    private static long createUtcTime(int year, int monthInYear, int day, int hourOfDay, int minute,
            int second) {
        Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("Etc/UTC"));