Attempt to better handle screwy MSFT time zone information
* Exchange appears to send time zond data that doesn't really match
any time zone in our database; in these cases, we have been
returning a random one with the same bias (base offset w/o DST)
* In this CL, we try to do better by giving the time zone information
a bit more slack when the regular determination fails (we allow
the hour of change to be up to 4 hours different from expected,
rather than one minute). This is certainly better, though I do
not have an explanation from MSFT about the reason for the
erroneous data.
* Updated unit tests to confirm that we don't break any existing
code and do, in fact, handle the case reported below as a P1/S1
bug.
Bug: 5605219
Change-Id: I8c17a687404204aff4feb1c3009adde279110cab
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
index 725eb4d..2f466b8 100644
--- a/src/com/android/exchange/utility/CalendarUtilities.java
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -74,6 +74,14 @@
static final int HOURS = MINUTES*60;
static final long DAYS = HOURS*24;
+ // We want to find a time zone whose DST info is accurate to one minute
+ static final int STANDARD_DST_PRECISION = MINUTES;
+ // If we can't find one, we'll try a more lenient standard (this is better than guessing a
+ // time zone, which is what we otherwise do). Note that this specifically addresses an issue
+ // seen in some time zones sent by MS Exchange in which the start and end hour differ
+ // for no apparent reason
+ static final int LENIENT_DST_PRECISION = 4*HOURS;
+
private static final String SYNC_VERSION = Events.SYNC_DATA4;
// NOTE All Microsoft data structures are little endian
@@ -729,9 +737,11 @@
/**
* Given a String as directly read from EAS, returns a TimeZone corresponding to that String
* @param timeZoneString the String read from the server
+ * @param precision the number of milliseconds of precision in TimeZone determination
* @return the TimeZone, or TimeZone.getDefault() if not found
*/
- static public TimeZone tziStringToTimeZone(String timeZoneString) {
+ @VisibleForTesting
+ static TimeZone tziStringToTimeZone(String timeZoneString, int precision) {
// If we have this time zone cached, use that value and return
TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
if (timeZone != null) {
@@ -739,7 +749,7 @@
ExchangeService.log(TAG, " Using cached TimeZone " + timeZone.getID());
}
} else {
- timeZone = tziStringToTimeZoneImpl(timeZoneString);
+ timeZone = tziStringToTimeZoneImpl(timeZoneString, precision);
if (timeZone == null) {
// If we don't find a match, we just return the current TimeZone. In theory, this
// shouldn't be happening...
@@ -752,12 +762,21 @@
}
/**
+ * The standard entry to EAS time zone conversion, using one minute as the precision
+ */
+ static public TimeZone tziStringToTimeZone(String timeZoneString) {
+ return tziStringToTimeZone(timeZoneString, MINUTES);
+ }
+
+ /**
* 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.
+ * time zones that corresponds to that String. If the test time zone string includes DST and
+ * we don't find a match, and we're using standard precision, we try again with lenient
+ * precision, which is a bit better than guessing
* @param timeZoneString the String read from the server
* @return the TimeZone, or null if not found
*/
- static TimeZone tziStringToTimeZoneImpl(String timeZoneString) {
+ static TimeZone tziStringToTimeZoneImpl(String timeZoneString, int precision) {
TimeZone timeZone = null;
// First, we need to decode the base64 string
byte[] timeZoneBytes = Base64.decode(timeZoneString, Base64.DEFAULT);
@@ -823,8 +842,8 @@
// 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);
+ Date before = new Date(millisAtTransition - precision);
+ Date after = new Date(millisAtTransition + precision);
if (timeZone.inDaylightTime(before)) continue;
if (!timeZone.inDaylightTime(after)) continue;
@@ -832,8 +851,8 @@
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);
+ before = new Date(millisAtTransition - (dstSavings + precision));
+ after = new Date(millisAtTransition + precision);
if (!timeZone.inDaylightTime(before)) continue;
if (timeZone.inDaylightTime(after)) continue;
@@ -843,11 +862,17 @@
}
// In this case, there is no daylight savings time, so the only interesting data
// is the offset, and we know that all of the zoneId's match; we'll take the first
- timeZone = TimeZone.getTimeZone(zoneIds[0]);
+ boolean lenient = false;
+ if ((dstStart.hour != dstEnd.hour) && (precision == STANDARD_DST_PRECISION)) {
+ timeZone = tziStringToTimeZoneImpl(timeZoneString, LENIENT_DST_PRECISION);
+ lenient = true;
+ } else {
+ timeZone = TimeZone.getTimeZone(zoneIds[0]);
+ }
if (Eas.USER_LOG) {
ExchangeService.log(TAG,
- "No TimeZone with correct DST settings; using first: " +
- timeZone.getID());
+ "No TimeZone with correct DST settings; using " +
+ (lenient ? "lenient" : "first") + ": " + timeZone.getID());
}
return timeZone;
}
diff --git a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
index b883b5a..0ecdb8f 100644
--- a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
+++ b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
@@ -103,6 +103,13 @@
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEgAYQB3AGEAaQBpAGEAbgAgAEQAYQB5AGwAaQBnAGgAdAAgAFQA" +
"aQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
+ // This is time zone sent by Exchange 2007, apparently; the start time of DST for the eastern
+ // time zone (EST) is off by two hours, which we should correct in our new "lenient" code
+ private static final String LENIENT_EASTERN_TIME =
+ "LAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAAAxP///w==";
+
private static final String ORGANIZER = "organizer@server.com";
private static final String ATTENDEE = "attendee@server.com";
@@ -132,6 +139,17 @@
assertEquals("Asia/Calcutta", tz.getID());
tz = CalendarUtilities.tziStringToTimeZone(AUSTRALIA_ACT_TIME);
assertEquals("Australia/ACT", tz.getID());
+
+ // Test peculiar MS sent EST data with and without lenient precision; send standard
+ // precision + 1 (i.e. 1ms) to make sure the code doesn't automatically flip to lenient
+ // when the tz isn't found
+ tz = CalendarUtilities.tziStringToTimeZoneImpl(LENIENT_EASTERN_TIME,
+ CalendarUtilities.STANDARD_DST_PRECISION+1);
+ assertEquals("America/Atikokan", tz.getID());
+ tz = CalendarUtilities.tziStringToTimeZoneImpl(LENIENT_EASTERN_TIME,
+ CalendarUtilities.LENIENT_DST_PRECISION);
+ assertEquals("America/Detroit", tz.getID());
+
tz = CalendarUtilities.tziStringToTimeZone(GMT_UNKNOWN_DAYLIGHT_TIME);
int bias = tz.getOffset(System.currentTimeMillis());
assertEquals(0, bias);