updated UTC type adapter to support JDK 1.6 using Jackson ISO8601 parsing util
diff --git a/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java b/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java
index 16e251f..f434008 100644
--- a/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java
+++ b/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java
@@ -19,8 +19,11 @@
 import java.io.IOException;
 import java.text.DateFormat;
 import java.text.ParseException;
+import java.text.ParsePosition;
 import java.text.SimpleDateFormat;
+import java.util.Calendar;
 import java.util.Date;
+import java.util.GregorianCalendar;
 import java.util.Locale;
 import java.util.TimeZone;
 
@@ -31,33 +34,288 @@
 
 public final class UtcDateTypeAdapter extends TypeAdapter<Date> {
   private final DateFormat iso8601Format;
+  private final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC");
 
   public UtcDateTypeAdapter() {
-    this.iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US);
-    this.iso8601Format.setTimeZone(TimeZone.getTimeZone("UTC"));
+    this(false);
   }
 
-  // These methods need to be synchronized since JDK DateFormat classes are not thread-safe
-  // See issue 162
+  public UtcDateTypeAdapter(boolean jdk6Compatible) {
+    if (jdk6Compatible) {
+      this.iso8601Format = null;
+    } else {
+      // XXX is only supported by JDK 1.7+
+      this.iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US);
+      this.iso8601Format.setTimeZone(UTC_TIME_ZONE);
+    }
+  }
+
   @Override
-  public synchronized void write(JsonWriter out, Date date) throws IOException {
+  public void write(JsonWriter out, Date date) throws IOException {
     if (date == null) {
       out.nullValue();
     }
-    out.value(iso8601Format.format(date));
+    String value = null;
+    if (iso8601Format != null) {
+      synchronized (iso8601Format) {
+        // Need synchronization since JDK DateFormat classes are not thread-safe
+        try {
+          value = iso8601Format.format(date);
+        } catch (Exception e) {
+          value = null;
+        }
+      }
+    }
+    if (value == null) { // Try other formatter
+      value = format(date, true, UTC_TIME_ZONE);
+    }
+    out.value(value);
   }
 
-  // These methods need to be synchronized since JDK DateFormat classes are not thread-safe
-  // See issue 162
   @Override
-  public synchronized Date read(JsonReader in) throws IOException {
+  public Date read(JsonReader in) throws IOException {
     try {
       switch (in.peek()) {
-      case NULL: return null;
-      default: return iso8601Format.parse(in.nextString());
+      case NULL:
+        in.nextNull();
+        return null;
+      default:
+        String date = in.nextString();
+        if (iso8601Format != null) {
+          synchronized (iso8601Format) {
+            // Need synchronization since JDK DateFormat classes are not thread-safe
+            try {
+              return iso8601Format.parse(date);
+            } catch (Exception e) {
+              // Ignore and try the other parser
+            }
+          }
+        }
+        // Instead of using iso8601Format.parse(value), we use Jackson's date parsing
+        // This is because Android doesn't support XXX because it is JDK 1.6
+        return parse(date, new ParsePosition(0));
       }
     } catch (ParseException e) {
       throw new JsonParseException(e);
     }
   }
+
+  // Date parsing code from Jackson databind ISO8601Utils.java
+  // https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
+  private static final String GMT_ID = "GMT";
+
+  /**
+   * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
+   *
+   * @param date the date to format
+   * @param millis true to include millis precision otherwise false
+   * @param tz timezone to use for the formatting (GMT will produce 'Z')
+   * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
+   */
+  private static String format(Date date, boolean millis, TimeZone tz) {
+      Calendar calendar = new GregorianCalendar(tz, Locale.US);
+      calendar.setTime(date);
+
+      // estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
+      int capacity = "yyyy-MM-ddThh:mm:ss".length();
+      capacity += millis ? ".sss".length() : 0;
+      capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length();
+      StringBuilder formatted = new StringBuilder(capacity);
+
+      padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length());
+      formatted.append('-');
+      padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length());
+      formatted.append('-');
+      padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length());
+      formatted.append('T');
+      padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length());
+      formatted.append(':');
+      padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length());
+      formatted.append(':');
+      padInt(formatted, calendar.get(Calendar.SECOND), "ss".length());
+      if (millis) {
+          formatted.append('.');
+          padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length());
+      }
+
+      int offset = tz.getOffset(calendar.getTimeInMillis());
+      if (offset != 0) {
+          int hours = Math.abs((offset / (60 * 1000)) / 60);
+          int minutes = Math.abs((offset / (60 * 1000)) % 60);
+          formatted.append(offset < 0 ? '-' : '+');
+          padInt(formatted, hours, "hh".length());
+          formatted.append(':');
+          padInt(formatted, minutes, "mm".length());
+      } else {
+          formatted.append('Z');
+      }
+
+      return formatted.toString();
+  }
+  /**
+   * Zero pad a number to a specified length
+   *
+   * @param buffer buffer to use for padding
+   * @param value the integer value to pad if necessary.
+   * @param length the length of the string we should zero pad
+   */
+  private static void padInt(StringBuilder buffer, int value, int length) {
+      String strValue = Integer.toString(value);
+      for (int i = length - strValue.length(); i > 0; i--) {
+          buffer.append('0');
+      }
+      buffer.append(strValue);
+  }
+
+  /**
+   * Parse a date from ISO-8601 formatted string. It expects a format
+   * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]
+   *
+   * @param date ISO string to parse in the appropriate format.
+   * @param pos The position to start parsing from, updated to where parsing stopped.
+   * @return the parsed date
+   * @throws ParseException if the date is not in the appropriate format
+   */
+  private static Date parse(String date, ParsePosition pos) throws ParseException {
+    Exception fail = null;
+    try {
+      int offset = pos.getIndex();
+
+      // extract year
+      int year = parseInt(date, offset, offset += 4);
+      if (checkOffset(date, offset, '-')) {
+        offset += 1;
+      }
+
+      // extract month
+      int month = parseInt(date, offset, offset += 2);
+      if (checkOffset(date, offset, '-')) {
+        offset += 1;
+      }
+
+      // extract day
+      int day = parseInt(date, offset, offset += 2);
+      // default time value
+      int hour = 0;
+      int minutes = 0;
+      int seconds = 0;
+      int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time
+      if (checkOffset(date, offset, 'T')) {
+
+        // extract hours, minutes, seconds and milliseconds
+        hour = parseInt(date, offset += 1, offset += 2);
+        if (checkOffset(date, offset, ':')) {
+          offset += 1;
+        }
+
+        minutes = parseInt(date, offset, offset += 2);
+        if (checkOffset(date, offset, ':')) {
+          offset += 1;
+        }
+        // second and milliseconds can be optional
+        if (date.length() > offset) {
+          char c = date.charAt(offset);
+          if (c != 'Z' && c != '+' && c != '-') {
+            seconds = parseInt(date, offset, offset += 2);
+            // milliseconds can be optional in the format
+            if (checkOffset(date, offset, '.')) {
+              milliseconds = parseInt(date, offset += 1, offset += 3);
+            }
+          }
+        }
+      }
+
+      // extract timezone
+      String timezoneId;
+      if (date.length() <= offset) {
+        throw new IllegalArgumentException("No time zone indicator");
+      }
+      char timezoneIndicator = date.charAt(offset);
+      if (timezoneIndicator == '+' || timezoneIndicator == '-') {
+        String timezoneOffset = date.substring(offset);
+        timezoneId = GMT_ID + timezoneOffset;
+        offset += timezoneOffset.length();
+      } else if (timezoneIndicator == 'Z') {
+        timezoneId = GMT_ID;
+        offset += 1;
+      } else {
+        throw new IndexOutOfBoundsException("Invalid time zone indicator " + timezoneIndicator);
+      }
+
+      TimeZone timezone = TimeZone.getTimeZone(timezoneId);
+      if (!timezone.getID().equals(timezoneId)) {
+        throw new IndexOutOfBoundsException();
+      }
+
+      Calendar calendar = new GregorianCalendar(timezone);
+      calendar.setLenient(false);
+      calendar.set(Calendar.YEAR, year);
+      calendar.set(Calendar.MONTH, month - 1);
+      calendar.set(Calendar.DAY_OF_MONTH, day);
+      calendar.set(Calendar.HOUR_OF_DAY, hour);
+      calendar.set(Calendar.MINUTE, minutes);
+      calendar.set(Calendar.SECOND, seconds);
+      calendar.set(Calendar.MILLISECOND, milliseconds);
+
+      pos.setIndex(offset);
+      return calendar.getTime();
+      // If we get a ParseException it'll already have the right message/offset.
+      // Other exception types can convert here.
+    } catch (IndexOutOfBoundsException e) {
+      fail = e;
+    } catch (NumberFormatException e) {
+      fail = e;
+    } catch (IllegalArgumentException e) {
+      fail = e;
+    }
+    String input = (date == null) ? null : ('"' + date + "'");
+    throw new ParseException("Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex());
+  }
+
+  /**
+   * Check if the expected character exist at the given offset in the value.
+   *
+   * @param value the string to check at the specified offset
+   * @param offset the offset to look for the expected character
+   * @param expected the expected character
+   * @return true if the expected character exist at the given offset
+   */
+  private static boolean checkOffset(String value, int offset, char expected) {
+    return (offset < value.length()) && (value.charAt(offset) == expected);
+  }
+
+  /**
+   * Parse an integer located between 2 given offsets in a string
+   *
+   * @param value the string to parse
+   * @param beginIndex the start index for the integer in the string
+   * @param endIndex the end index for the integer in the string
+   * @return the int
+   * @throws NumberFormatException if the value is not a number
+   */
+  private static int parseInt(String value, int beginIndex, int endIndex) throws NumberFormatException {
+    if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {
+      throw new NumberFormatException(value);
+    }
+    // use same logic as in Integer.parseInt() but less generic we're not supporting negative values
+    int i = beginIndex;
+    int result = 0;
+    int digit;
+    if (i < endIndex) {
+      digit = Character.digit(value.charAt(i++), 10);
+      if (digit < 0) {
+        throw new NumberFormatException("Invalid number: " + value);
+      }
+      result = -digit;
+    }
+    while (i < endIndex) {
+      digit = Character.digit(value.charAt(i++), 10);
+      if (digit < 0) {
+        throw new NumberFormatException("Invalid number: " + value);
+      }
+      result *= 10;
+      result -= digit;
+    }
+    return -result;
+  }
 }
diff --git a/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java b/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java
index 0ce7e8f..701dd30 100644
--- a/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java
+++ b/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java
@@ -47,4 +47,15 @@
       assertEquals(expected.getTime(), actual.getTime());
     }
   }
+
+  /**
+   * JDK 1.7 introduced support for XXX format to indicate UTC date. But Android is older JDK.
+   * We want to make sure that this date is parseable in Android.
+   */
+  public void testUtcDatesOnJdkBefore1_7() {
+    Gson gson = new GsonBuilder()
+      .registerTypeAdapter(Date.class, new UtcDateTypeAdapter(true))
+      .create();
+    gson.fromJson("'2014-12-05T04:00:00.000Z'", Date.class);
+  }
 }