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.LocalDateTime; 25 import java.time.ZoneOffset; 26 import java.util.Objects; 27 import java.util.TimeZone; 28 import java.util.regex.Pattern; 29 30 /** 31 * Represents NITZ data. Various static methods are provided to help with parsing and interpretation 32 * of NITZ data. 33 * 34 * {@hide} 35 */ 36 @VisibleForTesting(visibility = PACKAGE) 37 public final class NitzData { 38 private static final String LOG_TAG = ServiceStateTracker.LOG_TAG; 39 private static final int MS_PER_QUARTER_HOUR = 15 * 60 * 1000; 40 private static final int MS_PER_HOUR = 60 * 60 * 1000; 41 42 /* Time stamp after 19 January 2038 is not supported under 32 bit */ 43 private static final int MAX_NITZ_YEAR = 2037; 44 45 private static final Pattern NITZ_SPLIT_PATTERN = Pattern.compile("[/:,+-]"); 46 47 // Stored For logging / debugging only. 48 private final String mOriginalString; 49 50 private final int mZoneOffset; 51 52 private final Integer mDstOffset; 53 54 private final long mCurrentTimeMillis; 55 56 private final TimeZone mEmulatorHostTimeZone; 57 NitzData(String originalString, int zoneOffsetMillis, Integer dstOffsetMillis, long utcTimeMillis, TimeZone emulatorHostTimeZone)58 private NitzData(String originalString, int zoneOffsetMillis, Integer dstOffsetMillis, 59 long utcTimeMillis, TimeZone emulatorHostTimeZone) { 60 if (originalString == null) { 61 throw new NullPointerException("originalString==null"); 62 } 63 this.mOriginalString = originalString; 64 this.mZoneOffset = zoneOffsetMillis; 65 this.mDstOffset = dstOffsetMillis; 66 this.mCurrentTimeMillis = utcTimeMillis; 67 this.mEmulatorHostTimeZone = emulatorHostTimeZone; 68 } 69 70 /** 71 * Parses the supplied NITZ string, returning the encoded data. 72 */ parse(String nitz)73 public static NitzData parse(String nitz) { 74 // "yy/mm/dd,hh:mm:ss(+/-)tz[,dt[,tzid]]" 75 // tz, dt are in number of quarter-hours 76 77 try { 78 String[] nitzSubs = NITZ_SPLIT_PATTERN.split(nitz); 79 80 int year = 2000 + Integer.parseInt(nitzSubs[0]); 81 if (year > MAX_NITZ_YEAR) { 82 if (ServiceStateTracker.DBG) { 83 Rlog.e(LOG_TAG, "NITZ year: " + year + " exceeds limit, skip NITZ time update"); 84 } 85 return null; 86 } 87 88 int month = Integer.parseInt(nitzSubs[1]); 89 int date = Integer.parseInt(nitzSubs[2]); 90 int hour = Integer.parseInt(nitzSubs[3]); 91 int minute = Integer.parseInt(nitzSubs[4]); 92 int second = Integer.parseInt(nitzSubs[5]); 93 94 /* NITZ time (hour:min:sec) will be in UTC but it supplies the timezone 95 * offset as well (which we won't worry about until later) */ 96 long epochMillis = LocalDateTime.of(year, month, date, hour, minute, second) 97 .toInstant(ZoneOffset.UTC) 98 .toEpochMilli(); 99 100 // The offset received from NITZ is the offset to add to get current local time. 101 boolean sign = (nitz.indexOf('-') == -1); 102 int totalUtcOffsetQuarterHours = Integer.parseInt(nitzSubs[6]); 103 int totalUtcOffsetMillis = 104 (sign ? 1 : -1) * totalUtcOffsetQuarterHours * MS_PER_QUARTER_HOUR; 105 106 // DST correction is already applied to the UTC offset. We could subtract it if we 107 // wanted the raw offset. 108 Integer dstAdjustmentHours = 109 (nitzSubs.length >= 8) ? Integer.parseInt(nitzSubs[7]) : null; 110 Integer dstAdjustmentMillis = null; 111 if (dstAdjustmentHours != null) { 112 dstAdjustmentMillis = dstAdjustmentHours * MS_PER_HOUR; 113 } 114 115 // As a special extension, the Android emulator appends the name of 116 // the host computer's timezone to the nitz string. This is zoneinfo 117 // timezone name of the form Area!Location or Area!Location!SubLocation 118 // so we need to convert the ! into / 119 TimeZone zone = null; 120 if (nitzSubs.length >= 9) { 121 String tzname = nitzSubs[8].replace('!', '/'); 122 zone = TimeZone.getTimeZone(tzname); 123 } 124 return new NitzData(nitz, totalUtcOffsetMillis, dstAdjustmentMillis, epochMillis, zone); 125 } catch (RuntimeException ex) { 126 Rlog.e(LOG_TAG, "NITZ: Parsing NITZ time " + nitz + " ex=" + ex); 127 return null; 128 } 129 } 130 131 /** A method for use in tests to create NitzData instances. */ createForTests(int zoneOffsetMillis, Integer dstOffsetMillis, long utcTimeMillis, TimeZone emulatorHostTimeZone)132 public static NitzData createForTests(int zoneOffsetMillis, Integer dstOffsetMillis, 133 long utcTimeMillis, TimeZone emulatorHostTimeZone) { 134 return new NitzData("Test data", zoneOffsetMillis, dstOffsetMillis, utcTimeMillis, 135 emulatorHostTimeZone); 136 } 137 138 /** 139 * Returns the current time as the number of milliseconds since the beginning of the Unix epoch 140 * (1/1/1970 00:00:00 UTC). 141 */ getCurrentTimeInMillis()142 public long getCurrentTimeInMillis() { 143 return mCurrentTimeMillis; 144 } 145 146 /** 147 * Returns the total offset to apply to the {@link #getCurrentTimeInMillis()} to arrive at a 148 * local time. NITZ is limited in only being able to express total offsets in multiples of 15 149 * minutes. 150 * 151 * <p>Note that some time zones change offset during the year for reasons other than "daylight 152 * savings", e.g. for Ramadan. This is not well handled by most date / time APIs. 153 */ getLocalOffsetMillis()154 public int getLocalOffsetMillis() { 155 return mZoneOffset; 156 } 157 158 /** 159 * Returns the offset (already included in {@link #getLocalOffsetMillis()}) associated with 160 * Daylight Savings Time (DST). This field is optional: {@code null} means the DST offset is 161 * unknown. NITZ is limited in only being able to express DST offsets in positive multiples of 162 * one or two hours. 163 * 164 * <p>Callers should remember that standard time / DST is a matter of convention: it has 165 * historically been assumed by NITZ and many date/time APIs that DST happens in the summer and 166 * the "raw" offset will increase during this time, usually by one hour. However, the tzdb 167 * maintainers have moved to different conventions on a country-by-country basis so that some 168 * summer times are considered the "standard" time (i.e. in this model winter time is the "DST" 169 * and a negative adjustment, usually of (negative) one hour. 170 * 171 * <p>There is nothing that says NITZ and tzdb need to treat DST conventions the same. 172 * 173 * <p>At the time of writing Android date/time APIs are sticking with the historic tzdb 174 * convention that DST is used in summer time and is <em>always</em> a positive offset but this 175 * could change in future. If Android or carriers change the conventions used then it might make 176 * NITZ comparisons with tzdb information more error-prone. 177 * 178 * <p>See also {@link #getLocalOffsetMillis()} for other reasons besides DST that a local offset 179 * may change. 180 */ getDstAdjustmentMillis()181 public Integer getDstAdjustmentMillis() { 182 return mDstOffset; 183 } 184 185 /** 186 * Returns {@link true} if the time is in Daylight Savings Time (DST), {@link false} if it is 187 * unknown or not in DST. See {@link #getDstAdjustmentMillis()}. 188 */ isDst()189 public boolean isDst() { 190 return mDstOffset != null && mDstOffset != 0; 191 } 192 193 194 /** 195 * Returns the time zone of the host computer when Android is running in an emulator. It is 196 * {@code null} for real devices. This information is communicated via a non-standard Android 197 * extension to NITZ. 198 */ getEmulatorHostTimeZone()199 public TimeZone getEmulatorHostTimeZone() { 200 return mEmulatorHostTimeZone; 201 } 202 203 @Override equals(Object o)204 public boolean equals(Object o) { 205 if (this == o) { 206 return true; 207 } 208 if (o == null || getClass() != o.getClass()) { 209 return false; 210 } 211 212 NitzData nitzData = (NitzData) o; 213 214 if (mZoneOffset != nitzData.mZoneOffset) { 215 return false; 216 } 217 if (mCurrentTimeMillis != nitzData.mCurrentTimeMillis) { 218 return false; 219 } 220 if (!mOriginalString.equals(nitzData.mOriginalString)) { 221 return false; 222 } 223 if (!Objects.equals(mDstOffset, nitzData.mDstOffset)) { 224 return false; 225 } 226 return Objects.equals(mEmulatorHostTimeZone, nitzData.mEmulatorHostTimeZone); 227 } 228 229 @Override hashCode()230 public int hashCode() { 231 int result = mOriginalString.hashCode(); 232 result = 31 * result + mZoneOffset; 233 result = 31 * result + (mDstOffset != null ? mDstOffset.hashCode() : 0); 234 result = 31 * result + Long.hashCode(mCurrentTimeMillis); 235 result = 31 * result + (mEmulatorHostTimeZone != null ? mEmulatorHostTimeZone.hashCode() 236 : 0); 237 return result; 238 } 239 240 @Override toString()241 public String toString() { 242 return "NitzData{" 243 + "mOriginalString=" + mOriginalString 244 + ", mZoneOffset=" + mZoneOffset 245 + ", mDstOffset=" + mDstOffset 246 + ", mCurrentTimeMillis=" + mCurrentTimeMillis 247 + ", mEmulatorHostTimeZone=" + mEmulatorHostTimeZone 248 + '}'; 249 } 250 } 251