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