/* * 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 com.android.internal.annotations.VisibleForTesting; import com.android.telephony.Rlog; import java.time.DateTimeException; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Objects; import java.util.TimeZone; import java.util.regex.Pattern; /** * Represents NITZ data. Various static methods are provided to help with parsing and interpretation * 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_QUARTER_HOUR = 15 * 60 * 1000; private static final int MS_PER_HOUR = 60 * 60 * 1000; private static final Pattern NITZ_SPLIT_PATTERN = Pattern.compile("[/:,+-]"); // 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 unixEpochTimeMillis, TimeZone emulatorHostTimeZone) { if (originalString == null) { throw new NullPointerException("originalString==null"); } this.mOriginalString = originalString; this.mZoneOffset = zoneOffsetMillis; this.mDstOffset = dstOffsetMillis; this.mCurrentTimeMillis = unixEpochTimeMillis; this.mEmulatorHostTimeZone = emulatorHostTimeZone; } /** * 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 { String[] nitzSubs = NITZ_SPLIT_PATTERN.split(nitz); int year = Integer.parseInt(nitzSubs[0]); if (year < 1 || year > 99) { // 0 > year > 99 imply an invalid string. // // At the time of this comment (year 2023), a zero year is considered invalid and // assumed to be the result of invalid data being converted to zero in the code that // turns the binary NITZ into a string. For the next few decades at least, Android // devices should not need to interpret zero. Hopefully, NITZ will be replaced by // the time that's not true, or folks dealing the Y2K1 issue can handle it. // // DateTimeException is also thrown by LocalDateTime below if the values are out of // range and will be handled in the catch block. throw new DateTimeException("Invalid NITZ year == 0"); } // Values < {current year} could be considered invalid but are used in test code, so no // window is applied to adjust low values < {current year} with "+ 2100" (and would also // need to consider zero as valid). Code that processes the NitzData is in a better // position to log and discard obviously invalid NITZ signals from past years. year += 2000; int month = Integer.parseInt(nitzSubs[1]); int date = Integer.parseInt(nitzSubs[2]); int hour = Integer.parseInt(nitzSubs[3]); int minute = Integer.parseInt(nitzSubs[4]); int second = Integer.parseInt(nitzSubs[5]); // 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). // The LocalDateTime.of() will throw DateTimeException for values outside the allowed // range for the Gregorian calendar. long epochMillis = LocalDateTime.of(year, month, date, hour, minute, second) .toInstant(ZoneOffset.UTC) .toEpochMilli(); // 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 dstAdjustmentHours = (nitzSubs.length >= 8) ? Integer.parseInt(nitzSubs[7]) : null; Integer dstAdjustmentMillis = null; if (dstAdjustmentHours != null) { dstAdjustmentMillis = dstAdjustmentHours * MS_PER_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, epochMillis, zone); } catch (RuntimeException ex) { Rlog.e(LOG_TAG, "NITZ: Parsing NITZ time " + nitz + " ex=" + ex); return null; } } /** A method for use in tests to create NitzData instances. */ public static NitzData createForTests(int zoneOffsetMillis, Integer dstOffsetMillis, long unixEpochTimeMillis, TimeZone emulatorHostTimeZone) { return new NitzData("Test data", zoneOffsetMillis, dstOffsetMillis, unixEpochTimeMillis, emulatorHostTimeZone); } /** * 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. NITZ is limited in only being able to express total offsets in multiples of 15 * minutes. * *

Note that some time zones change offset during the year for reasons other than "daylight * savings", e.g. for Ramadan. This is not well handled by most date / time APIs. */ 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. NITZ is limited in only being able to express DST offsets in positive multiples of * one or two hours. * *

Callers should remember that standard time / DST is a matter of convention: it has * historically been assumed by NITZ and many date/time APIs that DST happens in the summer and * the "raw" offset will increase during this time, usually by one hour. However, the tzdb * maintainers have moved to different conventions on a country-by-country basis so that some * summer times are considered the "standard" time (i.e. in this model winter time is the "DST" * and a negative adjustment, usually of (negative) one hour. * *

There is nothing that says NITZ and tzdb need to treat DST conventions the same. * *

At the time of writing Android date/time APIs are sticking with the historic tzdb * convention that DST is used in summer time and is always a positive offset but this * could change in future. If Android or carriers change the conventions used then it might make * NITZ comparisons with tzdb information more error-prone. * *

See also {@link #getLocalOffsetMillis()} for other reasons besides DST that a local offset * may change. */ 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; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } NitzData nitzData = (NitzData) o; if (mZoneOffset != nitzData.mZoneOffset) { return false; } if (mCurrentTimeMillis != nitzData.mCurrentTimeMillis) { return false; } if (!mOriginalString.equals(nitzData.mOriginalString)) { return false; } if (!Objects.equals(mDstOffset, nitzData.mDstOffset)) { return false; } return Objects.equals(mEmulatorHostTimeZone, nitzData.mEmulatorHostTimeZone); } @Override public int hashCode() { int result = mOriginalString.hashCode(); result = 31 * result + mZoneOffset; result = 31 * result + (mDstOffset != null ? mDstOffset.hashCode() : 0); result = 31 * result + Long.hashCode(mCurrentTimeMillis); result = 31 * result + (mEmulatorHostTimeZone != null ? mEmulatorHostTimeZone.hashCode() : 0); return result; } @Override public String toString() { return "NitzData{" + "mOriginalString=" + mOriginalString + ", mZoneOffset=" + mZoneOffset + ", mDstOffset=" + mDstOffset + ", mCurrentTimeMillis=" + mCurrentTimeMillis + ", mEmulatorHostTimeZone=" + mEmulatorHostTimeZone + '}'; } }