Clean up EAS -> TimeZone determination code
* Use one minute before/after for transition checking, instead of the
sloppier early version
* Add tests for additional known time zones
* Change most methods in CalendarUtilities to package private (for use
with unit tests)
* Clean up a little bad formatting
Change-Id: I9e5be5e1c859f2294adf06874459f7db15fb8c22
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
index 9b6021d..d8216ac 100644
--- a/src/com/android/exchange/utility/CalendarUtilities.java
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -252,7 +252,7 @@
* @param tzd the TimeZoneDate we're interested in
* @return a GregorianCalendar with the given time zone and date
*/
- static GregorianCalendar getCheckCalendar(TimeZone timeZone, TimeZoneDate tzd) {
+ static long getMillisAtTimeZoneDateTransition(TimeZone timeZone, TimeZoneDate tzd) {
GregorianCalendar testCalendar = new GregorianCalendar(timeZone);
testCalendar.set(GregorianCalendar.YEAR, sCurrentYear);
testCalendar.set(GregorianCalendar.MONTH, tzd.month);
@@ -260,7 +260,8 @@
testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day);
testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour);
testCalendar.set(GregorianCalendar.MINUTE, tzd.minute);
- return testCalendar;
+ testCalendar.set(GregorianCalendar.SECOND, 0);
+ return testCalendar.getTimeInMillis();
}
/**
@@ -272,7 +273,7 @@
* @param startInDaylightTime whether daylight time is in effect at the startTime
* @return a GregorianCalendar representing the transition or null if none
*/
- static /*package*/ GregorianCalendar findTransitionDate(TimeZone tz, long startTime,
+ static GregorianCalendar findTransitionDate(TimeZone tz, long startTime,
long endTime, boolean startInDaylightTime) {
long startingEndTime = endTime;
Date date = null;
@@ -378,7 +379,7 @@
* consecutive years starting with the current year
* @return an RRULE or null if none could be inferred from the calendars
*/
- static private RRule inferRRuleFromCalendars(GregorianCalendar[] calendars) {
+ static RRule inferRRuleFromCalendars(GregorianCalendar[] calendars) {
// Let's see if we can make a rule about these
GregorianCalendar calendar = calendars[0];
if (calendar == null) return null;
@@ -439,7 +440,7 @@
* @param offsetMinutes minutes offset from GMT (east is positive, west is negative
* @return a utcOffset
*/
- static /*package*/ String utcOffsetString(int offsetMinutes) {
+ static String utcOffsetString(int offsetMinutes) {
StringBuilder sb = new StringBuilder();
int hours = offsetMinutes / 60;
if (hours < 0) {
@@ -531,7 +532,7 @@
* @param writer the SimpleIcsWriter to be used
* @throws IOException
*/
- static public void timeZoneToVTimezone(TimeZone tz, SimpleIcsWriter writer)
+ static void timeZoneToVTimezone(TimeZone tz, SimpleIcsWriter writer)
throws IOException {
// We'll use these regardless of whether there's DST in this time zone or not
int rawOffsetMinutes = tz.getRawOffset() / MINUTES;
@@ -605,7 +606,7 @@
* @param transitions calendars representing transitions to/from DST
* @return millis for the first transition after the current date/time
*/
- static private long findNextTransition(long startingMillis, GregorianCalendar[] transitions) {
+ static long findNextTransition(long startingMillis, GregorianCalendar[] transitions) {
for (GregorianCalendar transition: transitions) {
long transitionMillis = transition.getTimeInMillis();
if (transitionMillis > startingMillis) {
@@ -622,7 +623,7 @@
* @param tz the TimeZone
* @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
*/
- static public String timeZoneToTziStringImpl(TimeZone tz) {
+ static String timeZoneToTziStringImpl(TimeZone tz) {
String tziString;
byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE];
int standardBias = - tz.getRawOffset();
@@ -702,9 +703,9 @@
* Given a String as directly read from EAS, tries to find a TimeZone in the database of all
* time zones that corresponds to that String.
* @param timeZoneString the String read from the server
- * @return the TimeZone, or TimeZone.getDefault() if not found
+ * @return the TimeZone, or null if not found
*/
- static public TimeZone tziStringToTimeZoneImpl(String timeZoneString) {
+ static TimeZone tziStringToTimeZoneImpl(String timeZoneString) {
TimeZone timeZone = null;
// First, we need to decode the base64 string
byte[] timeZoneBytes = Base64.decode(timeZoneString, Base64.DEFAULT);
@@ -732,6 +733,7 @@
if (Eas.USER_LOG) {
Log.d(TAG, "TimeZone without DST found by offset: " + dn);
}
+ return timeZone;
} else {
TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes,
MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET);
@@ -751,39 +753,34 @@
// of dst. That's the best we can do for now, since there's no other info
// provided by EAS (i.e. we can't get dynamic transitions, etc.)
- int testSavingsMinutes = timeZone.getDSTSavings() / MINUTES;
- int errorBoundsMinutes = (testSavingsMinutes * 2) + 1;
-
- // Check start DST transition
- GregorianCalendar testCalendar = getCheckCalendar(timeZone, dstStart);
- testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes);
- Date before = testCalendar.getTime();
- testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes);
- Date after = testCalendar.getTime();
+ // Check one minute before and after DST start transition
+ long millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstStart);
+ Date before = new Date(millisAtTransition - MINUTES);
+ Date after = new Date(millisAtTransition + MINUTES);
if (timeZone.inDaylightTime(before)) continue;
if (!timeZone.inDaylightTime(after)) continue;
- // Check end DST transition
- testCalendar = getCheckCalendar(timeZone, dstEnd);
- testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes);
- before = testCalendar.getTime();
- testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes);
- after = testCalendar.getTime();
+ // Check one minute before and after DST end transition
+ millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstEnd);
+ // Note that we need to subtract an extra hour here, because we end up with
+ // gaining an hour in the transition BACK to standard time
+ before = new Date(millisAtTransition - (dstSavings + MINUTES));
+ after = new Date(millisAtTransition + MINUTES);
if (!timeZone.inDaylightTime(before)) continue;
if (timeZone.inDaylightTime(after)) continue;
// Check that the savings are the same
if (dstSavings != timeZone.getDSTSavings()) continue;
- break;
+ return timeZone;
}
}
}
- return timeZone;
+ return null;
}
static public String convertEmailDateTimeToCalendarDateTime(String date) {
// Format for email date strings is 2010-02-23T16:00:00.000Z
- // Format for calendar date strings is 2010-02-23T160000Z
+ // Format for calendar date strings is 20100223T160000Z
return date.substring(0, 4) + date.substring(5, 7) + date.substring(8, 13) +
date.substring(14, 16) + date.substring(17, 19) + 'Z';
}
@@ -836,7 +833,7 @@
* @param calendar the calendar holding the transition date/time
* @return the true minute of the transition
*/
- static public int getTrueTransitionMinute(GregorianCalendar calendar) {
+ static int getTrueTransitionMinute(GregorianCalendar calendar) {
int minute = calendar.get(Calendar.MINUTE);
if (minute == 59) {
minute = 0;
@@ -850,7 +847,7 @@
* @param calendar the calendar holding the transition date/time
* @return the true hour of the transition
*/
- static public int getTrueTransitionHour(GregorianCalendar calendar) {
+ static int getTrueTransitionHour(GregorianCalendar calendar) {
int hour = calendar.get(Calendar.HOUR_OF_DAY);
hour++;
if (hour == 24) {
@@ -866,7 +863,7 @@
* @param tz a time zone
* @param dst whether we're entering daylight time
*/
- static public String transitionMillisToVCalendarTime(long millis, TimeZone tz, boolean dst) {
+ static String transitionMillisToVCalendarTime(long millis, TimeZone tz, boolean dst) {
StringBuilder sb = new StringBuilder();
GregorianCalendar cal = new GregorianCalendar(tz);
cal.setTimeInMillis(millis);
@@ -993,7 +990,7 @@
// Calendar app UI, which is a subset of possible recurrence types
// This code must be updated when the Calendar adds new functionality
static public void recurrenceFromRrule(String rrule, long startTime, Serializer s)
- throws IOException {
+ throws IOException {
Log.d("RRULE", "rule: " + rrule);
String freq = tokenFromRrule(rrule, "FREQ=");
// If there's no FREQ=X, then we don't write a recurrence
@@ -1261,14 +1258,13 @@
if (entityValues.containsKey("DTSTAMP")) {
ics.writeTag("DTSTAMP", entityValues.getAsString("DTSTAMP"));
} else {
- ics.writeTag("DTSTAMP",
- CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
+ ics.writeTag("DTSTAMP", millisToEasDateTime(System.currentTimeMillis()));
}
long startTime = entityValues.getAsLong(Events.DTSTART);
if (startTime != 0) {
ics.writeTag("DTSTART" + vCalendarTimeZoneSuffix,
- CalendarUtilities.millisToEasDateTime(startTime, vCalendarTimeZone));
+ millisToEasDateTime(startTime, vCalendarTimeZone));
}
// If this is an Exception, we send the recurrence-id, which is just the original
@@ -1276,13 +1272,13 @@
if (isException) {
long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
ics.writeTag("RECURRENCE-ID" + vCalendarTimeZoneSuffix,
- CalendarUtilities.millisToEasDateTime(originalTime, vCalendarTimeZone));
+ millisToEasDateTime(originalTime, vCalendarTimeZone));
}
if (!entityValues.containsKey(Events.DURATION)) {
if (entityValues.containsKey(Events.DTEND)) {
ics.writeTag("DTEND" + vCalendarTimeZoneSuffix,
- CalendarUtilities.millisToEasDateTime(
+ millisToEasDateTime(
entityValues.getAsLong(Events.DTEND), vCalendarTimeZone));
}
} else {
@@ -1296,7 +1292,7 @@
// We'll use the default in this case
}
ics.writeTag("DTEND" + vCalendarTimeZoneSuffix,
- CalendarUtilities.millisToEasDateTime(
+ millisToEasDateTime(
startTime + durationMillis, vCalendarTimeZone));
}
diff --git a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
index 0fd89ef..f3f2a46 100644
--- a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
+++ b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
@@ -60,15 +60,31 @@
// Not all time zones are appropriate for testing. For example, ISRAEL_STANDARD_TIME cannot be
// used because DST is determined from year to year in a non-standard way (related to the lunar
// calendar); therefore, the test would only work during the year in which it was created
- private static final String INDIA_STANDARD_TIME =
+
+ // This time zone has no DST
+ private static final String ASIA_CALCUTTA_TIME =
"tv7//0kAbgBkAGkAYQAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAbgBkAGkAYQAgAEQAYQB5AGwAaQBnAGgAdAAgAFQAaQBtAGUA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
- private static final String PACIFIC_STANDARD_TIME =
+
+ // This time zone is equivalent to PST and uses DST
+ private static final String AMERICA_DAWSON_TIME =
"4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAYQBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkA" +
"bQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
+ // Test a southern hemisphere time zone w/ DST
+ private static final String AUSTRALIA_ACT_TIME =
+ "qP3//0EAVQBTACAARQBhAHMAdABlAHIAbgAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAA" +
+ "AAAAAAAAAAQAAAABAAMAAAAAAAAAAAAAAEEAVQBTACAARQBhAHMAdABlAHIAbgAgAEQAYQB5AGwAaQBnAGgA" +
+ "dAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAoAAAABAAIAAAAAAAAAxP///w==";
+
+ // Test a european time zone w/ DST
+ private static final String EUROPE_MOSCOW_TIME =
+ "TP///1IAdQBzAHMAaQBhAG4AIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAoAAAAFAAMAAAAAAAAAAAAAAFIAdQBzAHMAaQBhAG4AIABEAGEAeQBsAGkAZwBoAHQAIABUAGkA" +
+ "bQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAIAAAAAAAAAxP///w==";
+
private static final String ORGANIZER = "organizer@server.com";
private static final String ATTENDEE = "attendee@server.com";
@@ -92,10 +108,14 @@
}
public void testParseTimeZoneEndToEnd() {
- TimeZone tz = CalendarUtilities.tziStringToTimeZone(PACIFIC_STANDARD_TIME);
- assertEquals("Pacific Standard Time", tz.getDisplayName());
- tz = CalendarUtilities.tziStringToTimeZone(INDIA_STANDARD_TIME);
- assertEquals("India Standard Time", tz.getDisplayName());
+ TimeZone tz = CalendarUtilities.tziStringToTimeZone(AMERICA_DAWSON_TIME);
+ assertEquals("America/Dawson", tz.getID());
+ tz = CalendarUtilities.tziStringToTimeZone(ASIA_CALCUTTA_TIME);
+ assertEquals("Asia/Calcutta", tz.getID());
+ tz = CalendarUtilities.tziStringToTimeZone(AUSTRALIA_ACT_TIME);
+ assertEquals("Australia/ACT", tz.getID());
+ tz = CalendarUtilities.tziStringToTimeZone(EUROPE_MOSCOW_TIME);
+ assertEquals("Europe/Moscow", tz.getID());
}
public void testGenerateEasDayOfWeek() {