| /* |
| * 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. |
| * |
| * <p>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. |
| * |
| * <p>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)}. |
| * |
| * <p>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. |
| * |
| * <p>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 |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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}. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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 <code>offsets</code> and |
| * <code>isDsts</code> 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; |
| } |
| } |