264 lines
11 KiB
Java
264 lines
11 KiB
Java
/*
|
|
* 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
|
|
+ '}';
|
|
}
|
|
}
|