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

Commit 2b6601e3 authored by Neil Fuller's avatar Neil Fuller
Browse files

Extract low level NITZ parsing logic

This change shrinks ServiceStateTracker by a
couple of hundred lines and improves
documentation and variable/field/method naming.

This change is a side-effect of work done to
understand the time / time zone detection logic
with a view to splitting it out from the rest of
the spralling ServiceStateTracker class.

It is likely possible to isolate time / time
zone logic but it will certainly involve
behavior changes. This is useful first step
to reduce the amount of code involved
without altering behavior.

NITZ parsing has been extracted into the static
NitzData.parse() method. NitzData is a state-holding
class that replaces three primitive fields on
ServiceStateTracker that store NITZ-derived data
and are always set together.

This change is a pure refactoring. No functional
changes are intended. The refactoring exposed
various unusual (probably unintended) cases that
were a consequence of using three, potentially
uninitialized, primitive fields to store saved
NITZ state rather than a reference type. Now that
the code can detect that NITZ data is not available
they become more obvious and expose cases that would
(for example) try to do time zone detection
with an offset == 0 and time == 0 and dst = false
(resulting in Africa/Abidjan being detected if it
ever runs).

Now the parsing / time zone lookup code is
separate it can be tested so tests have been
included here.

Tested with:
make google-tradefed-all FrameworksTelephonyTests
tradefed.sh run template/local_min --template:map test=FrameworksTelephonyTests

...and also, more quickly, with...

vogar --no-multidex --classpath \
    out/target/product/marlin/data/app/FrameworksTelephonyTests/FrameworksTelephonyTests.apk \
    com.android.internal.telephony.NitzDataTest

Bug: 63743683
Test: See above
Test: Booted device
(cherry picked from commit d8ce8374)
Merged-In: Ie7004a7e67dff50b8271bdfe6d01111d7091dbe4
Change-Id: Ie7004a7e67dff50b8271bdfe6d01111d7091dbe4
parent 4f0e6a13
Loading
Loading
Loading
Loading
+228 −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 static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;

import android.telephony.Rlog;

import com.android.internal.annotations.VisibleForTesting;

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

/**
 * Represents NITZ data. Various static methods are provided to help with parsing and intepretation
 * of NITZ data.
 *
 * {@hide}
 */
@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 */
    private static final int MAX_NITZ_YEAR = 2037;

    // Stored For logging / debugging only.
    private final String mOriginalString;

    private final int mZoneOffset;

    private final Integer mDstOffset;

    private final long mCurrentTimeMillis;

    private final TimeZone mEmulatorHostTimeZone;

    private NitzData(String originalString, int zoneOffsetMillis, Integer dstOffsetMillis,
            long utcTimeMillis, TimeZone timeZone) {
        this.mOriginalString = originalString;
        this.mZoneOffset = zoneOffsetMillis;
        this.mDstOffset = dstOffsetMillis;
        this.mCurrentTimeMillis = utcTimeMillis;
        this.mEmulatorHostTimeZone = timeZone;
    }

    /**
     * Parses the supplied NITZ string, returning the encoded data.
     */
    public static NitzData parse(String nitz) {
        // "yy/mm/dd,hh:mm:ss(+/-)tz[,dt[,tzid]]"
        // tz, dt are in number of quarter-hours

        try {
            /* NITZ time (hour:min:sec) will be in UTC but it supplies the timezone
             * offset as well (which we won't worry about until later) */
            Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
            c.clear();
            c.set(Calendar.DST_OFFSET, 0);

            String[] nitzSubs = nitz.split("[/:,+-]");

            int year = 2000 + Integer.parseInt(nitzSubs[0]);
            if (year > MAX_NITZ_YEAR) {
                if (ServiceStateTracker.DBG) {
                    Rlog.e(LOG_TAG, "NITZ year: " + year + " exceeds limit, skip NITZ time update");
                }
                return null;
            }
            c.set(Calendar.YEAR, year);

            // month is 0 based!
            int month = Integer.parseInt(nitzSubs[1]) - 1;
            c.set(Calendar.MONTH, month);

            int date = Integer.parseInt(nitzSubs[2]);
            c.set(Calendar.DATE, date);

            int hour = Integer.parseInt(nitzSubs[3]);
            c.set(Calendar.HOUR, hour);

            int minute = Integer.parseInt(nitzSubs[4]);
            c.set(Calendar.MINUTE, minute);

            int second = Integer.parseInt(nitzSubs[5]);
            c.set(Calendar.SECOND, second);

            // The offset received from NITZ is the offset to add to get current local time.
            boolean sign = (nitz.indexOf('-') == -1);
            int totalUtcOffsetQuarterHours = Integer.parseInt(nitzSubs[6]);
            int totalUtcOffsetMillis =
                    (sign ? 1 : -1) * totalUtcOffsetQuarterHours * MS_PER_QUARTER_HOUR;

            // DST correction is already applied to the UTC offset. We could subtract it if we
            // wanted the raw offset.
            Integer dstAdjustmentQuarterHours =
                    (nitzSubs.length >= 8) ? Integer.parseInt(nitzSubs[7]) : null;
            Integer dstAdjustmentMillis = null;
            if (dstAdjustmentQuarterHours != null) {
                dstAdjustmentMillis = dstAdjustmentQuarterHours * MS_PER_QUARTER_HOUR;
            }

            // As a special extension, the Android emulator appends the name of
            // the host computer's timezone to the nitz string. this is zoneinfo
            // timezone name of the form Area!Location or Area!Location!SubLocation
            // so we need to convert the ! into /
            TimeZone zone = null;
            if (nitzSubs.length >= 9) {
                String tzname = nitzSubs[8].replace('!', '/');
                zone = TimeZone.getTimeZone(tzname);
            }
            return new NitzData(nitz, totalUtcOffsetMillis, dstAdjustmentMillis,
                    c.getTimeInMillis(), zone);
        } catch (RuntimeException ex) {
            Rlog.e(LOG_TAG, "NITZ: Parsing NITZ time " + nitz + " ex=" + ex);
            return null;
        }
    }

    /**
     * Returns the current time as the number of milliseconds since the beginning of the Unix epoch
     * (1/1/1970 00:00:00 UTC).
     */
    public long getCurrentTimeInMillis() {
        return mCurrentTimeMillis;
    }

    /**
     * Returns the total offset to apply to the {@link #getCurrentTimeInMillis()} to arrive at a
     * local time.
     */
    public int getLocalOffsetMillis() {
        return mZoneOffset;
    }

    /**
     * Returns the offset (already included in {@link #getLocalOffsetMillis()}) associated with
     * Daylight Savings Time (DST). This field is optional: {@code null} means the DST offset is
     * unknown.
     */
    public Integer getDstAdjustmentMillis() {
        return mDstOffset;
    }

    /**
     * Returns {@link true} if the time is in Daylight Savings Time (DST), {@link false} if it is
     * unknown or not in DST. See {@link #getDstAdjustmentMillis()}.
     */
    public boolean isDst() {
        return mDstOffset != null && mDstOffset != 0;
    }


    /**
     * Returns the time zone of the host computer when Android is running in an emulator. It is
     * {@code null} for real devices. This information is communicated via a non-standard Android
     * extension to NITZ.
     */
    public TimeZone getEmulatorHostTimeZone() {
        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 String toString() {
        return "NitzData{"
                + "mOriginalString=" + mOriginalString
                + ", mZoneOffset=" + mZoneOffset
                + ", mDstOffset=" + mDstOffset
                + ", mCurrentTimeMillis=" + mCurrentTimeMillis
                + ", mEmulatorHostTimeZone=" + mEmulatorHostTimeZone
                + '}';
    }
}
+98 −162

File changed.

Preview size limit exceeded, changes collapsed.

+170 −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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

import org.junit.Test;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;

public class NitzDataTest {

    @Test
    public void testParse_dateOutsideAllowedRange() {
        assertNull(NitzData.parse("38/06/20,00:00:00+0"));
    }

    @Test
    public void testParse_missingRequiredFields() {
        // "yy/mm/dd,hh:mm:ss(+/-)tz[,dt[,tzid]]"

        // No tz field.
        assertNull(NitzData.parse("38/06/20,00:00:00"));
    }

    @Test
    public void testParse_withDst() {
        // "yy/mm/dd,hh:mm:ss(+/-)tz[,dt[,tzid]]"
        // tz, dt are in number of quarter-hours
        {
            NitzData nitz = NitzData.parse("15/06/20,01:02:03-1,0");
            assertEquals(createUtcTime(2015, 6, 20, 1, 2, 3), nitz.getCurrentTimeInMillis());
            assertEquals(TimeUnit.MINUTES.toMillis(-1 * 15), nitz.getLocalOffsetMillis());
            assertEquals(0, nitz.getDstAdjustmentMillis().longValue());
            assertNull(nitz.getEmulatorHostTimeZone());
        }
        {
            NitzData nitz = NitzData.parse("15/06/20,01:02:03+8,4");
            assertEquals(createUtcTime(2015, 6, 20, 1, 2, 3), nitz.getCurrentTimeInMillis());
            assertEquals(TimeUnit.MINUTES.toMillis(8 * 15), nitz.getLocalOffsetMillis());
            assertEquals(TimeUnit.MINUTES.toMillis(4 * 15),
                    nitz.getDstAdjustmentMillis().longValue());
            assertNull(nitz.getEmulatorHostTimeZone());
        }
        {
            NitzData nitz = NitzData.parse("15/06/20,01:02:03-8,4");
            assertEquals(createUtcTime(2015, 6, 20, 1, 2, 3), nitz.getCurrentTimeInMillis());
            assertEquals(TimeUnit.MINUTES.toMillis(-8 * 15), nitz.getLocalOffsetMillis());
            assertEquals(TimeUnit.MINUTES.toMillis(4 * 15),
                    nitz.getDstAdjustmentMillis().longValue());
            assertNull(nitz.getEmulatorHostTimeZone());
        }
    }

    @Test
    public void testParse_noDstField() {
        {
            NitzData nitz = NitzData.parse("15/06/20,01:02:03+4");
            assertEquals(createUtcTime(2015, 6, 20, 1, 2, 3), nitz.getCurrentTimeInMillis());
            assertEquals(TimeUnit.MINUTES.toMillis(4 * 15), nitz.getLocalOffsetMillis());
            assertNull(nitz.getDstAdjustmentMillis());
            assertNull(nitz.getEmulatorHostTimeZone());
        }
        {
            NitzData nitz = NitzData.parse("15/06/20,01:02:03-4");
            assertEquals(createUtcTime(2015, 6, 20, 1, 2, 3), nitz.getCurrentTimeInMillis());
            assertEquals(TimeUnit.MINUTES.toMillis(-4 * 15), nitz.getLocalOffsetMillis());
            assertNull(nitz.getDstAdjustmentMillis());
            assertNull(nitz.getEmulatorHostTimeZone());
        }
    }

    @Test
    public void testParse_androidEmulatorTimeZoneExtension() {
        NitzData nitz = NitzData.parse("15/06/20,01:02:03-32,4,America!Los_Angeles");
        assertEquals(createUtcTime(2015, 6, 20, 1, 2, 3), nitz.getCurrentTimeInMillis());
        assertEquals(TimeUnit.MINUTES.toMillis(-32 * 15), nitz.getLocalOffsetMillis());
        assertEquals(TimeUnit.MINUTES.toMillis(4 * 15),
                nitz.getDstAdjustmentMillis().longValue());
        assertEquals("America/Los_Angeles", nitz.getEmulatorHostTimeZone().getID());
    }

    @Test
    public void testToString() {
        assertNotNull(NitzData.parse("15/06/20,01:02:03-32").toString());
        assertNotNull(NitzData.parse("15/06/20,01:02:03-32,4").toString());
        assertNotNull(NitzData.parse("15/06/20,01:02:03-32,4,America!Los_Angeles")
                .toString());
    }

    @Test
    public void testGuessTimeZone() {
        // Historical dates are used to avoid the test breaking due to data changes.
        // However, algorithm updates may change the exact time zone returned, though it shouldn't
        // ever be a less exact match.
        long nhSummerTimeMillis = createUtcTime(2015, 6, 20, 1, 2, 3);
        long nhWinterTimeMillis = createUtcTime(2015, 1, 20, 1, 2, 3);
        String nhSummerTimeString = "15/06/20,01:02:03";
        String nhWinterTimeString = "15/01/20,01:02:03";

        // Known DST state (true).
        assertTimeZone(nhSummerTimeMillis, TimeUnit.HOURS.toMillis(1), TimeUnit.HOURS.toMillis(1),
                NitzData.guessTimeZone(NitzData.parse(nhSummerTimeString + "+4,4")));
        assertTimeZone(nhSummerTimeMillis, TimeUnit.HOURS.toMillis(-8), TimeUnit.HOURS.toMillis(1),
                NitzData.guessTimeZone(NitzData.parse("15/06/20,01:02:03-32,4")));

        // Known DST state (false)
        assertTimeZone(nhWinterTimeMillis, 0L, 0L,
                NitzData.guessTimeZone(NitzData.parse(nhWinterTimeString + "+0,0")));
        assertTimeZone(nhWinterTimeMillis, TimeUnit.HOURS.toMillis(-8), 0L,
                NitzData.guessTimeZone(NitzData.parse(nhWinterTimeString + "-32,0")));

        // Unknown DST state
        assertTimeZone(nhSummerTimeMillis, TimeUnit.HOURS.toMillis(1), null,
                NitzData.guessTimeZone(NitzData.parse(nhSummerTimeString + "+4")));
        assertTimeZone(nhSummerTimeMillis, TimeUnit.HOURS.toMillis(-8), null,
                NitzData.guessTimeZone(NitzData.parse(nhSummerTimeString + "-32")));
        assertTimeZone(nhWinterTimeMillis, 0L, null,
                NitzData.guessTimeZone(NitzData.parse(nhWinterTimeString + "+0")));
        assertTimeZone(nhWinterTimeMillis, TimeUnit.HOURS.toMillis(-8), null,
                NitzData.guessTimeZone(NitzData.parse(nhWinterTimeString + "-32")));
    }

    private static void assertTimeZone(
            long time, long expectedOffsetAtTime, Long expectedDstAtTime, TimeZone timeZone) {

        GregorianCalendar calendar = new GregorianCalendar(timeZone);
        calendar.setTimeInMillis(time);
        int actualOffsetAtTime =
                calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
        assertEquals(expectedOffsetAtTime, actualOffsetAtTime);

        if (expectedDstAtTime != null) {
            Date date = new Date(time);
            assertEquals(expectedDstAtTime > 0, timeZone.inDaylightTime(date));

            // The code under test assumes DST means +1 in all cases,
            // This code makes fewer assumptions.
            assertEquals(expectedDstAtTime.intValue(), calendar.get(Calendar.DST_OFFSET));
        }
    }

    private static long createUtcTime(
            int year, int monthOfYear, int dayOfMonth, int hourOfDay, int minute, int second) {
        GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
        calendar.clear(); // Clear millis, etc.
        calendar.set(year, monthOfYear - 1, dayOfMonth, hourOfDay, minute, second);
        return calendar.getTimeInMillis();
    }
}