/* * 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. */ /* * Elements of the WallTime class are a port of Bionic's localtime.c to Java. That code had the * following header: * * This file is in the public domain, so clarified as of * 1996-06-05 by Arthur David Olson. */ package com.android.i18n.timezone; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.TimeZone; /** * A class that represents a "wall time". This class is modeled on the C tm struct and * is used to support android.text.format.Time behavior. Unlike the tm struct the year is * represented as the full year, not the years since 1900. * *

This class contains a rewrite of various native functions that android.text.format.Time * once relied on such as mktime_tz and localtime_tz. This replacement does not support leap * seconds but does try to preserve behavior around ambiguous date/times found in the BSD * version of mktime that was previously used. * *

The original native code used a 32-bit value for time_t on 32-bit Android, which * was the only variant of Android available at the time. To preserve old behavior this code * deliberately uses {@code int} rather than {@code long} for most things and performs * calculations in seconds. This creates deliberate truncation issues for date / times before * 1901 and after 2038. This is intentional but might be fixed in future if all the knock-ons * can be resolved: Application code may have come to rely on the range so previously values * like zero for year could indicate an invalid date but if we move to long the year zero would * be valid. * *

All offsets are considered to be safe for addition / subtraction / multiplication without * worrying about overflow. All absolute time arithmetic is checked for overflow / underflow. * * @hide */ @libcore.api.CorePlatformApi public class WallTime { // We use a GregorianCalendar (set to UTC) to handle all the date/time normalization logic // and to convert from a broken-down date/time to a millis value. // Unfortunately, it cannot represent an initial state with a zero day and would // automatically normalize it, so we must copy values into and out of it as needed. private final GregorianCalendar calendar; private int year; private int month; private int monthDay; private int hour; private int minute; private int second; private int weekDay; private int yearDay; private int isDst; private int gmtOffsetSeconds; @libcore.api.CorePlatformApi public WallTime() { this.calendar = new GregorianCalendar(0, 0, 0, 0, 0, 0); calendar.setTimeZone(TimeZone.getTimeZone("UTC")); } /** * Sets the wall time to a point in time using the time zone information provided. This * is a replacement for the old native localtime_tz() function. * *

When going from an instant to a wall time it is always unambiguous because there * is only one offset rule acting at any given instant. We do not consider leap seconds. */ @libcore.api.CorePlatformApi public void localtime(int timeSeconds, ZoneInfoData zoneInfo) { try { int offsetSeconds = zoneInfo.getRawOffset() / 1000; // Find out the timezone DST state and adjustment. byte isDst; if (zoneInfo.mTransitions.length == 0) { isDst = 0; } else { // offsetIndex can be in the range -1..zoneInfo.mOffsets.length - 1 int offsetIndex = zoneInfo.findOffsetIndexForTimeInSeconds(timeSeconds); if (offsetIndex == -1) { // -1 means timeSeconds is "before the first recorded transition". The first // recorded transition is treated as a transition from non-DST and the // earliest known raw offset. offsetSeconds = zoneInfo.getEarliestRawOffset() / 1000; isDst = 0; } else { offsetSeconds += zoneInfo.mOffsets[offsetIndex]; isDst = zoneInfo.mIsDsts[offsetIndex]; } } // Perform arithmetic that might underflow before setting fields. int wallTimeSeconds = checked32BitAdd(timeSeconds, offsetSeconds); // Set fields. calendar.setTimeInMillis(wallTimeSeconds * 1000L); copyFieldsFromCalendar(); this.isDst = isDst; this.gmtOffsetSeconds = offsetSeconds; } catch (CheckedArithmeticException e) { // Just stop, leaving fields untouched. } } /** * Returns the time in seconds since beginning of the Unix epoch for the wall time using the * time zone information provided. This is a replacement for an old native mktime_tz() C * function. * *

When going from a wall time to an instant the answer can be ambiguous. A wall * time can map to zero, one or two instants given rational date/time transitions. Rational * in this case means that transitions occur less frequently than the offset * differences between them (which could cause all sorts of craziness like the * skipping out of transitions). * *

For example, this is not fully supported: *

* A wall time in this case might map to t1, t2 or t3. * *

We do not handle leap seconds. *

We assume that no timezone offset transition has an absolute offset > 24 hours. *

We do not assume that adjacent transitions modify the DST state; adjustments can * occur for other reasons such as when a zone changes its raw offset. */ @libcore.api.CorePlatformApi public int mktime(ZoneInfoData zoneInfo) { // Normalize isDst to -1, 0 or 1 to simplify isDst equality checks below. this.isDst = this.isDst > 0 ? this.isDst = 1 : this.isDst < 0 ? this.isDst = -1 : 0; copyFieldsToCalendar(); final long longWallTimeSeconds = calendar.getTimeInMillis() / 1000; if (Integer.MIN_VALUE > longWallTimeSeconds || longWallTimeSeconds > Integer.MAX_VALUE) { // For compatibility with the old native 32-bit implementation we must treat // this as an error. Note: -1 could be confused with a real time. return -1; } try { final int wallTimeSeconds = (int) longWallTimeSeconds; final int rawOffsetSeconds = zoneInfo.getRawOffset() / 1000; final int rawTimeSeconds = checked32BitSubtract(wallTimeSeconds, rawOffsetSeconds); if (zoneInfo.mTransitions.length == 0) { // There is no transition information. There is just a raw offset for all time. if (this.isDst > 0) { // Caller has asserted DST, but there is no DST information available. return -1; } copyFieldsFromCalendar(); this.isDst = 0; this.gmtOffsetSeconds = rawOffsetSeconds; return rawTimeSeconds; } // We cannot know for sure what instant the wall time will map to. Unfortunately, in // order to know for sure we need the timezone information, but to get the timezone // information we need an instant. To resolve this we use the raw offset to find an // OffsetInterval; this will get us the OffsetInterval we need or very close. // The initialTransition can be between -1 and (zoneInfo.mTransitions - 1). -1 // indicates the rawTime is before the first transition and is handled gracefully by // createOffsetInterval(). final int initialTransitionIndex = zoneInfo.findTransitionIndex(rawTimeSeconds); if (isDst < 0) { // This is treated as a special case to get it out of the way: // When a caller has set isDst == -1 it means we can return the first match for // the wall time we find. If the caller has specified a wall time that cannot // exist this always returns -1. Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, true /* mustMatchDst */); return result == null ? -1 : result; } // If the wall time asserts a DST (isDst == 0 or 1) the search is performed twice: // 1) The first attempts to find a DST offset that matches isDst exactly. // 2) If it fails, isDst is assumed to be incorrect and adjustments are made to see // if a valid wall time can be created. The result can be somewhat arbitrary. Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, true /* mustMatchDst */); if (result == null) { result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, false /* mustMatchDst */); } if (result == null) { result = -1; } return result; } catch (CheckedArithmeticException e) { return -1; } } /** * Attempt to apply DST adjustments to {@code oldWallTimeSeconds} to create a wall time in * {@code targetInterval}. * *

This is used when a caller has made an assertion about standard time / DST that cannot * be matched to any offset interval that exists. We must therefore assume that the isDst * assertion is incorrect and the invalid wall time is the result of some modification the * caller made to a valid wall time that pushed them outside of the offset interval they * were in. We must correct for any DST change that should have been applied when they did * so. * *

Unfortunately, we have no information about what adjustment they made and so cannot * know which offset interval they were previously in. For example, they may have added a * second or a year to a valid time to arrive at what they have. * *

We try all offset types that are not the same as the isDst the caller asserted. For * each possible offset we work out the offset difference between that and * {@code targetInterval}, apply it, and see if we are still in {@code targetInterval}. If * we are, then we have found an adjustment. */ private Integer tryOffsetAdjustments(ZoneInfoData zoneInfo, int oldWallTimeSeconds, OffsetInterval targetInterval, int transitionIndex, int isDstToFind) throws CheckedArithmeticException { int[] offsetsToTry = getOffsetsOfType(zoneInfo, transitionIndex, isDstToFind); for (int j = 0; j < offsetsToTry.length; j++) { int rawOffsetSeconds = zoneInfo.getRawOffset() / 1000; int jOffsetSeconds = rawOffsetSeconds + offsetsToTry[j]; int targetIntervalOffsetSeconds = targetInterval.getTotalOffsetSeconds(); int adjustmentSeconds = targetIntervalOffsetSeconds - jOffsetSeconds; int adjustedWallTimeSeconds = checked32BitAdd(oldWallTimeSeconds, adjustmentSeconds); if (targetInterval.containsWallTime(adjustedWallTimeSeconds)) { // Perform any arithmetic that might overflow. int returnValue = checked32BitSubtract(adjustedWallTimeSeconds, targetIntervalOffsetSeconds); // Modify field state and return the result. calendar.setTimeInMillis(adjustedWallTimeSeconds * 1000L); copyFieldsFromCalendar(); this.isDst = targetInterval.getIsDst(); this.gmtOffsetSeconds = targetIntervalOffsetSeconds; return returnValue; } } return null; } /** * Return an array of offsets that have the requested {@code isDst} value. * The {@code startIndex} is used as a starting point so transitions nearest * to that index are returned first. */ private static int[] getOffsetsOfType(ZoneInfoData zoneInfo, int startIndex, int isDst) { // +1 to account for the synthetic transition we invent before the first recorded one. int[] offsets = new int[zoneInfo.mOffsets.length + 1]; boolean[] seen = new boolean[zoneInfo.mOffsets.length]; int numFound = 0; int delta = 0; boolean clampTop = false; boolean clampBottom = false; do { // delta = { 1, -1, 2, -2, 3, -3...} delta *= -1; if (delta >= 0) { delta++; } int transitionIndex = startIndex + delta; if (delta < 0 && transitionIndex < -1) { clampBottom = true; continue; } else if (delta > 0 && transitionIndex >= zoneInfo.mTypes.length) { clampTop = true; continue; } if (transitionIndex == -1) { if (isDst == 0) { // Synthesize a non-DST transition before the first transition we have // data for. offsets[numFound++] = 0; // offset of 0 from raw offset } continue; } int type = zoneInfo.mTypes[transitionIndex] & 0xff; if (!seen[type]) { if (zoneInfo.mIsDsts[type] == isDst) { offsets[numFound++] = zoneInfo.mOffsets[type]; } seen[type] = true; } } while (!(clampTop && clampBottom)); int[] toReturn = new int[numFound]; System.arraycopy(offsets, 0, toReturn, 0, numFound); return toReturn; } /** * Find a time in seconds the same or close to {@code wallTimeSeconds} that * satisfies {@code mustMatchDst}. The search begins around the timezone offset transition * with {@code initialTransitionIndex}. * *

If {@code mustMatchDst} is {@code true} the method can only return times that * use timezone offsets that satisfy the {@code this.isDst} requirements. * If {@code this.isDst == -1} it means that any offset can be used. * *

If {@code mustMatchDst} is {@code false} any offset that covers the * currently set time is acceptable. That is: if {@code this.isDst} == -1, any offset * transition can be used, if it is 0 or 1 the offset used must match {@code this.isDst}. * *

Note: This method both uses and can modify field state. It returns the matching time * in seconds if a match has been found and modifies fields, or it returns {@code null} and * leaves the field state unmodified. */ private Integer doWallTimeSearch(ZoneInfoData zoneInfo, int initialTransitionIndex, int wallTimeSeconds, boolean mustMatchDst) throws CheckedArithmeticException { // The loop below starts at the initialTransitionIndex and radiates out from that point // up to 24 hours in either direction by applying transitionIndexDelta to inspect // adjacent transitions (0, -1, +1, -2, +2). 24 hours is used because we assume that no // total offset from UTC is ever > 24 hours. clampTop and clampBottom are used to // indicate whether the search has either searched > 24 hours or exhausted the // transition data in that direction. The search stops when a match is found or if // clampTop and clampBottom are both true. // The match logic employed is determined by the mustMatchDst parameter. final int MAX_SEARCH_SECONDS = 24 * 60 * 60; boolean clampTop = false, clampBottom = false; int loop = 0; do { // transitionIndexDelta = { 0, -1, 1, -2, 2,..} int transitionIndexDelta = (loop + 1) / 2; if (loop % 2 == 1) { transitionIndexDelta *= -1; } loop++; // Only do any work in this iteration if we need to. if (transitionIndexDelta > 0 && clampTop || transitionIndexDelta < 0 && clampBottom) { continue; } // Obtain the OffsetInterval to use. int currentTransitionIndex = initialTransitionIndex + transitionIndexDelta; OffsetInterval offsetInterval = OffsetInterval.create(zoneInfo, currentTransitionIndex); if (offsetInterval == null) { // No transition exists with the index we tried: Stop searching in the // current direction. clampTop |= (transitionIndexDelta > 0); clampBottom |= (transitionIndexDelta < 0); continue; } // Match the wallTimeSeconds against the OffsetInterval. if (mustMatchDst) { // Work out if the interval contains the wall time the caller specified and // matches their isDst value. if (offsetInterval.containsWallTime(wallTimeSeconds)) { if (this.isDst == -1 || offsetInterval.getIsDst() == this.isDst) { // This always returns the first OffsetInterval it finds that matches // the wall time and isDst requirements. If this.isDst == -1 this means // the result might be a DST or a non-DST answer for wall times that can // exist in two OffsetIntervals. int totalOffsetSeconds = offsetInterval.getTotalOffsetSeconds(); int returnValue = checked32BitSubtract(wallTimeSeconds, totalOffsetSeconds); copyFieldsFromCalendar(); this.isDst = offsetInterval.getIsDst(); this.gmtOffsetSeconds = totalOffsetSeconds; return returnValue; } } } else { // To retain similar behavior to the old native implementation: if the caller is // asserting the same isDst value as the OffsetInterval we are looking at we do // not try to find an adjustment from another OffsetInterval of the same isDst // type. If you remove this you get different results in situations like a // DST -> DST transition or STD -> STD transition that results in an interval of // "skipped" wall time. For example: if 01:30 (DST) is invalid and between two // DST intervals, and the caller has passed isDst == 1, this results in a -1 // being returned. if (isDst != offsetInterval.getIsDst()) { final int isDstToFind = isDst; Integer returnValue = tryOffsetAdjustments(zoneInfo, wallTimeSeconds, offsetInterval, currentTransitionIndex, isDstToFind); if (returnValue != null) { return returnValue; } } } // See if we can avoid another loop in the current direction. if (transitionIndexDelta > 0) { // If we are searching forward and the OffsetInterval we have ends // > MAX_SEARCH_SECONDS after the wall time, we don't need to look any further // forward. boolean endSearch = offsetInterval.getEndWallTimeSeconds() - wallTimeSeconds > MAX_SEARCH_SECONDS; if (endSearch) { clampTop = true; } } else if (transitionIndexDelta < 0) { boolean endSearch = wallTimeSeconds - offsetInterval.getStartWallTimeSeconds() >= MAX_SEARCH_SECONDS; if (endSearch) { // If we are searching backward and the OffsetInterval starts // > MAX_SEARCH_SECONDS before the wall time, we don't need to look any // further backwards. clampBottom = true; } } } while (!(clampTop && clampBottom)); return null; } @libcore.api.CorePlatformApi public void setYear(int year) { this.year = year; } @libcore.api.CorePlatformApi public void setMonth(int month) { this.month = month; } @libcore.api.CorePlatformApi public void setMonthDay(int monthDay) { this.monthDay = monthDay; } @libcore.api.CorePlatformApi public void setHour(int hour) { this.hour = hour; } @libcore.api.CorePlatformApi public void setMinute(int minute) { this.minute = minute; } @libcore.api.CorePlatformApi public void setSecond(int second) { this.second = second; } @libcore.api.CorePlatformApi public void setWeekDay(int weekDay) { this.weekDay = weekDay; } @libcore.api.CorePlatformApi public void setYearDay(int yearDay) { this.yearDay = yearDay; } @libcore.api.CorePlatformApi public void setIsDst(int isDst) { this.isDst = isDst; } @libcore.api.CorePlatformApi public void setGmtOffset(int gmtoff) { this.gmtOffsetSeconds = gmtoff; } @libcore.api.CorePlatformApi public int getYear() { return year; } @libcore.api.CorePlatformApi public int getMonth() { return month; } @libcore.api.CorePlatformApi public int getMonthDay() { return monthDay; } @libcore.api.CorePlatformApi public int getHour() { return hour; } @libcore.api.CorePlatformApi public int getMinute() { return minute; } @libcore.api.CorePlatformApi public int getSecond() { return second; } @libcore.api.CorePlatformApi public int getWeekDay() { return weekDay; } @libcore.api.CorePlatformApi public int getYearDay() { return yearDay; } @libcore.api.CorePlatformApi public int getGmtOffset() { return gmtOffsetSeconds; } @libcore.api.CorePlatformApi public int getIsDst() { return isDst; } private void copyFieldsToCalendar() { calendar.set(Calendar.YEAR, year); calendar.set(Calendar.MONTH, month); calendar.set(Calendar.DAY_OF_MONTH, monthDay); calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, second); calendar.set(Calendar.MILLISECOND, 0); } private void copyFieldsFromCalendar() { year = calendar.get(Calendar.YEAR); month = calendar.get(Calendar.MONTH); monthDay = calendar.get(Calendar.DAY_OF_MONTH); hour = calendar.get(Calendar.HOUR_OF_DAY); minute = calendar.get(Calendar.MINUTE); second = calendar.get(Calendar.SECOND); // Calendar uses Sunday == 1. Android Time uses Sunday = 0. weekDay = calendar.get(Calendar.DAY_OF_WEEK) - 1; // Calendar enumerates from 1, Android Time enumerates from 0. yearDay = calendar.get(Calendar.DAY_OF_YEAR) - 1; } /** * A wall-time representation of a timezone offset interval. * *

Wall-time means "as it would appear locally in the timezone in which it applies". * For example in 2007: * PST was a -8:00 offset that ran until Mar 11, 2:00 AM. * PDT was a -7:00 offset and ran from Mar 11, 3:00 AM to Nov 4, 2:00 AM. * PST was a -8:00 offset and ran from Nov 4, 1:00 AM. * Crucially this means that there was a "gap" after PST when PDT started, and an overlap when * PDT ended and PST began. * *

Although wall-time means "local time", for convenience all wall-time values are stored in * the number of seconds since the beginning of the Unix epoch to get that time in UTC. * To convert from a wall-time to the actual UTC time it is necessary to subtract the * {@code totalOffsetSeconds}. * For example: If the offset in PST is -07:00 hours, then: * timeInPstSeconds = wallTimeUtcSeconds - offsetSeconds * i.e. 13:00 UTC - (-07:00) = 20:00 UTC = 13:00 PST */ static class OffsetInterval { /** The time the interval starts in seconds since start of epoch, inclusive. */ private final int startWallTimeSeconds; /** The time the interval ends in seconds since start of epoch, exclusive. */ private final int endWallTimeSeconds; private final int isDst; private final int totalOffsetSeconds; /** * Creates an {@link OffsetInterval}. * *

If {@code transitionIndex} is -1, where possible the transition is synthesized to run * from the beginning of 32-bit time until the first transition in {@code zoneInfo} with * offset information based on the first type defined. If {@code transitionIndex} is the * last transition, that transition is considered to run until the end of 32-bit time. * Otherwise, the information is extracted from {@code zoneInfo.mTransitions}, * {@code zoneInfo.mOffsets} and {@code zoneInfo.mIsDsts}. * *

This method can return null when: *

    *
  1. the {@code transitionIndex} is outside the allowed range, i.e. * {@code transitionIndex < -1 || transitionIndex >= [the number of transitions]}.
  2. *
  3. when calculations result in a zero-length interval. This is only expected to occur * when dealing with transitions close to (or exactly at) {@code Integer.MIN_VALUE} and * {@code Integer.MAX_VALUE} and where it's difficult to convert from UTC to local times. *
  4. *
*/ public static OffsetInterval create(ZoneInfoData zoneInfo, int transitionIndex) { if (transitionIndex < -1 || transitionIndex >= zoneInfo.mTransitions.length) { return null; } if (transitionIndex == -1) { int totalOffsetSeconds = zoneInfo.getEarliestRawOffset() / 1000; int isDst = 0; int startWallTimeSeconds = Integer.MIN_VALUE; int endWallTimeSeconds = saturated32BitAdd(zoneInfo.mTransitions[0], totalOffsetSeconds); if (startWallTimeSeconds == endWallTimeSeconds) { // There's no point in returning an OffsetInterval that lasts 0 seconds. return null; } return new OffsetInterval(startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds); } int rawOffsetSeconds = zoneInfo.getRawOffset() / 1000; int type = zoneInfo.mTypes[transitionIndex] & 0xff; int totalOffsetSeconds = zoneInfo.mOffsets[type] + rawOffsetSeconds; int endWallTimeSeconds; if (transitionIndex == zoneInfo.mTransitions.length - 1) { endWallTimeSeconds = Integer.MAX_VALUE; } else { endWallTimeSeconds = saturated32BitAdd( zoneInfo.mTransitions[transitionIndex + 1], totalOffsetSeconds); } int isDst = zoneInfo.mIsDsts[type]; int startWallTimeSeconds = saturated32BitAdd(zoneInfo.mTransitions[transitionIndex], totalOffsetSeconds); if (startWallTimeSeconds == endWallTimeSeconds) { // There's no point in returning an OffsetInterval that lasts 0 seconds. return null; } return new OffsetInterval( startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds); } private OffsetInterval(int startWallTimeSeconds, int endWallTimeSeconds, int isDst, int totalOffsetSeconds) { this.startWallTimeSeconds = startWallTimeSeconds; this.endWallTimeSeconds = endWallTimeSeconds; this.isDst = isDst; this.totalOffsetSeconds = totalOffsetSeconds; } public boolean containsWallTime(long wallTimeSeconds) { return wallTimeSeconds >= startWallTimeSeconds && wallTimeSeconds < endWallTimeSeconds; } public int getIsDst() { return isDst; } public int getTotalOffsetSeconds() { return totalOffsetSeconds; } public long getEndWallTimeSeconds() { return endWallTimeSeconds; } public long getStartWallTimeSeconds() { return startWallTimeSeconds; } } /** * An exception used to indicate an arithmetic overflow or underflow. */ private static class CheckedArithmeticException extends Exception { } /** * Calculate (a + b). The result must be in the Integer range otherwise an exception is thrown. * * @throws CheckedArithmeticException if overflow or underflow occurs */ private static int checked32BitAdd(long a, int b) throws CheckedArithmeticException { // Adapted from Guava IntMath.checkedAdd(); long result = a + b; if (result != (int) result) { throw new CheckedArithmeticException(); } return (int) result; } /** * Calculate (a - b). The result must be in the Integer range otherwise an exception is thrown. * * @throws CheckedArithmeticException if overflow or underflow occurs */ private static int checked32BitSubtract(long a, int b) throws CheckedArithmeticException { // Adapted from Guava IntMath.checkedSubtract(); long result = a - b; if (result != (int) result) { throw new CheckedArithmeticException(); } return (int) result; } /** * Calculate (a + b). If the result would overflow or underflow outside of the Integer range * Integer.MAX_VALUE or Integer.MIN_VALUE will be returned, respectively. */ private static int saturated32BitAdd(long a, int b) { long result = a + b; if (result > Integer.MAX_VALUE) { return Integer.MAX_VALUE; } else if (result < Integer.MIN_VALUE) { return Integer.MIN_VALUE; } return (int) result; } }