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