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 android.telephony.Rlog;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 
25 import java.util.Calendar;
26 import java.util.TimeZone;
27 
28 /**
29  * Represents NITZ data. Various static methods are provided to help with parsing and intepretation
30  * of NITZ data.
31  *
32  * {@hide}
33  */
34 @VisibleForTesting(visibility = PACKAGE)
35 public final class NitzData {
36     private static final String LOG_TAG = ServiceStateTracker.LOG_TAG;
37     private static final int MS_PER_QUARTER_HOUR = 15 * 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 dstAdjustmentQuarterHours =
115                     (nitzSubs.length >= 8) ? Integer.parseInt(nitzSubs[7]) : null;
116             Integer dstAdjustmentMillis = null;
117             if (dstAdjustmentQuarterHours != null) {
118                 dstAdjustmentMillis = dstAdjustmentQuarterHours * MS_PER_QUARTER_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.
156      */
getLocalOffsetMillis()157     public int getLocalOffsetMillis() {
158         return mZoneOffset;
159     }
160 
161     /**
162      * Returns the offset (already included in {@link #getLocalOffsetMillis()}) associated with
163      * Daylight Savings Time (DST). This field is optional: {@code null} means the DST offset is
164      * unknown.
165      */
getDstAdjustmentMillis()166     public Integer getDstAdjustmentMillis() {
167         return mDstOffset;
168     }
169 
170     /**
171      * Returns {@link true} if the time is in Daylight Savings Time (DST), {@link false} if it is
172      * unknown or not in DST. See {@link #getDstAdjustmentMillis()}.
173      */
isDst()174     public boolean isDst() {
175         return mDstOffset != null && mDstOffset != 0;
176     }
177 
178 
179     /**
180      * Returns the time zone of the host computer when Android is running in an emulator. It is
181      * {@code null} for real devices. This information is communicated via a non-standard Android
182      * extension to NITZ.
183      */
getEmulatorHostTimeZone()184     public TimeZone getEmulatorHostTimeZone() {
185         return mEmulatorHostTimeZone;
186     }
187 
188     @Override
equals(Object o)189     public boolean equals(Object o) {
190         if (this == o) {
191             return true;
192         }
193         if (o == null || getClass() != o.getClass()) {
194             return false;
195         }
196 
197         NitzData nitzData = (NitzData) o;
198 
199         if (mZoneOffset != nitzData.mZoneOffset) {
200             return false;
201         }
202         if (mCurrentTimeMillis != nitzData.mCurrentTimeMillis) {
203             return false;
204         }
205         if (!mOriginalString.equals(nitzData.mOriginalString)) {
206             return false;
207         }
208         if (mDstOffset != null ? !mDstOffset.equals(nitzData.mDstOffset)
209                 : nitzData.mDstOffset != null) {
210             return false;
211         }
212         return mEmulatorHostTimeZone != null ? mEmulatorHostTimeZone
213                 .equals(nitzData.mEmulatorHostTimeZone) : nitzData.mEmulatorHostTimeZone == null;
214     }
215 
216     @Override
hashCode()217     public int hashCode() {
218         int result = mOriginalString.hashCode();
219         result = 31 * result + mZoneOffset;
220         result = 31 * result + (mDstOffset != null ? mDstOffset.hashCode() : 0);
221         result = 31 * result + (int) (mCurrentTimeMillis ^ (mCurrentTimeMillis >>> 32));
222         result = 31 * result + (mEmulatorHostTimeZone != null ? mEmulatorHostTimeZone.hashCode()
223                 : 0);
224         return result;
225     }
226 
227     @Override
toString()228     public String toString() {
229         return "NitzData{"
230                 + "mOriginalString=" + mOriginalString
231                 + ", mZoneOffset=" + mZoneOffset
232                 + ", mDstOffset=" + mDstOffset
233                 + ", mCurrentTimeMillis=" + mCurrentTimeMillis
234                 + ", mEmulatorHostTimeZone=" + mEmulatorHostTimeZone
235                 + '}';
236     }
237 }
238