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
17package com.android.internal.telephony;
18
19import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
20
21import android.telephony.Rlog;
22
23import com.android.internal.annotations.VisibleForTesting;
24
25import java.util.Calendar;
26import 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)
35public 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
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     */
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. */
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     */
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     */
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     */
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     */
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     */
184    public TimeZone getEmulatorHostTimeZone() {
185        return mEmulatorHostTimeZone;
186    }
187
188    @Override
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
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
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