| /* |
| * 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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>There is nothing that says NITZ and tzdb need to treat DST conventions the same. |
| * |
| * <p>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 <em>always</em> 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. |
| * |
| * <p>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 |
| + '}'; |
| } |
| } |