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