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