/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.google.j2objc.util; import java.util.Date; import java.util.Locale; import java.util.TimeZone; /*-[ #import "IOSClass.h" #import "java/lang/IllegalArgumentException.h" #import "java/util/GregorianCalendar.h" #import "java/util/GregorianCalendar.h" ]-*/ /** * An NSTimeZone-backed concrete TimeZone implementation that provides daylight saving time (DST) * and historical time zone offsets from the native iOS/OS X time zone database. * * @author Lukhnos Liu */ public final class NativeTimeZone extends TimeZone { // Constants for quick Gregorian calendar computations; these are only used by {@link // #getOffset(int, int, int, int, int, int)} and are adapted from libcore's ZoneInfo. The // MILLISECONDS_PER_400_YEARS constant reflects the fact that there are only 97 leap years for // every 400 years. See the linked method for a brief summary of the leap year rule. private static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; private static final long MILLISECONDS_PER_400_YEARS = MILLISECONDS_PER_DAY * (400 * 365 + 100 - 3); private static final long UNIX_OFFSET = 62167219200000L; private static final int[] NORMAL = new int[] { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, }; private static final int[] LEAP = new int[] { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, }; private final Object nativeTimeZone; private final int rawOffset; private final int dstSavings; private final boolean useDaylightTime; static { // Observe the native NSSystemTimeZoneDidChangeNotification so that we can flush TimeZone's // cached default time zone upon system time zone change. setUpTimeZoneDidChangeNotificationHandler(); } public static native String[] getAvailableNativeTimeZoneNames() /*-[ NSArray *timeZones = [NSTimeZone knownTimeZoneNames]; return [IOSObjectArray arrayWithNSArray:timeZones type:NSString_class_()]; ]-*/; public static native NativeTimeZone get(String name) /*-[ return [ComGoogleJ2objcUtilNativeTimeZone fromNativeTimeZoneWithId: [NSTimeZone timeZoneWithName:name]]; ]-*/; public static native NativeTimeZone getDefaultNativeTimeZone() /*-[ return [ComGoogleJ2objcUtilNativeTimeZone fromNativeTimeZoneWithId: [NSTimeZone defaultTimeZone]]; ]-*/; private static native NativeTimeZone fromNativeTimeZone(Object nativeTimeZone) /*-[ NSTimeZone *tz = (NSTimeZone *)nativeTimeZone; if (!tz) { return nil; } NSDate *now = [NSDate date]; NSInteger offset = [tz secondsFromGMTForDate:now]; NSTimeInterval dstOffset = [tz daylightSavingTimeOffsetForDate:now]; // The DST offset is relative to the current offset, and hence the math here. jint rawOffset = (jint)(offset * 1000) - (jint)(dstOffset * 1000.0); NSDate *nextTransition = [tz nextDaylightSavingTimeTransitionAfterDate:now]; jint dstSavings; jboolean useDaylightTime; if (nextTransition) { NSTimeInterval nextDstOffset = [tz daylightSavingTimeOffsetForDate:nextTransition]; // This is a simplified assumption. Technically, there's nothing in the TZ rules // that says you can't have a +1 transition tomorrow, and a +2 the day after. This // is why in more modern time libraries, there is no longer the notion of a fixed // DST offset. NSTimeInterval fixedDstOffset = (dstOffset != 0) ? dstOffset : nextDstOffset; // And the offset is always positive regardless the hemisphere the TZ is in. dstSavings = fabs(fixedDstOffset) * 1000; useDaylightTime = true; } else { dstSavings = 0; useDaylightTime = false; } return create_ComGoogleJ2objcUtilNativeTimeZone_initWithId_withNSString_withInt_withInt_withBoolean_( nativeTimeZone, tz.name, rawOffset, dstSavings, useDaylightTime); ]-*/; private static native void setUpTimeZoneDidChangeNotificationHandler() /*-[ [[NSNotificationCenter defaultCenter] addObserver:[ComGoogleJ2objcUtilNativeTimeZone class] selector:@selector(handleTimeZoneChangeWithId:) name:NSSystemTimeZoneDidChangeNotification object:nil]; ]-*/; private static void handleTimeZoneChange(Object notification) { TimeZone.setDefault(null); } /** * Create an NSTimeZone-backed TimeZone instance. * * @param nativeTimeZone the NSTimeZone instance. * @param name the native time zone's name. * @param rawOffset the pre-calculated raw offset (in millis) from UTC. When TimeZone was * designed, the assumption was that the rawOffset would be a constant at * all times. We pre-compute this offset using the instant when the * instance is created. * @param useDaylightTime whether this time zone observes DST at the moment this instance * is created. */ private NativeTimeZone(Object nativeTimeZone, String name, int rawOffset, int dstSavings, boolean useDaylightTime) { setID(name); this.nativeTimeZone = nativeTimeZone; this.rawOffset = rawOffset; this.dstSavings = dstSavings; this.useDaylightTime = useDaylightTime; } @Override public boolean equals(Object obj) { if (!(obj instanceof NativeTimeZone)) { return false; } return nativeTimeZone.equals(((NativeTimeZone) obj).nativeTimeZone); } @Override public native int hashCode() /*-[ return [nativeTimeZone_ hash]; ]-*/; @Override public boolean hasSameRules(TimeZone other) { if (other instanceof NativeTimeZone) { return compareNativeTimeZoneRules(((NativeTimeZone) other).nativeTimeZone); } return super.hasSameRules(other); } @Override public native int getOffset(long time) /*-[ double interval = (double)time / 1000.0; NSDate *date = [NSDate dateWithTimeIntervalSince1970:interval]; return (jint)[(NSTimeZone *)nativeTimeZone_ secondsFromGMTForDate:date] * 1000; ]-*/; /** * Used by java.util.GregorianCalendar. */ public native int getOffsetsByUtcTime(long utcTimeInMillis, int[] offsets) /*-[ NSDate *date = [NSDate dateWithTimeIntervalSince1970:(double)utcTimeInMillis / 1000.0]; NSTimeZone *tz = (NSTimeZone *)nativeTimeZone_; jint totalOffset = (jint)[tz secondsFromGMTForDate:date] * 1000; jint dstOffset = (jint)[tz daylightSavingTimeOffsetForDate:date] * 1000; jint rawOffset = totalOffset - dstOffset; *IOSIntArray_GetRef(offsets, 0) = rawOffset; *IOSIntArray_GetRef(offsets, 1) = dstOffset; return totalOffset; ]-*/; /** * This implementation is adapted from libcore's ZoneInfo. * * The method always assumes Gregorian calendar, and uses a simple formula to first derive the * instant of the local datetime arguments and then call {@link #getOffset(long)} to get the * actual offset. The local datetime used here is always in the non-DST time zone, i.e. the time * zone with the "raw" offset, as evidenced by actual JDK implementation and the code below. This * means it's possible to call getOffset with a practically non-existent date time, such as 2:30 * AM, March 13, 2016, which does not exist in US Pacific Time -- it falls in the DST gap of that * day. * * When we compute the milliseconds for the year component, we need to take leap years into * consideration. According to http://aa.usno.navy.mil/faq/docs/calendars.php: "The Gregorian leap * year rule is: Every year that is exactly divisible by four is a leap year, except for years * that are exactly divisible by 100, but these centurial years are leap years if they are exactly * divisible by 400. For example, the years 1700, 1800, and 1900 are not leap years, but the year * 2000 is." Hence the rules and constants used here. * * Since this method only supports Gregorian calendar, the return value of any date before October * 4, 1582 is not reliable. In addition, the era and dayOfWeek arguments are not used in this * method. */ @Override public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) { long calc = (year / 400) * MILLISECONDS_PER_400_YEARS; year %= 400; calc += year * (365 * MILLISECONDS_PER_DAY); calc += ((year + 3) / 4) * MILLISECONDS_PER_DAY; if (year > 0) { calc -= ((year - 1) / 100) * MILLISECONDS_PER_DAY; } boolean isLeap = (year == 0 || (year % 4 == 0 && year % 100 != 0)); int[] mlen = isLeap ? LEAP : NORMAL; calc += mlen[month] * MILLISECONDS_PER_DAY; calc += (day - 1) * MILLISECONDS_PER_DAY; calc += millis; calc -= rawOffset; calc -= UNIX_OFFSET; return getOffset(calc); } @Override public int getRawOffset() { return rawOffset; } @Override public void setRawOffset(int offsetMillis) { throw new UnsupportedOperationException("Cannot set raw offset on a native TimeZone"); } @Override public int getDSTSavings() { return dstSavings; } @Override public boolean useDaylightTime() { return useDaylightTime; } @Override public boolean inDaylightTime(Date date) { return getOffset(date.getTime()) != rawOffset; } @Override public native String getDisplayName(boolean daylight, int style, Locale locale) /*-[ if (style != JavaUtilTimeZone_SHORT && style != JavaUtilTimeZone_LONG) { @throw [[[JavaLangIllegalArgumentException alloc] init] autorelease]; } NSTimeZoneNameStyle zoneStyle; // "daylight" is defined in <time.h>, hence the renaming. if (daylight_ && useDaylightTime_) { zoneStyle = (style == JavaUtilTimeZone_SHORT) ? NSTimeZoneNameStyleShortDaylightSaving : NSTimeZoneNameStyleDaylightSaving; } else { zoneStyle = (style == JavaUtilTimeZone_SHORT) ? NSTimeZoneNameStyleShortStandard : NSTimeZoneNameStyleStandard; } // Find native locale. NSLocale *nativeLocale; if (locale) { NSMutableDictionary *components = [NSMutableDictionary dictionary]; [components setObject:[locale getLanguage] forKey:NSLocaleLanguageCode]; [components setObject:[locale getCountry] forKey:NSLocaleCountryCode]; [components setObject:[locale getVariant] forKey:NSLocaleVariantCode]; NSString *localeId = [NSLocale localeIdentifierFromComponents:components]; nativeLocale = AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier:localeId]); } else { nativeLocale = [NSLocale currentLocale]; } return [(NSTimeZone *) nativeTimeZone_ localizedName:zoneStyle locale:nativeLocale]; ]-*/; private native boolean compareNativeTimeZoneRules(Object otherNativeTimeZone) /*-[ // [NSTimeZone isEqualToTimeZone:] also compares names, which we don't want. Since we // only deal with native time zones that can be obtained with known names, we'll just // compare the underlying data. NSTimeZone *other = (NSTimeZone *)otherNativeTimeZone; return [((NSTimeZone *)self->nativeTimeZone_).data isEqualToData:other.data]; ]-*/; }