/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.i18n.timezone; import com.android.i18n.timezone.internal.BufferIterator; import com.android.i18n.timezone.internal.ByteBufferIterator; import libcore.util.NonNull; import libcore.util.Nullable; import java.io.IOException; import java.io.ObjectInputStream.GetField; import java.io.ObjectOutputStream.PutField; import java.io.ObjectStreamField; import java.nio.ByteBuffer; import java.util.Arrays; /** * This class holds the data of a time zone backed by the tzfiles. An instance is immutable. * *

This reads time zone information from a binary file stored on the platform. The binary file * is essentially a single file containing compacted versions of all the tzfiles produced by the * zone info compiler (zic) tool (see {@code man 5 tzfile} for details of the format and * {@code man 8 zic}) and an index by long name, e.g. Europe/London. * *

The compacted form is created by * {@code system/timezone/input_tools/android/zone_compactor/main/java/ZoneCompactor.java} and is * used by both this and Bionic. {@link ZoneInfoDb} is responsible for mapping the binary file, and * reading the index and creating a {@link BufferIterator} that provides access to an entry for a * specific file. This class is responsible for reading the data from that {@link BufferIterator} * and storing it a representation to support the {@link java.util.TimeZone} and * {@link java.util.GregorianCalendar} implementations. See * {@link ZoneInfoData#readTimeZone(String, BufferIterator)}. * *

This class does not use all the information from the {@code tzfile}; it uses: * {@code tzh_timecnt} and the associated transition times and type information. For each type * (described by {@code struct ttinfo}) it uses {@code tt_gmtoff} and {@code tt_isdst}. * * @hide */ @libcore.api.IntraCoreApi @libcore.api.CorePlatformApi public final class ZoneInfoData { /** * The serialized fields in {@link libcore.util.ZoneInfo} kept for backward app compatibility. * * @hide */ @libcore.api.IntraCoreApi public static final @NonNull ObjectStreamField @NonNull [] ZONEINFO_SERIALIZED_FIELDS = new ObjectStreamField[] { new ObjectStreamField("mRawOffset", int.class), new ObjectStreamField("mEarliestRawOffset", int.class), new ObjectStreamField("mTransitions", long[].class), new ObjectStreamField("mTypes", byte[].class), new ObjectStreamField("mOffsets", int[].class), new ObjectStreamField("mIsDsts", byte[].class), }; private final String mId; /** * The (best guess) non-DST offset used "today". It is stored in milliseconds. * See also {@link #mOffsets} which holds values relative to this value, albeit in seconds. */ private final int mRawOffset; /** * The earliest non-DST offset for the zone. It is stored in milliseconds and is absolute, i.e. * it is not relative to mRawOffset. */ private final int mEarliestRawOffset; /** * The times (in Unix epoch time, seconds since 1st Jan 1970 00:00:00 UTC) at which the offsets * changes for any reason, whether that is a change in the offset from UTC or a change in the * DST. * *

These times are pre-calculated externally from a set of rules (both historical and * future) and stored in a file from which {@link ZoneInfoData#readTimeZone(String, * BufferIterator)} reads the data. That is quite different to {@link java.util.SimpleTimeZone}, * which has essentially human readable rules (e.g. DST starts at 01:00 on the first Sunday in * March and ends at 01:00 on the last Sunday in October) that can be used to determine the * DST transition times across a number of years * *

In terms of {@link ZoneInfoData tzfile} structure this array is of length * {@code tzh_timecnt} and contains the times in seconds converted to long to make them safer * to use. * *

They are stored in order from earliest (lowest) time to latest (highest). A transition is * identified by its index within this array. A transition {@code T} is active at a specific * time {@code X} if {@code T} is the highest transition whose time is less than or equal to * {@code X}. * * @see #mTypes */ final long[] mTransitions; /** * The type of the transition, where type is a pair consisting of the offset and whether the * offset includes DST or not. * *

Each transition in {@link #mTransitions} has an associated type in this array at the same * index. The type is an index into the arrays {@link #mOffsets} and {@link #mIsDsts} that each * contain one part of the pair. * *

In the {@link ZoneInfoData tzfile} structure the type array only contains unique instances * of the {@code struct ttinfo} to save space and each type may be referenced by multiple * transitions. However, the type pairs stored in this class are not guaranteed unique because * they do not include the {@code tt_abbrind}, which is the abbreviated identifier to use for * the time zone after the transition. * * @see #mTransitions * @see #mOffsets * @see #mIsDsts */ final byte[] mTypes; /** * The offset parts of the transition types, in seconds. * *

These are actually a delta to the {@link #mRawOffset}. So, if the offset is say +7200 * seconds and {@link #mRawOffset} is say +3600 then this will have a value of +3600. * *

The offset in milliseconds can be computed using: * {@code mRawOffset + mOffsets[type] * 1000} * * @see #mTypes * @see #mIsDsts */ final int[] mOffsets; /** * Specifies whether an associated offset includes DST or not. * *

Each entry in here is 1 if the offset at the same index in {@link #mOffsets} includes DST * and 0 otherwise. * * @see #mTypes * @see #mOffsets */ final byte[] mIsDsts; private ZoneInfoData(String id, int rawOffset, int earliestRawOffset, long[] transitions, byte[] types, int[] offsets, byte[] isDsts) { mId = id; mRawOffset = rawOffset; mEarliestRawOffset = earliestRawOffset; mTransitions = transitions; mTypes = types; mOffsets = offsets; mIsDsts = isDsts; } /** * Copy constructor */ private ZoneInfoData(ZoneInfoData that) { this(that, that.mRawOffset); } /** * Copy constructor with a new raw offset. */ private ZoneInfoData(ZoneInfoData that, int newRawOffset) { mRawOffset = newRawOffset; mId = that.mId; mEarliestRawOffset = that.mEarliestRawOffset; mTransitions = that.mTransitions == null ? null : that.mTransitions.clone(); mTypes = that.mTypes == null ? null : that.mTypes.clone(); mOffsets = that.mOffsets == null ? null : that.mOffsets.clone(); mIsDsts = that.mIsDsts == null ? null : that.mIsDsts.clone(); } // VisibleForTesting public static ZoneInfoData readTimeZone(String id, BufferIterator it) throws IOException { // Skip over the superseded 32-bit header and data. skipOver32BitData(id, it); // Read the v2+ 64-bit header and data. return read64BitData(id, it); } /** * Skip over the 32-bit data with some minimal validation to make sure sure we reading a valid * and supported file. */ private static void skipOver32BitData(String id, BufferIterator it) throws IOException { // Variable names beginning tzh_ correspond to those in "tzfile.h". // Check tzh_magic. int tzh_magic = it.readInt(); if (tzh_magic != 0x545a6966) { // "TZif" throw new IOException("Timezone id=" + id + " has an invalid header=" + tzh_magic); } byte tzh_version = it.readByte(); checkTzifVersionAcceptable(id, tzh_version); // Skip the unused bytes. it.skip(15); // Read the header values necessary to read through all the 32-bit data. int tzh_ttisgmtcnt = it.readInt(); int tzh_ttisstdcnt = it.readInt(); int tzh_leapcnt = it.readInt(); int tzh_timecnt = it.readInt(); int tzh_typecnt = it.readInt(); int tzh_charcnt = it.readInt(); // Skip transitions data, 4 bytes for each 32-bit time + 1 byte for isDst. final int transitionInfoSize = 4 + 1; it.skip(tzh_timecnt * transitionInfoSize); // Skip ttinfos. // struct ttinfo { // int32_t tt_gmtoff; // unsigned char tt_isdst; // unsigned char tt_abbrind; // }; final int ttinfoSize = 4 + 1 + 1; it.skip(tzh_typecnt * ttinfoSize); // Skip tzh_charcnt time zone abbreviations. it.skip(tzh_charcnt); // Skip tzh_leapcnt repetitions of a 32-bit time + a 32-bit correction. int leapInfoSize = 4 + 4; it.skip(tzh_leapcnt * leapInfoSize); // Skip ttisstds and ttisgmts information. These can be ignored for our usecases as per // https://mm.icann.org/pipermail/tz/2006-February/013359.html it.skip(tzh_ttisstdcnt + tzh_ttisgmtcnt); } /** * Read the 64-bit header and data for {@code id} from the current position of {@code it} and * return a ZoneInfo. */ private static ZoneInfoData read64BitData(String id, BufferIterator it) throws IOException { // Variable names beginning tzh_ correspond to those in "tzfile.h". // Check tzh_magic. int tzh_magic = it.readInt(); if (tzh_magic != 0x545a6966) { // "TZif" throw new IOException("Timezone id=" + id + " has an invalid header=" + tzh_magic); } byte tzh_version = it.readByte(); checkTzifVersionAcceptable(id, tzh_version); // Skip the uninteresting parts of the header. it.skip(27); // Read the sizes of the arrays we're about to read. int tzh_timecnt = it.readInt(); // Arbitrary ceiling to prevent allocating memory for corrupt data. final int MAX_TRANSITIONS = 2000; if (tzh_timecnt < 0 || tzh_timecnt > MAX_TRANSITIONS) { throw new IOException( "Timezone id=" + id + " has an invalid number of transitions=" + tzh_timecnt); } int tzh_typecnt = it.readInt(); final int MAX_TYPES = 256; if (tzh_typecnt < 1) { throw new IOException("ZoneInfo requires at least one type " + "to be provided for each timezone but could not find one for '" + id + "'"); } else if (tzh_typecnt > MAX_TYPES) { throw new IOException( "Timezone with id " + id + " has too many types=" + tzh_typecnt); } it.skip(4); // Skip tzh_charcnt. long[] transitions64 = new long[tzh_timecnt]; it.readLongArray(transitions64, 0, transitions64.length); for (int i = 0; i < tzh_timecnt; ++i) { if (i > 0 && transitions64[i] <= transitions64[i - 1]) { throw new IOException( id + " transition at " + i + " is not sorted correctly, is " + transitions64[i] + ", previous is " + transitions64[i - 1]); } } byte[] types = new byte[tzh_timecnt]; it.readByteArray(types, 0, types.length); for (int i = 0; i < types.length; i++) { int typeIndex = types[i] & 0xff; if (typeIndex >= tzh_typecnt) { throw new IOException( id + " type at " + i + " is not < " + tzh_typecnt + ", is " + typeIndex); } } int[] gmtOffsets = new int[tzh_typecnt]; byte[] isDsts = new byte[tzh_typecnt]; for (int i = 0; i < tzh_typecnt; ++i) { gmtOffsets[i] = it.readInt(); byte isDst = it.readByte(); if (isDst != 0 && isDst != 1) { throw new IOException(id + " dst at " + i + " is not 0 or 1, is " + isDst); } isDsts[i] = isDst; // We skip the abbreviation index. This would let us provide historically-accurate // time zone abbreviations (such as "AHST", "YST", and "AKST" for standard time in // America/Anchorage in 1982, 1983, and 1984 respectively). ICU only knows the current // names, though, so even if we did use this data to provide the correct abbreviations // for en_US, we wouldn't be able to provide correct abbreviations for other locales, // nor would we be able to provide correct long forms (such as "Yukon Standard Time") // for any locale. (The RI doesn't do any better than us here either.) it.skip(1); } return new ZoneInfoData(id, transitions64, types, gmtOffsets, isDsts); } private static void checkTzifVersionAcceptable(String id, byte tzh_version) throws IOException { char tzh_version_char = (char) tzh_version; // Version >= 2 is required because the 64-bit time section is required. v3 is the latest // version known at the time of writing and is identical to v2 in the parts used by this // class. if (tzh_version_char != '2' && tzh_version_char != '3') { throw new IOException( "Timezone id=" + id + " has an invalid format version=\'" + tzh_version_char + "\' (" + tzh_version + ")"); } } private ZoneInfoData(String name, long[] transitions, byte[] types, int[] gmtOffsets, byte[] isDsts) { if (gmtOffsets.length == 0) { throw new IllegalArgumentException("ZoneInfo requires at least one offset " + "to be provided for each timezone but could not find one for '" + name + "'"); } mTransitions = transitions; mTypes = types; mIsDsts = isDsts; mId = name; // Find the latest standard offsets (if any). int lastStdTransitionIndex = -1; for (int i = mTransitions.length - 1; lastStdTransitionIndex == -1 && i >= 0; --i) { int typeIndex = mTypes[i] & 0xff; if (lastStdTransitionIndex == -1 && mIsDsts[typeIndex] == 0) { lastStdTransitionIndex = i; } } final int rawOffsetInSeconds; // Use the latest non-daylight offset (if any) as the raw offset. if (mTransitions.length == 0) { // This case is no longer expected to occur in the data used on Android after changes // made in zic version 2014c. It is kept as a fallback. // If there are no transitions then use the first GMT offset. rawOffsetInSeconds = gmtOffsets[0]; } else { if (lastStdTransitionIndex == -1) { throw new IllegalStateException( "ZoneInfo requires at least one non-DST " + "transition to be provided for each timezone that has at least one " + "transition but could not find one for '" + name + "'"); } rawOffsetInSeconds = gmtOffsets[mTypes[lastStdTransitionIndex] & 0xff]; } // From the tzfile docs (Jan 2019): // The localtime(3) function uses the first standard-time ttinfo structure // in the file (or simply the first ttinfo structure in the absence of a // standard-time structure) if either tzh_timecnt is zero or the time // argument is less than the first transition time recorded in the file. // // Cache the raw offset associated with the first nonDst type, in case we're asked about // times that predate our transition data. Android falls back to mRawOffset if there are // only DST ttinfo structures (assumed rare). int firstStdTypeIndex = -1; for (int i = 0; i < mIsDsts.length; ++i) { if (mIsDsts[i] == 0) { firstStdTypeIndex = i; break; } } int earliestRawOffset = (firstStdTypeIndex != -1) ? gmtOffsets[firstStdTypeIndex] : rawOffsetInSeconds; // Rather than keep offsets from UTC, we use offsets from local time, so the raw offset // can be changed in the new instance and automatically affects all the offsets. mOffsets = gmtOffsets; for (int i = 0; i < mOffsets.length; i++) { mOffsets[i] -= rawOffsetInSeconds; } // tzdata uses seconds, but Java uses milliseconds. mRawOffset = rawOffsetInSeconds * 1000; mEarliestRawOffset = earliestRawOffset * 1000; } /** * Create an instance from the serialized fields from {@link libcore.util.ZoneInfo} * for backward app compatibility. * * @hide */ @libcore.api.IntraCoreApi public static @NonNull ZoneInfoData createFromSerializationFields(@NonNull String id, @NonNull GetField getField) throws IOException { int rawOffset = getField.get("mRawOffset", 0); int earliestRawOffset = getField.get("mEarliestRawOffset", 0); long[] transitions = (long[]) getField.get("mTransitions", null); byte[] types = (byte[]) getField.get("mTypes", null); int[] offsets = (int[]) getField.get("mOffsets", null); byte[] isDsts = (byte[]) getField.get("mIsDsts", null); return new ZoneInfoData(id, rawOffset, earliestRawOffset, transitions, types, offsets, isDsts); } /** * Serialize {@link libcore.util.ZoneInfo} into backward app compatible form. * * @hide */ @libcore.api.IntraCoreApi public void writeToSerializationFields(@NonNull PutField putField) { putField.put("mRawOffset", mRawOffset); putField.put("mEarliestRawOffset", mEarliestRawOffset); putField.put("mTransitions", mTransitions); putField.put("mTypes", mTypes); putField.put("mOffsets", mOffsets); putField.put("mIsDsts", mIsDsts); } /** * Find the transition in the {@code timezone} in effect at {@code seconds}. * *

Returns an index in the range -1..timeZone.mTransitions.length - 1. -1 is used to * indicate the time is before the first transition. Other values are an index into * timeZone.mTransitions. */ public int findTransitionIndex(long seconds) { int transition = Arrays.binarySearch(mTransitions, seconds); if (transition < 0) { transition = ~transition - 1; if (transition < 0) { return -1; } } return transition; } /** * Finds the index within the {@link #mOffsets}/{@link #mIsDsts} arrays for the specified time * in seconds, since 1st Jan 1970 00:00:00. * @param seconds the time in seconds. * @return -1 if the time is before the first transition, or [0..{@code mOffsets}-1] for the * active offset. */ int findOffsetIndexForTimeInSeconds(long seconds) { int transition = findTransitionIndex(seconds); if (transition < 0) { return -1; } return mTypes[transition] & 0xff; } /** * Finds the index within the {@link #mOffsets}/{@link #mIsDsts} arrays for the specified time * in milliseconds, since 1st Jan 1970 00:00:00.000. * @param millis the time in milliseconds. * @return -1 if the time is before the first transition, or [0..{@code mOffsets}-1] for the * active offset. */ int findOffsetIndexForTimeInMilliseconds(long millis) { // This rounds the time in milliseconds down to the time in seconds. // // It can't just divide a timestamp in millis by 1000 to obtain a transition time in // seconds because / (div) in Java rounds towards zero. Times before 1970 are negative and // if they have a millisecond component then div would result in obtaining a time that is // one second after what we need. // // e.g. dividing -12,001 milliseconds by 1000 would result in -12 seconds. If there was a // transition at -12 seconds then that would be incorrectly treated as being active // for a time of -12,001 milliseconds even though that time is before the transition // should occur. return findOffsetIndexForTimeInSeconds(roundDownMillisToSeconds(millis)); } /** * Converts time in milliseconds into a time in seconds, rounding down to the closest time * in seconds before the time in milliseconds. * *

It's not sufficient to simply divide by 1000 because that rounds towards 0 and so while * for positive numbers it produces a time in seconds that precedes the time in milliseconds * for negative numbers it can produce a time in seconds that follows the time in milliseconds. * *

This basically does the same as {@code (long) Math.floor(millis / 1000.0)} but should be * faster. * * @param millis the time in milliseconds, may be negative. * @return the time in seconds. */ static long roundDownMillisToSeconds(long millis) { if (millis < 0) { // If the time is less than zero then subtract 999 and then divide by 1000 rounding // towards 0 as usual, e.g. // -12345 -> -13344 / 1000 = -13 // -12000 -> -12999 / 1000 = -12 // -12001 -> -13000 / 1000 = -13 return (millis - 999) / 1000; } else { return millis / 1000; } } /** * Converts time in milliseconds into a time in seconds, rounding up to the closest time * in seconds before the time in milliseconds. * *

It's not sufficient to simply divide by 1000 because that rounds towards 0 and so while * for negative numbers it produces a time in seconds that follows the time in milliseconds * for positive numbers it can produce a time in seconds that precedes the time in milliseconds. * *

This basically does the same as {@code (long) Math.ceil(millis / 1000.0)} but should be * faster. * * @param millis the time in milliseconds, may be negative. * @return the time in seconds. */ static long roundUpMillisToSeconds(long millis) { if (millis > 0) { // If the time is greater than zero then add 999 and then divide by 1000 rounding // towards 0 as usual, e.g. // 12345 -> 13344 / 1000 = 13 // 12000 -> 12999 / 1000 = 12 // 12001 -> 13000 / 1000 = 13 return (millis + 999) / 1000; } else { return millis / 1000; } } /** * Get the raw and DST offsets for the specified time in milliseconds since * 1st Jan 1970 00:00:00 UTC. * *

The raw offset, i.e. that part of the total offset which is not due to DST, is stored at * index 0 of the {@code offsets} array and the DST offset, i.e. that part of the offset which * is due to DST is stored at index 1. * * @param unixEpochTimeInMillis the Unix epoch time in milliseconds. * @param offsets the array whose length must be greater than or equal to 2. * @return the total offset which is the sum of the raw and DST offsets. * * @hide */ @libcore.api.IntraCoreApi public int getOffsetsByUtcTime(long unixEpochTimeInMillis, @NonNull int[] offsets) { int transitionIndex = findTransitionIndex(roundDownMillisToSeconds(unixEpochTimeInMillis)); int totalOffset; int rawOffset; int dstOffset; if (transitionIndex == -1) { // See getOffset(long) and inDaylightTime(Date) for an explanation as to why these // values are used for times before the first transition. rawOffset = mEarliestRawOffset; dstOffset = 0; totalOffset = rawOffset; } else { int type = mTypes[transitionIndex] & 0xff; // Get the total offset used for the transition. totalOffset = mRawOffset + mOffsets[type] * 1000; if (mIsDsts[type] == 0) { // Offset does not include DST so DST is 0 and the raw offset is the total offset. rawOffset = totalOffset; dstOffset = 0; } else { // Offset does include DST, we need to find the preceding transition that did not // include the DST offset so that we can calculate the DST offset. rawOffset = -1; for (transitionIndex -= 1; transitionIndex >= 0; --transitionIndex) { type = mTypes[transitionIndex] & 0xff; if (mIsDsts[type] == 0) { rawOffset = mRawOffset + mOffsets[type] * 1000; break; } } // If no previous transition was found then use the earliest raw offset. if (rawOffset == -1) { rawOffset = mEarliestRawOffset; } // The DST offset is the difference between the total and the raw offset. dstOffset = totalOffset - rawOffset; } } offsets[0] = rawOffset; offsets[1] = dstOffset; return totalOffset; } /** * Returns the offset from UTC in milliseconds at the specified time {@code whenMillis}. * * @param whenMillis the Unix epoch time in milliseconds since 1st Jan 1970, 00:00:00 UTC * * @hide */ @libcore.api.IntraCoreApi public int getOffset(long whenMillis) { int offsetIndex = findOffsetIndexForTimeInMilliseconds(whenMillis); if (offsetIndex == -1) { // Assume that all times before our first transition correspond to the // oldest-known non-daylight offset. The obvious alternative would be to // use the current raw offset, but that seems like a greater leap of faith. return mEarliestRawOffset; } return mRawOffset + mOffsets[offsetIndex] * 1000; } /** * Returns whether the given {@code whenMillis} is in daylight saving time in this time zone. * * @param whenMillis the Unix epoch time in milliseconds since 1st Jan 1970, 00:00:00 UTC * * @hide */ @libcore.api.IntraCoreApi public boolean isInDaylightTime(long whenMillis) { int offsetIndex = findOffsetIndexForTimeInMilliseconds(whenMillis); if (offsetIndex == -1) { // Assume that all times before our first transition are non-daylight. // Transition data tends to start with a transition to daylight, so just // copying the first transition would assume the opposite. // http://code.google.com/p/android/issues/detail?id=14395 return false; } return mIsDsts[offsetIndex] == 1; } /** * Returns the raw offset in milliseconds. The value is not affected by daylight saving. * * @hide */ @libcore.api.IntraCoreApi public int getRawOffset() { return mRawOffset; } /** * Returns the offset of daylight saving in milliseconds in the latest Daylight Savings Time * after the time {@code whenMillis}. If no known DST occurs after {@code whenMillis}, it * returns {@code null}. * * @param whenMillis the Unix epoch time in milliseconds since 1st Jan 1970, 00:00:00 UTC * * @hide */ @libcore.api.IntraCoreApi public @Nullable Integer getLatestDstSavingsMillis(long whenMillis) { // Find the latest daylight and standard offsets (if any). int lastStdTransitionIndex = -1; int lastDstTransitionIndex = -1; for (int i = mTransitions.length - 1; (lastStdTransitionIndex == -1 || lastDstTransitionIndex == -1) && i >= 0; --i) { int typeIndex = mTypes[i] & 0xff; if (lastStdTransitionIndex == -1 && mIsDsts[typeIndex] == 0) { lastStdTransitionIndex = i; } if (lastDstTransitionIndex == -1 && mIsDsts[typeIndex] != 0) { lastDstTransitionIndex = i; } } if (lastDstTransitionIndex != -1) { // Check to see if the last DST transition is in the future or the past. If it is in // the past then we treat it as if it doesn't exist, at least for the purposes of // TimeZone#useDaylightTime() and #getDSTSavings() long lastDSTTransitionTime = mTransitions[lastDstTransitionIndex]; // Convert the current time in millis into seconds. Unlike other places that convert // time in milliseconds into seconds in order to compare with transition time this // rounds up rather than down. It does that because this is interested in what // transitions apply in future long currentUnixTimeSeconds = roundUpMillisToSeconds(whenMillis); // Is this zone observing DST currently or in the future? // We don't care if they've historically used it: most places have at least once. // See http://b/36905574. // This test means that for somewhere like Morocco, which tried DST in 2009 but has // no future plans (and thus no future schedule info) will report "true" from // useDaylightTime at the start of 2009 but "false" at the end. This seems appropriate. if (lastDSTTransitionTime < currentUnixTimeSeconds) { // The last DST transition is before now so treat it as if it doesn't exist. lastDstTransitionIndex = -1; } } final Integer dstSavings; if (lastDstTransitionIndex == -1) { // There were no DST transitions or at least no future DST transitions so DST is not // used. dstSavings = null; } else { // Use the latest transition's pair of offsets to compute the DST savings. // This isn't generally useful, but it's exposed by TimeZone.getDSTSavings. int lastGmtOffset = mOffsets[mTypes[lastStdTransitionIndex] & 0xff]; int lastDstOffset = mOffsets[mTypes[lastDstTransitionIndex] & 0xff]; dstSavings = (lastDstOffset - lastGmtOffset) * 1000; } return dstSavings; } int getEarliestRawOffset() { return mEarliestRawOffset; } /** * Returns {@code true} if 2 time zones have the same time zone rule. * * @hide */ @libcore.api.IntraCoreApi public boolean hasSameRules(@NonNull ZoneInfoData other) { return mRawOffset == other.mRawOffset // Arrays.equals returns true if both arrays are null && Arrays.equals(mOffsets, other.mOffsets) && Arrays.equals(mIsDsts, other.mIsDsts) && Arrays.equals(mTypes, other.mTypes) && Arrays.equals(mTransitions, other.mTransitions); } @Override public boolean equals(Object obj) { if (!(obj instanceof ZoneInfoData)) { return false; } ZoneInfoData other = (ZoneInfoData) obj; return getID().equals(other.getID()) && hasSameRules(other); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getID().hashCode(); result = prime * result + Arrays.hashCode(mOffsets); result = prime * result + Arrays.hashCode(mIsDsts); result = prime * result + mRawOffset; result = prime * result + Arrays.hashCode(mTransitions); result = prime * result + Arrays.hashCode(mTypes); return result; } /** * Returns a string containing the internal states for debug purpose. */ @Override public String toString() { return "[id=\"" + getID() + "\"" + ",mRawOffset=" + mRawOffset + ",mEarliestRawOffset=" + mEarliestRawOffset + ",transitions=" + mTransitions.length + "]"; } /** * Returns the time zone id. * * @hide */ @libcore.api.CorePlatformApi @libcore.api.IntraCoreApi public @NonNull String getID() { return mId; } /** * Create a deep copy of this object with a new raw offset. * * @hide */ @libcore.api.IntraCoreApi public @NonNull ZoneInfoData createCopyWithRawOffset(int newRawOffset) { return new ZoneInfoData(this, newRawOffset); } /** * Returns the Unix epoch times (in seconds since 1st Jan 1970 00:00:00 UTC) at which the * offsets changes for any reason, whether that is a change in the offset from UTC or a change * in the DST. * * WARNING: This API is exposed only for app compat usage in @link libcore.util.ZoneInfo}. * * @hide */ @libcore.api.IntraCoreApi public @Nullable long[] getTransitions() { return mTransitions == null ? null : mTransitions.clone(); } /** * Creates an instance. This method is only for testing purposes. * * @param transitions The times (in seconds) since beginning of the Unix epoch at which * the offsets changes * @param types the type of the transition. The offsets and standard/daylight states are * represented in the corresponding entry in offsets and * isDsts respectively * @param offsets the total offsets of each type. The max allowed size of this array is 256. * @param isDsts an entry is {@code true} if the type is daylight saving time. The max allowed * size of this array is 256. * @hide */ @libcore.api.IntraCoreApi public static @NonNull ZoneInfoData createInstance(@NonNull String id, @NonNull long[] transitions, @NonNull byte[] types, @NonNull int[] offsets, @NonNull boolean[] isDsts) { return new ZoneInfoData(id, transitions, types, offsets, toByteArray(isDsts)); } private static byte[] toByteArray(boolean[] isDsts) { byte[] result = new byte[isDsts.length]; for (int i = 0; i < isDsts.length; i++) { result[i] = (byte) (isDsts[i] ? 1 : 0); } return result; } }