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);