1 /*
2  * Copyright (C) 2007 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  * Elements of the WallTime class are a port of Bionic's localtime.c to Java. That code had the
18  * following header:
19  *
20  * This file is in the public domain, so clarified as of
21  * 1996-06-05 by Arthur David Olson.
22  */
23 package libcore.util;
24 
25 import android.compat.annotation.UnsupportedAppUsage;
26 
27 import com.android.i18n.timezone.ZoneInfoData;
28 import java.io.IOException;
29 import java.io.ObjectInputStream;
30 import java.io.ObjectInputStream.GetField;
31 import java.io.ObjectOutputStream;
32 import java.io.ObjectOutputStream.PutField;
33 import java.io.ObjectStreamField;
34 import java.lang.reflect.Field;
35 import java.util.Arrays;
36 import java.util.Date;
37 import java.util.Objects;
38 import java.util.TimeZone;
39 
40 /**
41  *  Our concrete TimeZone implementation, backed by a {@link ZoneInfoData}. This class is not
42  *  thread-safe.
43  *
44  * This class exists in this package and has certain fields / a defined serialization footprint for
45  * app compatibility reasons. The knowledge of the underlying file format has been split out into
46  * {@link ZoneInfoData} which is intended to be updated independently of the classes in
47  * libcore.util.
48  *
49  * @hide - used to implement TimeZone
50  */
51 public final class ZoneInfo extends TimeZone {
52     private static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
53     private static final long MILLISECONDS_PER_400_YEARS =
54             MILLISECONDS_PER_DAY * (400 * 365 + 100 - 3);
55 
56     private static final long UNIX_OFFSET = 62167219200000L;
57 
58     private static final int[] NORMAL = new int[] {
59             0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
60     };
61 
62     private static final int[] LEAP = new int[] {
63             0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335,
64     };
65 
66     // Proclaim serialization compatibility with pre-OpenJDK AOSP
67     static final long serialVersionUID = -4598738130123921552L;
68 
69     /**
70      * Keep the serialization compatibility even though most of the fields have been moved to
71      * {@link ZoneInfoData}. Originally, Android just had the Serializable subclass of TimeZone,
72      * ZoneInfo. This has been split into two: ZoneInfoData is now responsible for the immutable
73      * data fields, and this class holds the rest.
74      */
75     private static final ObjectStreamField[] serialPersistentFields;
76 
77     static {
78         int srcLen = ZoneInfoData.ZONEINFO_SERIALIZED_FIELDS.length;
79         serialPersistentFields = new ObjectStreamField[2 + srcLen];
80         // Serialize mDstSavings and mUseDst fields, but not mTransitions because the
81         // ZoneInfoData delegate should handle it.
82         serialPersistentFields[0] = new ObjectStreamField("mDstSavings", int.class);
83         serialPersistentFields[1] = new ObjectStreamField("mUseDst", boolean.class);
System.arraycopy(ZoneInfoData.ZONEINFO_SERIALIZED_FIELDS, 0 , serialPersistentFields, 2 , srcLen )84         System.arraycopy(ZoneInfoData.ZONEINFO_SERIALIZED_FIELDS, 0 /* srcPos */,
85                 serialPersistentFields, 2 /* destPos */, srcLen /* length */);
86     }
87 
88     /**
89      * Implements {@link #useDaylightTime()}
90      *
91      * <p>True if the transition active at the time this instance was created, or future
92      * transitions support DST. It is possible that caching this value at construction time and
93      * using it for the lifetime of the instance does not match the contract of the
94      * {@link java.util.TimeZone#useDaylightTime()} method but it appears to be what the RI does
95      * and that method is not particularly useful when it comes to historical or future times as it
96      * does not allow the time to be specified.
97      *
98      * <p>When this is false then {@link #mDstSavings} will be 0.
99      *
100      * @see #mDstSavings
101      */
102     private final boolean mUseDst;
103 
104     /**
105      * Implements {@link #getDSTSavings()}
106      *
107      * @see #mUseDst
108      */
109     private final int mDstSavings;
110 
111     /**
112      * This field is kept only for app compatibility indicated by @UnsupportedAppUsage. Do not
113      * modify the content of this array as it is a reference to an internal data structure used by
114      * mDelegate.
115      */
116     @UnsupportedAppUsage
117     private final long[] mTransitions;
118 
119     /**
120      * Despite being transient, mDelegate is still serialized as part of this object. Please
121      * see {@link #readObject(ObjectInputStream)} and {@link #writeObject(ObjectOutputStream)}
122      */
123     private transient ZoneInfoData mDelegate;
124 
125     /**
126      * Creates an instance using the current system clock time to calculate the {@link #mDstSavings}
127      * and {@link #mUseDst} fields. See also {@link #createZoneInfo(ZoneInfoData, long)}.
128      */
createZoneInfo(ZoneInfoData delegate)129     public static ZoneInfo createZoneInfo(ZoneInfoData delegate) {
130         return createZoneInfo(delegate, System.currentTimeMillis());
131     }
132 
133     /**
134      * Creates an instance and recalculate the fields {@link #mDstSavings} and {@link #mUseDst} from
135      * the {@code timeInMillis}.
136      */
137     // VisibleForTesting
createZoneInfo(ZoneInfoData delegate, long timeInMillis)138     public static ZoneInfo createZoneInfo(ZoneInfoData delegate, long timeInMillis) {
139         Integer latestDstSavings = delegate.getLatestDstSavingsMillis(timeInMillis);
140         boolean useDst = latestDstSavings != null;
141         int dstSavings = latestDstSavings == null ? 0 : latestDstSavings;
142         return new ZoneInfo(delegate, dstSavings, useDst);
143     }
144 
ZoneInfo(ZoneInfoData delegate, int dstSavings, boolean useDst)145     private ZoneInfo(ZoneInfoData delegate, int dstSavings, boolean useDst) {
146         mDelegate = delegate;
147         mDstSavings = dstSavings;
148         mUseDst = useDst;
149         mTransitions = delegate.getTransitions();
150         setID(delegate.getID());
151     }
152 
readObject(ObjectInputStream in)153     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
154         GetField getField = in.readFields();
155         // TimeZone#getID() should return the proper ID because the fields in the superclass should
156         // have been deserialized.
157         mDelegate = ZoneInfoData.createFromSerializationFields(getID(), getField);
158 
159         // Set the final fields by reflection.
160         boolean useDst = getField.get("mUseDst", false);
161         int dstSavings = getField.get("mDstSavings", 0);
162         long[] transitions = mDelegate.getTransitions();
163         /** For pre-OpenJDK compatibility, ensure that when deserializing an instance that
164          * {@link #mDstSavings} is always 0 when {@link #mUseDst} is false
165          */
166         if (!useDst && dstSavings != 0) {
167             dstSavings = 0;
168         }
169         int finalDstSavings = dstSavings;
170         setFinalField("mDstSavings", (f -> f.setInt(this, finalDstSavings)));
171         setFinalField("mUseDst", (f -> f.setBoolean(this, useDst)));
172         setFinalField("mTransitions", (f -> f.set(this, transitions)));
173     }
174 
writeObject(ObjectOutputStream out)175     private void writeObject(ObjectOutputStream out) throws IOException {
176         PutField putField = out.putFields();
177         putField.put("mUseDst", mUseDst);
178         putField.put("mDstSavings", mDstSavings);
179         // mDelegate writes the field mTransitions.
180         mDelegate.writeToSerializationFields(putField);
181         out.writeFields();
182     }
183 
setFinalField(String field, FieldSetter setter)184     private static void setFinalField(String field, FieldSetter setter) {
185         try {
186             Field mTransitionsField = ZoneInfo.class.getDeclaredField(field);
187             mTransitionsField.setAccessible(true);
188             setter.set(mTransitionsField);
189         } catch (ReflectiveOperationException e) {
190             // The field should always exist because it's a member field in this class.
191         }
192     }
193 
194     private interface FieldSetter {
set(Field field)195         void set(Field field) throws IllegalArgumentException, IllegalAccessException;
196     }
197 
198     @Override
getOffset(int era, int year, int month, int day, int dayOfWeek, int millis)199     public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) {
200         // XXX This assumes Gregorian always; Calendar switches from
201         // Julian to Gregorian in 1582.  What calendar system are the
202         // arguments supposed to come from?
203 
204         long calc = (year / 400) * MILLISECONDS_PER_400_YEARS;
205         year %= 400;
206 
207         calc += year * (365 * MILLISECONDS_PER_DAY);
208         calc += ((year + 3) / 4) * MILLISECONDS_PER_DAY;
209 
210         if (year > 0) {
211             calc -= ((year - 1) / 100) * MILLISECONDS_PER_DAY;
212         }
213 
214         boolean isLeap = (year == 0 || (year % 4 == 0 && year % 100 != 0));
215         int[] mlen = isLeap ? LEAP : NORMAL;
216 
217         calc += mlen[month] * MILLISECONDS_PER_DAY;
218         calc += (day - 1) * MILLISECONDS_PER_DAY;
219         calc += millis;
220 
221         calc -= mDelegate.getRawOffset();
222         calc -= UNIX_OFFSET;
223 
224         return mDelegate.getOffset(calc);
225     }
226 
227     @Override
getOffset(long when)228     public int getOffset(long when) {
229         return mDelegate.getOffset(when);
230     }
231 
232     @Override
inDaylightTime(Date time)233     public boolean inDaylightTime(Date time) {
234         return mDelegate.isInDaylightTime(time.getTime());
235     }
236 
237     @Override
getRawOffset()238     public int getRawOffset() {
239         return mDelegate.getRawOffset();
240     }
241 
242     @Override
setRawOffset(int off)243     public void setRawOffset(int off) {
244         mDelegate = mDelegate.createCopyWithRawOffset(off);
245     }
246 
247     @Override
getDSTSavings()248     public int getDSTSavings() {
249         return mDstSavings;
250     }
251 
252     @Override
useDaylightTime()253     public boolean useDaylightTime() {
254         return mUseDst;
255     }
256 
257     @Override
hasSameRules(TimeZone timeZone)258     public boolean hasSameRules(TimeZone timeZone) {
259         if (!(timeZone instanceof ZoneInfo)) {
260             return false;
261         }
262         ZoneInfo other = (ZoneInfo) timeZone;
263 
264         if (mUseDst != other.mUseDst) {
265             return false;
266         }
267         if (!mUseDst) {
268             return mDelegate.getRawOffset() == other.getRawOffset();
269         }
270         return mDelegate.hasSameRules(other.mDelegate);
271     }
272 
273     @Override
equals(Object obj)274     public boolean equals(Object obj) {
275         if (!(obj instanceof ZoneInfo)) {
276             return false;
277         }
278         ZoneInfo other = (ZoneInfo) obj;
279         return getID().equals(other.getID()) && hasSameRules(other);
280     }
281 
282     @Override
hashCode()283     public int hashCode() {
284         /*
285          * TODO(http://b/173499812): Can 2 ZoneInfo objects have different hashCode but equals?
286          * mDelegate.hashCode compares more fields than rules and ID.
287          */
288         return Objects.hash(mUseDst, mDelegate);
289     }
290 
291     @Override
toString()292     public String toString() {
293         return getClass().getName() +
294                 "[mDstSavings=" + mDstSavings +
295                 ",mUseDst=" + mUseDst +
296                 ",mDelegate=" + mDelegate.toString() + "]";
297     }
298 
299     @Override
clone()300     public Object clone() {
301         // Pass the mDstSavings and mUseDst explicitly because they must not be recalculated when
302         // cloning. See {@link #create(ZoneInfoData)}.
303         return new ZoneInfo(mDelegate, mDstSavings, mUseDst);
304     }
305 
getOffsetsByUtcTime(long utcTimeInMillis, int[] offsets)306     public int getOffsetsByUtcTime(long utcTimeInMillis, int[] offsets) {
307         return mDelegate.getOffsetsByUtcTime(utcTimeInMillis, offsets);
308     }
309 }
310