1 /* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.telephony; 18 19 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; 20 21 import com.android.internal.annotations.VisibleForTesting; 22 import com.android.telephony.Rlog; 23 24 import java.time.DateTimeException; 25 import java.time.LocalDateTime; 26 import java.time.ZoneOffset; 27 import java.util.Objects; 28 import java.util.TimeZone; 29 import java.util.regex.Pattern; 30 31 /** 32 * Represents NITZ data. Various static methods are provided to help with parsing and interpretation 33 * of NITZ data. 34 * 35 * {@hide} 36 */ 37 @VisibleForTesting(visibility = PACKAGE) 38 public final class NitzData { 39 private static final String LOG_TAG = ServiceStateTracker.LOG_TAG; 40 private static final int MS_PER_QUARTER_HOUR = 15 * 60 * 1000; 41 private static final int MS_PER_HOUR = 60 * 60 * 1000; 42 43 private static final Pattern NITZ_SPLIT_PATTERN = Pattern.compile("[/:,+-]"); 44 45 // Stored For logging / debugging only. 46 private final String mOriginalString; 47 48 private final int mZoneOffset; 49 50 private final Integer mDstOffset; 51 52 private final long mCurrentTimeMillis; 53 54 private final TimeZone mEmulatorHostTimeZone; 55 NitzData(String originalString, int zoneOffsetMillis, Integer dstOffsetMillis, long unixEpochTimeMillis, TimeZone emulatorHostTimeZone)56 private NitzData(String originalString, int zoneOffsetMillis, Integer dstOffsetMillis, 57 long unixEpochTimeMillis, TimeZone emulatorHostTimeZone) { 58 if (originalString == null) { 59 throw new NullPointerException("originalString==null"); 60 } 61 this.mOriginalString = originalString; 62 this.mZoneOffset = zoneOffsetMillis; 63 this.mDstOffset = dstOffsetMillis; 64 this.mCurrentTimeMillis = unixEpochTimeMillis; 65 this.mEmulatorHostTimeZone = emulatorHostTimeZone; 66 } 67 68 /** 69 * Parses the supplied NITZ string, returning the encoded data. 70 */ parse(String nitz)71 public static NitzData parse(String nitz) { 72 // "yy/mm/dd,hh:mm:ss(+/-)tz[,dt[,tzid]]" 73 // tz, dt are in number of quarter-hours 74 75 try { 76 String[] nitzSubs = NITZ_SPLIT_PATTERN.split(nitz); 77 78 int year = Integer.parseInt(nitzSubs[0]); 79 if (year < 1 || year > 99) { 80 // 0 > year > 99 imply an invalid string. 81 // 82 // At the time of this comment (year 2023), a zero year is considered invalid and 83 // assumed to be the result of invalid data being converted to zero in the code that 84 // turns the binary NITZ into a string. For the next few decades at least, Android 85 // devices should not need to interpret zero. Hopefully, NITZ will be replaced by 86 // the time that's not true, or folks dealing the Y2K1 issue can handle it. 87 // 88 // DateTimeException is also thrown by LocalDateTime below if the values are out of 89 // range and will be handled in the catch block. 90 throw new DateTimeException("Invalid NITZ year == 0"); 91 } 92 93 // Values < {current year} could be considered invalid but are used in test code, so no 94 // window is applied to adjust low values < {current year} with "+ 2100" (and would also 95 // need to consider zero as valid). Code that processes the NitzData is in a better 96 // position to log and discard obviously invalid NITZ signals from past years. 97 year += 2000; 98 99 int month = Integer.parseInt(nitzSubs[1]); 100 int date = Integer.parseInt(nitzSubs[2]); 101 int hour = Integer.parseInt(nitzSubs[3]); 102 int minute = Integer.parseInt(nitzSubs[4]); 103 int second = Integer.parseInt(nitzSubs[5]); 104 105 // NITZ time (hour:min:sec) will be in UTC but it supplies the timezone 106 // offset as well (which we won't worry about until later). 107 // The LocalDateTime.of() will throw DateTimeException for values outside the allowed 108 // range for the Gregorian calendar. 109 long epochMillis = LocalDateTime.of(year, month, date, hour, minute, second) 110 .toInstant(ZoneOffset.UTC) 111 .toEpochMilli(); 112 113 // The offset received from NITZ is the offset to add to get current local time. 114 boolean sign = (nitz.indexOf('-') == -1); 115 int totalUtcOffsetQuarterHours = Integer.parseInt(nitzSubs[6]); 116 int totalUtcOffsetMillis = 117 (sign ? 1 : -1) * totalUtcOffsetQuarterHours * MS_PER_QUARTER_HOUR; 118 119 // DST correction is already applied to the UTC offset. We could subtract it if we 120 // wanted the raw offset. 121 Integer dstAdjustmentHours = 122 (nitzSubs.length >= 8) ? Integer.parseInt(nitzSubs[7]) : null; 123 Integer dstAdjustmentMillis = null; 124 if (dstAdjustmentHours != null) { 125 dstAdjustmentMillis = dstAdjustmentHours * MS_PER_HOUR; 126 } 127 128 // As a special extension, the Android emulator appends the name of 129 // the host computer's timezone to the nitz string. This is zoneinfo 130 // timezone name of the form Area!Location or Area!Location!SubLocation 131 // so we need to convert the ! into / 132 TimeZone zone = null; 133 if (nitzSubs.length >= 9) { 134 String tzname = nitzSubs[8].replace('!', '/'); 135 zone = TimeZone.getTimeZone(tzname); 136 } 137 return new NitzData(nitz, totalUtcOffsetMillis, dstAdjustmentMillis, epochMillis, zone); 138 } catch (RuntimeException ex) { 139 Rlog.e(LOG_TAG, "NITZ: Parsing NITZ time " + nitz + " ex=" + ex); 140 return null; 141 } 142 } 143 144 /** A method for use in tests to create NitzData instances. */ createForTests(int zoneOffsetMillis, Integer dstOffsetMillis, long unixEpochTimeMillis, TimeZone emulatorHostTimeZone)145 public static NitzData createForTests(int zoneOffsetMillis, Integer dstOffsetMillis, 146 long unixEpochTimeMillis, TimeZone emulatorHostTimeZone) { 147 return new NitzData("Test data", zoneOffsetMillis, dstOffsetMillis, unixEpochTimeMillis, 148 emulatorHostTimeZone); 149 } 150 151 /** 152 * Returns the current time as the number of milliseconds since the beginning of the Unix epoch 153 * (1/1/1970 00:00:00 UTC). 154 */ getCurrentTimeInMillis()155 public long getCurrentTimeInMillis() { 156 return mCurrentTimeMillis; 157 } 158 159 /** 160 * Returns the total offset to apply to the {@link #getCurrentTimeInMillis()} to arrive at a 161 * local time. NITZ is limited in only being able to express total offsets in multiples of 15 162 * minutes. 163 * 164 * <p>Note that some time zones change offset during the year for reasons other than "daylight 165 * savings", e.g. for Ramadan. This is not well handled by most date / time APIs. 166 */ getLocalOffsetMillis()167 public int getLocalOffsetMillis() { 168 return mZoneOffset; 169 } 170 171 /** 172 * Returns the offset (already included in {@link #getLocalOffsetMillis()}) associated with 173 * Daylight Savings Time (DST). This field is optional: {@code null} means the DST offset is 174 * unknown. NITZ is limited in only being able to express DST offsets in positive multiples of 175 * one or two hours. 176 * 177 * <p>Callers should remember that standard time / DST is a matter of convention: it has 178 * historically been assumed by NITZ and many date/time APIs that DST happens in the summer and 179 * the "raw" offset will increase during this time, usually by one hour. However, the tzdb 180 * maintainers have moved to different conventions on a country-by-country basis so that some 181 * summer times are considered the "standard" time (i.e. in this model winter time is the "DST" 182 * and a negative adjustment, usually of (negative) one hour. 183 * 184 * <p>There is nothing that says NITZ and tzdb need to treat DST conventions the same. 185 * 186 * <p>At the time of writing Android date/time APIs are sticking with the historic tzdb 187 * convention that DST is used in summer time and is <em>always</em> a positive offset but this 188 * could change in future. If Android or carriers change the conventions used then it might make 189 * NITZ comparisons with tzdb information more error-prone. 190 * 191 * <p>See also {@link #getLocalOffsetMillis()} for other reasons besides DST that a local offset 192 * may change. 193 */ getDstAdjustmentMillis()194 public Integer getDstAdjustmentMillis() { 195 return mDstOffset; 196 } 197 198 /** 199 * Returns {@link true} if the time is in Daylight Savings Time (DST), {@link false} if it is 200 * unknown or not in DST. See {@link #getDstAdjustmentMillis()}. 201 */ isDst()202 public boolean isDst() { 203 return mDstOffset != null && mDstOffset != 0; 204 } 205 206 207 /** 208 * Returns the time zone of the host computer when Android is running in an emulator. It is 209 * {@code null} for real devices. This information is communicated via a non-standard Android 210 * extension to NITZ. 211 */ getEmulatorHostTimeZone()212 public TimeZone getEmulatorHostTimeZone() { 213 return mEmulatorHostTimeZone; 214 } 215 216 @Override equals(Object o)217 public boolean equals(Object o) { 218 if (this == o) { 219 return true; 220 } 221 if (o == null || getClass() != o.getClass()) { 222 return false; 223 } 224 225 NitzData nitzData = (NitzData) o; 226 227 if (mZoneOffset != nitzData.mZoneOffset) { 228 return false; 229 } 230 if (mCurrentTimeMillis != nitzData.mCurrentTimeMillis) { 231 return false; 232 } 233 if (!mOriginalString.equals(nitzData.mOriginalString)) { 234 return false; 235 } 236 if (!Objects.equals(mDstOffset, nitzData.mDstOffset)) { 237 return false; 238 } 239 return Objects.equals(mEmulatorHostTimeZone, nitzData.mEmulatorHostTimeZone); 240 } 241 242 @Override hashCode()243 public int hashCode() { 244 int result = mOriginalString.hashCode(); 245 result = 31 * result + mZoneOffset; 246 result = 31 * result + (mDstOffset != null ? mDstOffset.hashCode() : 0); 247 result = 31 * result + Long.hashCode(mCurrentTimeMillis); 248 result = 31 * result + (mEmulatorHostTimeZone != null ? mEmulatorHostTimeZone.hashCode() 249 : 0); 250 return result; 251 } 252 253 @Override toString()254 public String toString() { 255 return "NitzData{" 256 + "mOriginalString=" + mOriginalString 257 + ", mZoneOffset=" + mZoneOffset 258 + ", mDstOffset=" + mDstOffset 259 + ", mCurrentTimeMillis=" + mCurrentTimeMillis 260 + ", mEmulatorHostTimeZone=" + mEmulatorHostTimeZone 261 + '}'; 262 } 263 } 264