Merge Android 12

Bug: 202323961
Merged-In: If0b691fb263ee2717844e8bda388310d2360ffd6
Change-Id: I9de23217a0b002a577a47086e1d6a9794a8b657f
diff --git a/src/com/android/calendarcommon2/EventRecurrence.java b/src/com/android/calendarcommon2/EventRecurrence.java
index 45fd12f..d54f2fe 100644
--- a/src/com/android/calendarcommon2/EventRecurrence.java
+++ b/src/com/android/calendarcommon2/EventRecurrence.java
@@ -17,9 +17,7 @@
 package com.android.calendarcommon2;
 
 import android.text.TextUtils;
-import android.text.format.Time;
 import android.util.Log;
-import android.util.TimeFormatException;
 
 import java.util.Calendar;
 import java.util.HashMap;
@@ -476,7 +474,7 @@
 
         EventRecurrence er = (EventRecurrence) obj;
         return  (startDate == null ?
-                        er.startDate == null : Time.compare(startDate, er.startDate) == 0) &&
+                        er.startDate == null : startDate.compareTo(er.startDate) == 0) &&
                 freq == er.freq &&
                 (until == null ? er.until == null : until.equals(er.until)) &&
                 count == er.count &&
@@ -740,7 +738,7 @@
                     // Parse the time to validate it.  The result isn't retained.
                     Time until = new Time();
                     until.parse(value);
-                } catch (TimeFormatException tfe) {
+                } catch (IllegalArgumentException iae) {
                     throw new InvalidFormatException("Invalid UNTIL value: " + value);
                 }
             }
diff --git a/src/com/android/calendarcommon2/RecurrenceProcessor.java b/src/com/android/calendarcommon2/RecurrenceProcessor.java
index d0a647a..24decce 100644
--- a/src/com/android/calendarcommon2/RecurrenceProcessor.java
+++ b/src/com/android/calendarcommon2/RecurrenceProcessor.java
@@ -17,7 +17,6 @@
 
 package com.android.calendarcommon2;
 
-import android.text.format.Time;
 import android.util.Log;
 
 import java.util.TreeSet;
@@ -93,7 +92,7 @@
                 } else if (rrule.until != null) {
                     // according to RFC 2445, until must be in UTC.
                     mIterator.parse(rrule.until);
-                    long untilTime = mIterator.toMillis(false /* use isDst */);
+                    long untilTime = mIterator.toMillis();
                     if (untilTime > lastTime) {
                         lastTime = untilTime;
                     }
@@ -129,9 +128,8 @@
             // The expansion might not contain any dates if the exrule or
             // exdates cancel all the generated dates.
             long[] dates = expand(dtstart, recur,
-                    dtstart.toMillis(false /* use isDst */) /* range start */,
-                    (maxtime != null) ?
-                            maxtime.toMillis(false /* use isDst */) : -1 /* range end */);
+                    dtstart.toMillis() /* range start */,
+                    (maxtime != null) ? maxtime.toMillis() : -1 /* range end */);
 
             // The expansion might not contain any dates if exrule or exdates
             // cancel all the generated dates.
@@ -201,7 +199,7 @@
             // BYMONTH
             if (r.bymonthCount > 0) {
                 found = listContains(r.bymonth, r.bymonthCount,
-                        iterator.month + 1);
+                        iterator.getMonth() + 1);
                 if (!found) {
                     return 1;
                 }
@@ -223,7 +221,7 @@
             // BYYEARDAY
             if (r.byyeardayCount > 0) {
                 found = listContains(r.byyearday, r.byyeardayCount,
-                                iterator.yearDay, iterator.getActualMaximum(Time.YEAR_DAY));
+                                iterator.getYearDay(), iterator.getActualMaximum(Time.YEAR_DAY));
                 if (!found) {
                     return 3;
                 }
@@ -231,7 +229,7 @@
             // BYMONTHDAY
             if (r.bymonthdayCount > 0 ) {
                 found = listContains(r.bymonthday, r.bymonthdayCount,
-                                iterator.monthDay,
+                                iterator.getDay(),
                                 iterator.getActualMaximum(Time.MONTH_DAY));
                 if (!found) {
                     return 4;
@@ -243,7 +241,7 @@
             if (r.bydayCount > 0) {
                 int a[] = r.byday;
                 int N = r.bydayCount;
-                int v = EventRecurrence.timeDay2Day(iterator.weekDay);
+                int v = EventRecurrence.timeDay2Day(iterator.getWeekDay());
                 for (int i=0; i<N; i++) {
                     if (a[i] == v) {
                         break byday;
@@ -255,7 +253,7 @@
         if (EventRecurrence.HOURLY >= freq) {
             // BYHOUR
             found = listContains(r.byhour, r.byhourCount,
-                            iterator.hour,
+                            iterator.getHour(),
                             iterator.getActualMaximum(Time.HOUR));
             if (!found) {
                 return 6;
@@ -264,7 +262,7 @@
         if (EventRecurrence.MINUTELY >= freq) {
             // BYMINUTE
             found = listContains(r.byminute, r.byminuteCount,
-                            iterator.minute,
+                            iterator.getMinute(),
                             iterator.getActualMaximum(Time.MINUTE));
             if (!found) {
                 return 7;
@@ -273,7 +271,7 @@
         if (EventRecurrence.SECONDLY >= freq) {
             // BYSECOND
             found = listContains(r.bysecond, r.bysecondCount,
-                            iterator.second,
+                            iterator.getSecond(),
                             iterator.getActualMaximum(Time.SECOND));
             if (!found) {
                 return 8;
@@ -326,7 +324,7 @@
          * (day of the month - 1) mod 7, and then make sure it's positive.  We can simplify
          * that with some algebra.
          */
-        int dotw = (instance.weekDay - instance.monthDay + 36) % 7;
+        int dotw = (instance.getWeekDay() - instance.getDay() + 36) % 7;
 
         /*
          * The byday[] values are specified as bits, so we can just OR them all
@@ -368,14 +366,14 @@
                 if (index > daySetLength) {
                     continue;  // out of range
                 }
-                if (daySet[index-1] == instance.monthDay) {
+                if (daySet[index-1] == instance.getDay()) {
                     return true;
                 }
             } else if (index < 0) {
                 if (daySetLength + index < 0) {
                     continue;  // out of range
                 }
-                if (daySet[daySetLength + index] == instance.monthDay) {
+                if (daySet[daySetLength + index] == instance.getDay()) {
                     return true;
                 }
             } else {
@@ -429,29 +427,29 @@
 
         boolean get(Time iterator, int day)
         {
-            int realYear = iterator.year;
-            int realMonth = iterator.month;
+            int realYear = iterator.getYear();
+            int realMonth = iterator.getMonth();
 
             Time t = null;
 
             if (SPEW) {
                 Log.i(TAG, "get called with iterator=" + iterator
-                        + " " + iterator.month
-                        + "/" + iterator.monthDay
-                        + "/" + iterator.year + " day=" + day);
+                        + " " + iterator.getMonth()
+                        + "/" + iterator.getDay()
+                        + "/" + iterator.getYear() + " day=" + day);
             }
             if (day < 1 || day > 28) {
                 // if might be past the end of the month, we need to normalize it
                 t = mTime;
                 t.set(day, realMonth, realYear);
                 unsafeNormalize(t);
-                realYear = t.year;
-                realMonth = t.month;
-                day = t.monthDay;
+                realYear = t.getYear();
+                realMonth = t.getMonth();
+                day = t.getDay();
                 if (SPEW) {
-                    Log.i(TAG, "normalized t=" + t + " " + t.month
-                            + "/" + t.monthDay
-                            + "/" + t.year);
+                    Log.i(TAG, "normalized t=" + t + " " + t.getMonth()
+                            + "/" + t.getDay()
+                            + "/" + t.getYear());
                 }
             }
 
@@ -466,9 +464,9 @@
                     t.set(day, realMonth, realYear);
                     unsafeNormalize(t);
                     if (SPEW) {
-                        Log.i(TAG, "set t=" + t + " " + t.month
-                                + "/" + t.monthDay
-                                + "/" + t.year
+                        Log.i(TAG, "set t=" + t + " " + t.getMonth()
+                                + "/" + t.getDay()
+                                + "/" + t.getYear()
                                 + " realMonth=" + realMonth + " mMonth=" + mMonth);
                     }
                 }
@@ -507,11 +505,11 @@
             count = r.bydayCount;
             if (count > 0) {
                 // calculate the day of week for the first of this month (first)
-                j = generated.monthDay;
+                j = generated.getDay();
                 while (j >= 8) {
                     j -= 7;
                 }
-                first = generated.weekDay;
+                first = generated.getWeekDay();
                 if (first >= j) {
                     first = first - j + 1;
                 } else {
@@ -631,13 +629,13 @@
      * UTC milliseconds; use -1 for the entire range.
      * @return an array of dates, each date is in UTC milliseconds
      * @throws DateException
-     * @throws android.util.TimeFormatException if recur cannot be parsed
+     * @throws IllegalArgumentException if recur cannot be parsed
      */
     public long[] expand(Time dtstart,
             RecurrenceSet recur,
             long rangeStartMillis,
             long rangeEndMillis) throws DateException {
-        String timezone = dtstart.timezone;
+        String timezone = dtstart.getTimezone();
         mIterator.clear(timezone);
         mGenerated.clear(timezone);
 
@@ -703,7 +701,7 @@
         int i = 0;
         for (Long val: dtSet) {
             setTimeFromLongValue(mIterator, val);
-            dates[i++] = mIterator.toMillis(true /* ignore isDst */);
+            dates[i++] = mIterator.toMillis();
         }
         return dates;
     }
@@ -728,7 +726,7 @@
      * @param add Whether or not we should add to out, or remove from out.
      * @param out the TreeSet you'd like to fill with the events
      * @throws DateException
-     * @throws android.util.TimeFormatException if r cannot be parsed.
+     * @throws IllegalArgumentException if r cannot be parsed.
      */
     public void expand(Time dtstart,
             EventRecurrence r,
@@ -827,7 +825,7 @@
                     // we'll skip months if it's greater than 28.
                     // XXX Do we generate days for MONTHLY w/ BYHOUR?  If so,
                     // we need to do this then too.
-                    iterator.monthDay = 1;
+                    iterator.setDay(1);
                 }
             }
 
@@ -847,7 +845,7 @@
                 // We need the "until" year/month/day values to be in the same
                 // timezone as all the generated dates so that we can compare them
                 // using the values returned by normDateTimeComparisonValue().
-                until.switchTimezone(dtstart.timezone);
+                until.switchTimezone(dtstart.getTimezone());
                 untilDateValue = normDateTimeComparisonValue(until);
             } else {
                 untilDateValue = Long.MAX_VALUE;
@@ -876,17 +874,17 @@
 
                     unsafeNormalize(iterator);
 
-                    int iteratorYear = iterator.year;
-                    int iteratorMonth = iterator.month + 1;
-                    int iteratorDay = iterator.monthDay;
-                    int iteratorHour = iterator.hour;
-                    int iteratorMinute = iterator.minute;
-                    int iteratorSecond = iterator.second;
+                    int iteratorYear = iterator.getYear();
+                    int iteratorMonth = iterator.getMonth() + 1;
+                    int iteratorDay = iterator.getDay();
+                    int iteratorHour = iterator.getHour();
+                    int iteratorMinute = iterator.getMinute();
+                    int iteratorSecond = iterator.getSecond();
 
                     // year is never expanded -- there is no BYYEAR
                     generated.set(iterator);
 
-                    if (SPEW) Log.i(TAG, "year=" + generated.year);
+                    if (SPEW) Log.i(TAG, "year=" + generated.getYear());
 
                     do { // month
                         int month = usebymonth
@@ -923,9 +921,9 @@
                                  * Thursday.  If weeks started on Mondays, we would only
                                  * need to move back (2 - 1 + 7) % 7 = 1 day.
                                  */
-                                int weekStartAdj = (iterator.weekDay -
+                                int weekStartAdj = (iterator.getWeekDay() -
                                         EventRecurrence.day2TimeDay(r.wkst) + 7) % 7;
-                                dayIndex = iterator.monthDay - weekStartAdj;
+                                dayIndex = iterator.getDay() - weekStartAdj;
                                 lastDayToExamine = dayIndex + 6;
                             } else {
                                 lastDayToExamine = generated
@@ -1065,35 +1063,21 @@
                     // We don't want to "generate" dates with the iterator.
                     // XXX: We do this for days, because there is a varying number of days
                     // per month
-                    int oldDay = iterator.monthDay;
+                    int oldDay = iterator.getDay();
                     generated.set(iterator);  // just using generated as a temporary.
                     int n = 1;
                     while (true) {
                         int value = freqAmount * n;
                         switch (freqField) {
                             case Time.SECOND:
-                                iterator.second += value;
-                                break;
                             case Time.MINUTE:
-                                iterator.minute += value;
-                                break;
                             case Time.HOUR:
-                                iterator.hour += value;
-                                break;
                             case Time.MONTH_DAY:
-                                iterator.monthDay += value;
-                                break;
                             case Time.MONTH:
-                                iterator.month += value;
-                                break;
                             case Time.YEAR:
-                                iterator.year += value;
-                                break;
                             case Time.WEEK_DAY:
-                                iterator.monthDay += value;
-                                break;
                             case Time.YEAR_DAY:
-                                iterator.monthDay += value;
+                                iterator.add(freqField, value);
                                 break;
                             default:
                                 throw new RuntimeException("bad field=" + freqField);
@@ -1103,7 +1087,7 @@
                         if (freqField != Time.YEAR && freqField != Time.MONTH) {
                             break;
                         }
-                        if (iterator.monthDay == oldDay) {
+                        if (iterator.getDay() == oldDay) {
                             break;
                         }
                         n++;
@@ -1136,12 +1120,12 @@
      * This method does not modify the fields isDst, or gmtOff.
      */
     static void unsafeNormalize(Time date) {
-        int second = date.second;
-        int minute = date.minute;
-        int hour = date.hour;
-        int monthDay = date.monthDay;
-        int month = date.month;
-        int year = date.year;
+        int second = date.getSecond();
+        int minute = date.getMinute();
+        int hour = date.getHour();
+        int monthDay = date.getDay();
+        int month = date.getMonth();
+        int year = date.getYear();
 
         int addMinutes = ((second < 0) ? (second - 59) : second) / 60;
         second -= addMinutes * 60;
@@ -1202,14 +1186,14 @@
         // At this point, monthDay <= the length of the current month and is
         // in the range [1,31].
 
-        date.second = second;
-        date.minute = minute;
-        date.hour = hour;
-        date.monthDay = monthDay;
-        date.month = month;
-        date.year = year;
-        date.weekDay = weekDay(year, month, monthDay);
-        date.yearDay = yearDay(year, month, monthDay);
+        date.setSecond(second);
+        date.setMinute(minute);
+        date.setHour(hour);
+        date.setDay(monthDay);
+        date.setMonth(month);
+        date.setYear(year);
+        date.setWeekDay(weekDay(year, month, monthDay));
+        date.setYearDay(yearDay(year, month, monthDay));
     }
 
     /**
@@ -1300,17 +1284,17 @@
     private static final long normDateTimeComparisonValue(Time normalized) {
         // 37 bits for the year, 4 bits for the month, 5 bits for the monthDay,
         // 5 bits for the hour, 6 bits for the minute, 6 bits for the second.
-        return ((long)normalized.year << 26) + (normalized.month << 22)
-                + (normalized.monthDay << 17) + (normalized.hour << 12)
-                + (normalized.minute << 6) + normalized.second;
+        return ((long)normalized.getYear() << 26) + (normalized.getMonth() << 22)
+                + (normalized.getDay() << 17) + (normalized.getHour() << 12)
+                + (normalized.getMinute() << 6) + normalized.getSecond();
     }
 
     private static final void setTimeFromLongValue(Time date, long val) {
-        date.year = (int) (val >> 26);
-        date.month = (int) (val >> 22) & 0xf;
-        date.monthDay = (int) (val >> 17) & 0x1f;
-        date.hour = (int) (val >> 12) & 0x1f;
-        date.minute = (int) (val >> 6) & 0x3f;
-        date.second = (int) (val & 0x3f);
+        date.setYear((int) (val >> 26));
+        date.setMonth((int) (val >> 22) & 0xf);
+        date.setDay((int) (val >> 17) & 0x1f);
+        date.setHour((int) (val >> 12) & 0x1f);
+        date.setMinute((int) (val >> 6) & 0x3f);
+        date.setSecond((int) (val & 0x3f));
     }
 }
diff --git a/src/com/android/calendarcommon2/RecurrenceSet.java b/src/com/android/calendarcommon2/RecurrenceSet.java
index 86e6a2d..e42c0e9 100644
--- a/src/com/android/calendarcommon2/RecurrenceSet.java
+++ b/src/com/android/calendarcommon2/RecurrenceSet.java
@@ -20,9 +20,7 @@
 import android.database.Cursor;
 import android.provider.CalendarContract;
 import android.text.TextUtils;
-import android.text.format.Time;
 import android.util.Log;
-import android.util.TimeFormatException;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -162,14 +160,14 @@
             // The timezone is updated to UTC if the time string specified 'Z'.
             try {
                 time.parse(rawDates[i]);
-            } catch (TimeFormatException e) {
+            } catch (IllegalArgumentException e) {
                 throw new EventRecurrence.InvalidFormatException(
-                        "TimeFormatException thrown when parsing time " + rawDates[i]
+                        "IllegalArgumentException thrown when parsing time " + rawDates[i]
                                 + " in recurrence " + recurrence);
 
             }
-            dates[i] = time.toMillis(false /* use isDst */);
-            time.timezone = tz;
+            dates[i] = time.toMillis();
+            time.setTimezone(tz);
         }
         return dates;
     }
@@ -196,8 +194,9 @@
             // NOTE: the timezone may be null, if this is a floating time.
             String tzid = tzidParam == null ? null : tzidParam.value;
             Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid);
-            boolean inUtc = start.parse(dtstart);
-            boolean allDay = start.allDay;
+            start.parse(dtstart);
+            boolean inUtc = dtstart.length() == 16 && dtstart.charAt(15) == 'Z';
+            boolean allDay = start.isAllDay();
 
             // We force TimeZone to UTC for "all day recurring events" as the server is sending no
             // TimeZone in DTSTART for them
@@ -224,9 +223,9 @@
             }
 
             if (allDay) {
-                start.timezone = Time.TIMEZONE_UTC;
+                start.setTimezone(Time.TIMEZONE_UTC);
             }
-            long millis = start.toMillis(false /* use isDst */);
+            long millis = start.toMillis();
             values.put(CalendarContract.Events.DTSTART, millis);
             if (millis == -1) {
                 if (false) {
@@ -243,7 +242,7 @@
             values.put(CalendarContract.Events.DURATION, duration);
             values.put(CalendarContract.Events.ALL_DAY, allDay ? 1 : 0);
             return true;
-        } catch (TimeFormatException e) {
+        } catch (IllegalArgumentException e) {
             // Something is wrong with the format of this event
             Log.i(TAG,"Failed to parse event: " + component.toString());
             return false;
@@ -301,10 +300,10 @@
         // TODO: android.pim.Time really should take care of this for us.
         if (allDay) {
             dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
-            dtstartTime.allDay = true;
-            dtstartTime.hour = 0;
-            dtstartTime.minute = 0;
-            dtstartTime.second = 0;
+            dtstartTime.setAllDay(true);
+            dtstartTime.setHour(0);
+            dtstartTime.setMinute(0);
+            dtstartTime.setSecond(0);
         }
 
         dtstartProp.setValue(dtstartTime.format2445());
@@ -360,10 +359,10 @@
         // TODO: android.pim.Time really should take care of this for us.
         if (allDay) {
             dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
-            dtstartTime.allDay = true;
-            dtstartTime.hour = 0;
-            dtstartTime.minute = 0;
-            dtstartTime.second = 0;
+            dtstartTime.setAllDay(true);
+            dtstartTime.setHour(0);
+            dtstartTime.setMinute(0);
+            dtstartTime.setSecond(0);
         }
 
         dtstartProp.setValue(dtstartTime.format2445());
@@ -480,14 +479,13 @@
         ICalendar.Parameter endTzidParameter =
                 dtendProperty.getFirstParameter("TZID");
         String endTzid = (endTzidParameter == null)
-                ? start.timezone : endTzidParameter.value;
+                ? start.getTimezone() : endTzidParameter.value;
 
         Time end = new Time(endTzid);
         end.parse(dtendProperty.getValue());
-        long durationMillis = end.toMillis(false /* use isDst */)
-                - start.toMillis(false /* use isDst */);
+        long durationMillis = end.toMillis() - start.toMillis();
         long durationSeconds = (durationMillis / 1000);
-        if (start.allDay && (durationSeconds % 86400) == 0) {
+        if (start.isAllDay() && (durationSeconds % 86400) == 0) {
             return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S
         } else {
             return "P" + durationSeconds + "S";
diff --git a/src/com/android/calendarcommon2/Time.java b/src/com/android/calendarcommon2/Time.java
new file mode 100644
index 0000000..f0af248
--- /dev/null
+++ b/src/com/android/calendarcommon2/Time.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed 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.android.calendarcommon2;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Helper class to make migration out of android.text.format.Time smoother.
+ */
+public class Time {
+
+    public static final String TIMEZONE_UTC = "UTC";
+
+    private static final int EPOCH_JULIAN_DAY = 2440588;
+    private static final long HOUR_IN_MILLIS = 60 * 60 * 1000;
+    private static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
+
+    private static final String FORMAT_ALL_DAY_PATTERN = "yyyyMMdd";
+    private static final String FORMAT_TIME_PATTERN = "yyyyMMdd'T'HHmmss";
+    private static final String FORMAT_TIME_UTC_PATTERN = "yyyyMMdd'T'HHmmss'Z'";
+    private static final String FORMAT_LOG_TIME_PATTERN = "EEE, MMM dd, yyyy hh:mm a";
+
+    /*
+     * Define symbolic constants for accessing the fields in this class. Used in
+     * getActualMaximum().
+     */
+    public static final int SECOND = 1;
+    public static final int MINUTE = 2;
+    public static final int HOUR = 3;
+    public static final int MONTH_DAY = 4;
+    public static final int MONTH = 5;
+    public static final int YEAR = 6;
+    public static final int WEEK_DAY = 7;
+    public static final int YEAR_DAY = 8;
+    public static final int WEEK_NUM = 9;
+
+    public static final int SUNDAY = 0;
+    public static final int MONDAY = 1;
+    public static final int TUESDAY = 2;
+    public static final int WEDNESDAY = 3;
+    public static final int THURSDAY = 4;
+    public static final int FRIDAY = 5;
+    public static final int SATURDAY = 6;
+
+    private final GregorianCalendar mCalendar;
+
+    private int year;
+    private int month;
+    private int monthDay;
+    private int hour;
+    private int minute;
+    private int second;
+
+    private int yearDay;
+    private int weekDay;
+
+    private String timezone;
+    private boolean allDay;
+
+    /**
+     * Enabling this flag will apply appropriate dst transition logic when calling either
+     * {@code toMillis()} or {@code normalize()} and their respective *ApplyDst() equivalents. <br>
+     * When this flag is enabled, the following calls would be considered equivalent:
+     * <ul>
+     *     <li>{@code a.t.f.Time#normalize(true)} and {@code #normalize()}</li>
+     *     <li>{@code a.t.f.Time#toMillis(true)} and {@code #toMillis()}</li>
+     *     <li>{@code a.t.f.Time#normalize(false)} and {@code #normalizeApplyDst()}</li>
+     *     <li>{@code a.t.f.Time#toMillis(false)} and {@code #toMillisApplyDst()}</li>
+     * </ul>
+     * When the flag is disabled, both {@code toMillis()} and {@code normalize()} will ignore any
+     * dst transitions unless minutes or hours were added to the time (the default behavior of the
+     * a.t.f.Time class). <br>
+     *
+     * NOTE: currently, this flag is disabled because there are no direct manipulations of the day,
+     * hour, or minute fields. All of the accesses are correctly done via setters and they rely on
+     * a private normalize call in their respective classes to achieve their expected behavior.
+     * Additionally, using any of the {@code #set()} methods or {@code #parse()} will result in
+     * normalizing by ignoring DST, which is what the default behavior is for the a.t.f.Time class.
+     */
+    static final boolean APPLY_DST_CHANGE_LOGIC = false;
+    private int mDstChangedByField = -1;
+
+    public Time() {
+        this(TimeZone.getDefault().getID());
+    }
+
+    public Time(String timezone) {
+        if (timezone == null) {
+            throw new NullPointerException("timezone cannot be null.");
+        }
+        this.timezone = timezone;
+        // Although the process's default locale is used here, #clear() will explicitly set the
+        // first day of the week to MONDAY to match with the expected a.t.f.Time implementation.
+        mCalendar = new GregorianCalendar(getTimeZone(), Locale.getDefault());
+        clear(this.timezone);
+    }
+
+    private void readFieldsFromCalendar() {
+        year = mCalendar.get(Calendar.YEAR);
+        month = mCalendar.get(Calendar.MONTH);
+        monthDay = mCalendar.get(Calendar.DAY_OF_MONTH);
+        hour = mCalendar.get(Calendar.HOUR_OF_DAY);
+        minute = mCalendar.get(Calendar.MINUTE);
+        second = mCalendar.get(Calendar.SECOND);
+    }
+
+    private void writeFieldsToCalendar() {
+        clearCalendar();
+        mCalendar.set(year, month, monthDay, hour, minute, second);
+        mCalendar.set(Calendar.MILLISECOND, 0);
+    }
+
+    private boolean isInDst() {
+        return mCalendar.getTimeZone().inDaylightTime(mCalendar.getTime());
+    }
+
+    public void add(int field, int amount) {
+        final boolean wasDstBefore = isInDst();
+        mCalendar.add(getCalendarField(field), amount);
+        if (APPLY_DST_CHANGE_LOGIC && wasDstBefore != isInDst()
+                && (field == MONTH_DAY || field == HOUR || field == MINUTE)) {
+            mDstChangedByField = field;
+        }
+    }
+
+    public void set(long millis) {
+        clearCalendar();
+        mCalendar.setTimeInMillis(millis);
+        readFieldsFromCalendar();
+    }
+
+    public void set(Time other) {
+        clearCalendar();
+        mCalendar.setTimeZone(other.getTimeZone());
+        mCalendar.setTimeInMillis(other.mCalendar.getTimeInMillis());
+        readFieldsFromCalendar();
+    }
+
+    public void set(int day, int month, int year) {
+        clearCalendar();
+        mCalendar.set(year, month, day);
+        readFieldsFromCalendar();
+    }
+
+    public void set(int second, int minute, int hour, int day, int month, int year) {
+        clearCalendar();
+        mCalendar.set(year, month, day, hour, minute, second);
+        readFieldsFromCalendar();
+    }
+
+    public long setJulianDay(int julianDay) {
+        long millis = (julianDay - EPOCH_JULIAN_DAY) * DAY_IN_MILLIS;
+        mCalendar.setTimeInMillis(millis);
+        readFieldsFromCalendar();
+
+        // adjust day approximation, set the time to 12am, and re-normalize
+        monthDay += julianDay - getJulianDay(millis, getGmtOffset());
+        hour = 0;
+        minute = 0;
+        second = 0;
+        writeFieldsToCalendar();
+        return normalize();
+    }
+
+    public static int getJulianDay(long begin, long gmtOff) {
+        return android.text.format.Time.getJulianDay(begin, gmtOff);
+    }
+
+    public int getWeekNumber() {
+        return mCalendar.get(Calendar.WEEK_OF_YEAR);
+    }
+
+    private int getCalendarField(int field) {
+        switch (field) {
+            case SECOND: return Calendar.SECOND;
+            case MINUTE: return Calendar.MINUTE;
+            case HOUR: return Calendar.HOUR_OF_DAY;
+            case MONTH_DAY: return Calendar.DAY_OF_MONTH;
+            case MONTH: return Calendar.MONTH;
+            case YEAR: return Calendar.YEAR;
+            case WEEK_DAY: return Calendar.DAY_OF_WEEK;
+            case YEAR_DAY: return Calendar.DAY_OF_YEAR;
+            case WEEK_NUM: return Calendar.WEEK_OF_YEAR;
+            default:
+                throw new RuntimeException("bad field=" + field);
+        }
+    }
+
+    public int getActualMaximum(int field) {
+        return mCalendar.getActualMaximum(getCalendarField(field));
+    }
+
+    public void switchTimezone(String timezone) {
+        long msBefore = mCalendar.getTimeInMillis();
+        mCalendar.setTimeZone(TimeZone.getTimeZone(timezone));
+        mCalendar.setTimeInMillis(msBefore);
+        mDstChangedByField = -1;
+        readFieldsFromCalendar();
+    }
+
+    /**
+     * @param apply whether to apply dst logic on the ms or not; if apply is true, it is equivalent
+     *              to calling the normalize or toMillis APIs in a.t.f.Time with ignoreDst=false
+     */
+    private long getDstAdjustedMillis(boolean apply, long ms) {
+        if (APPLY_DST_CHANGE_LOGIC) {
+            if (apply && mDstChangedByField == MONTH_DAY) {
+                return isInDst() ? (ms + HOUR_IN_MILLIS) : (ms - HOUR_IN_MILLIS);
+            } else if (!apply && (mDstChangedByField == HOUR || mDstChangedByField == MINUTE)) {
+                return isInDst() ? (ms - HOUR_IN_MILLIS) : (ms + HOUR_IN_MILLIS);
+            }
+        }
+        return ms;
+    }
+
+    private long normalizeInternal() {
+        final long ms = mCalendar.getTimeInMillis();
+        readFieldsFromCalendar();
+        return ms;
+    }
+
+    public long normalize() {
+        return getDstAdjustedMillis(false, normalizeInternal());
+    }
+
+    long normalizeApplyDst() {
+        return getDstAdjustedMillis(true, normalizeInternal());
+    }
+
+    public void parse(String time) {
+        if (time == null) {
+            throw new NullPointerException("time string is null");
+        }
+        parseInternal(time);
+        writeFieldsToCalendar();
+    }
+
+    public String format2445() {
+        writeFieldsToCalendar();
+        final SimpleDateFormat sdf = new SimpleDateFormat(
+                allDay ? FORMAT_ALL_DAY_PATTERN
+                       : (TIMEZONE_UTC.equals(getTimezone()) ? FORMAT_TIME_UTC_PATTERN
+                                                             : FORMAT_TIME_PATTERN));
+        sdf.setTimeZone(getTimeZone());
+        return sdf.format(mCalendar.getTime());
+    }
+
+    public long toMillis() {
+        return getDstAdjustedMillis(false, mCalendar.getTimeInMillis());
+    }
+
+    long toMillisApplyDst() {
+        return getDstAdjustedMillis(true, mCalendar.getTimeInMillis());
+    }
+
+    private TimeZone getTimeZone() {
+        return timezone != null ? TimeZone.getTimeZone(timezone) : TimeZone.getDefault();
+    }
+
+    public int compareTo(Time other) {
+        return mCalendar.compareTo(other.mCalendar);
+    }
+
+    private void clearCalendar() {
+        mDstChangedByField = -1;
+        mCalendar.clear();
+        mCalendar.set(Calendar.HOUR_OF_DAY, 0); // HOUR_OF_DAY doesn't get reset with #clear
+        mCalendar.setTimeZone(getTimeZone());
+        // set fields for week number computation according to ISO 8601.
+        mCalendar.setFirstDayOfWeek(Calendar.MONDAY);
+        mCalendar.setMinimalDaysInFirstWeek(4);
+    }
+
+    public void clear(String timezoneId) {
+        clearCalendar();
+        readFieldsFromCalendar();
+        setTimezone(timezoneId);
+    }
+
+    public int getYear() {
+        return mCalendar.get(Calendar.YEAR);
+    }
+
+    public void setYear(int year) {
+        this.year = year;
+        mCalendar.set(Calendar.YEAR, year);
+    }
+
+    public int getMonth() {
+        return mCalendar.get(Calendar.MONTH);
+    }
+
+    public void setMonth(int month) {
+        this.month = month;
+        mCalendar.set(Calendar.MONTH, month);
+    }
+
+    public int getDay() {
+        return mCalendar.get(Calendar.DAY_OF_MONTH);
+    }
+
+    public void setDay(int day) {
+        this.monthDay = day;
+        mCalendar.set(Calendar.DAY_OF_MONTH, day);
+    }
+
+    public int getHour() {
+        return mCalendar.get(Calendar.HOUR_OF_DAY);
+    }
+
+    public void setHour(int hour) {
+        this.hour = hour;
+        mCalendar.set(Calendar.HOUR_OF_DAY, hour);
+    }
+
+    public int getMinute() {
+        return mCalendar.get(Calendar.MINUTE);
+    }
+
+    public void setMinute(int minute) {
+        this.minute = minute;
+        mCalendar.set(Calendar.MINUTE, minute);
+    }
+
+    public int getSecond() {
+        return mCalendar.get(Calendar.SECOND);
+    }
+
+    public void setSecond(int second) {
+        this.second = second;
+        mCalendar.set(Calendar.SECOND, second);
+    }
+
+    public String getTimezone() {
+        return mCalendar.getTimeZone().getID();
+    }
+
+    public void setTimezone(String timezone) {
+        this.timezone = timezone;
+        mCalendar.setTimeZone(getTimeZone());
+    }
+
+    public int getYearDay() {
+        // yearDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1.
+        return mCalendar.get(Calendar.DAY_OF_YEAR) - 1;
+    }
+
+    public void setYearDay(int yearDay) {
+        this.yearDay = yearDay;
+        // yearDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1.
+        mCalendar.set(Calendar.DAY_OF_YEAR, yearDay + 1);
+    }
+
+    public int getWeekDay() {
+        // weekDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1.
+        return mCalendar.get(Calendar.DAY_OF_WEEK) - 1;
+    }
+
+    public void setWeekDay(int weekDay) {
+        this.weekDay = weekDay;
+        // weekDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1.
+        mCalendar.set(Calendar.DAY_OF_WEEK, weekDay + 1);
+    }
+
+    public boolean isAllDay() {
+        return allDay;
+    }
+
+    public void setAllDay(boolean allDay) {
+        this.allDay = allDay;
+    }
+
+    public long getGmtOffset() {
+        return mCalendar.getTimeZone().getOffset(mCalendar.getTimeInMillis()) / 1000;
+    }
+
+    private void parseInternal(String s) {
+        int len = s.length();
+        if (len < 8) {
+            throw new IllegalArgumentException("String is too short: \"" + s +
+                    "\" Expected at least 8 characters.");
+        } else if (len > 8 && len < 15) {
+            throw new IllegalArgumentException("String is too short: \"" + s
+                    + "\" If there are more than 8 characters there must be at least 15.");
+        }
+
+        // year
+        int n = getChar(s, 0, 1000);
+        n += getChar(s, 1, 100);
+        n += getChar(s, 2, 10);
+        n += getChar(s, 3, 1);
+        year = n;
+
+        // month
+        n = getChar(s, 4, 10);
+        n += getChar(s, 5, 1);
+        n--;
+        month = n;
+
+        // day of month
+        n = getChar(s, 6, 10);
+        n += getChar(s, 7, 1);
+        monthDay = n;
+
+        if (len > 8) {
+            checkChar(s, 8, 'T');
+            allDay = false;
+
+            // hour
+            n = getChar(s, 9, 10);
+            n += getChar(s, 10, 1);
+            hour = n;
+
+            // min
+            n = getChar(s, 11, 10);
+            n += getChar(s, 12, 1);
+            minute = n;
+
+            // sec
+            n = getChar(s, 13, 10);
+            n += getChar(s, 14, 1);
+            second = n;
+
+            if (len > 15) {
+                // Z
+                checkChar(s, 15, 'Z');
+                timezone = TIMEZONE_UTC;
+            }
+        } else {
+            allDay = true;
+            hour = 0;
+            minute = 0;
+            second = 0;
+        }
+
+        weekDay = 0;
+        yearDay = 0;
+    }
+
+    private void checkChar(String s, int spos, char expected) {
+        final char c = s.charAt(spos);
+        if (c != expected) {
+            throw new IllegalArgumentException(String.format(
+                    "Unexpected character 0x%02d at pos=%d.  Expected 0x%02d (\'%c\').",
+                    (int) c, spos, (int) expected, expected));
+        }
+    }
+
+    private int getChar(String s, int spos, int mul) {
+        final char c = s.charAt(spos);
+        if (Character.isDigit(c)) {
+            return Character.getNumericValue(c) * mul;
+        } else {
+            throw new IllegalArgumentException("Parse error at pos=" + spos);
+        }
+    }
+
+    // NOTE: only used for outputting time to error logs
+    public String format() {
+        final SimpleDateFormat sdf =
+                new SimpleDateFormat(FORMAT_LOG_TIME_PATTERN, Locale.getDefault());
+        return sdf.format(mCalendar.getTime());
+    }
+
+    // NOTE: only used in tests
+    public boolean parse3339(String time) {
+        android.text.format.Time tmp = generateInstance();
+        boolean success = tmp.parse3339(time);
+        copyAndWriteInstance(tmp);
+        return success;
+    }
+
+    // NOTE: only used in tests
+    public String format3339(boolean allDay) {
+        return generateInstance().format3339(allDay);
+    }
+
+    private android.text.format.Time generateInstance() {
+        android.text.format.Time tmp = new android.text.format.Time(timezone);
+        tmp.set(second, minute, hour, monthDay, month, year);
+
+        tmp.yearDay = yearDay;
+        tmp.weekDay = weekDay;
+
+        tmp.timezone = timezone;
+        tmp.gmtoff = getGmtOffset();
+        tmp.allDay = allDay;
+        tmp.set(mCalendar.getTimeInMillis());
+        if (tmp.allDay && (tmp.hour != 0 || tmp.minute != 0 || tmp.second != 0)) {
+            // Time SDK expects hour, minute, second to be 0 if allDay is true
+            tmp.hour = 0;
+            tmp.minute = 0;
+            tmp.second = 0;
+        }
+
+        return tmp;
+    }
+
+    private void copyAndWriteInstance(android.text.format.Time time) {
+        year = time.year;
+        month = time.month;
+        monthDay = time.monthDay;
+        hour = time.hour;
+        minute = time.minute;
+        second = time.second;
+
+        yearDay = time.yearDay;
+        weekDay = time.weekDay;
+
+        timezone = time.timezone;
+        allDay = time.allDay;
+
+        writeFieldsToCalendar();
+    }
+}
diff --git a/tests/src/com/android/calendarcommon2/RRuleTest.java b/tests/src/com/android/calendarcommon2/RRuleTest.java
index 1d72366..18217a3 100644
--- a/tests/src/com/android/calendarcommon2/RRuleTest.java
+++ b/tests/src/com/android/calendarcommon2/RRuleTest.java
@@ -24,7 +24,6 @@
 import android.os.Debug;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.test.suitebuilder.annotation.Suppress;
-import android.text.format.Time;
 import junit.framework.TestCase;
 
 /**
@@ -115,8 +114,7 @@
         RecurrenceProcessor rp = new RecurrenceProcessor();
         RecurrenceSet recur = new RecurrenceSet(rrule, rdate, exrule, exdate);
 
-        long[] out = rp.expand(dtstart, recur, rangeStart.toMillis(false /* use isDst */),
-                rangeEnd.toMillis(false /* use isDst */));
+        long[] out = rp.expand(dtstart, recur, rangeStart.toMillis(), rangeEnd.toMillis());
 
         if (METHOD_TRACE) {
             Debug.stopMethodTracing();
diff --git a/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java b/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java
index 3503aae..3cd9177 100644
--- a/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java
+++ b/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java
@@ -22,9 +22,7 @@
 import android.test.suitebuilder.annotation.MediumTest;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.text.TextUtils;
-import android.text.format.Time;
 import android.util.Log;
-import android.util.TimeFormatException;
 import junit.framework.TestCase;
 
 import java.util.TreeSet;
@@ -106,8 +104,7 @@
         RecurrenceProcessor rp = new RecurrenceProcessor();
         RecurrenceSet recur = new RecurrenceSet(rrule, rdate, exrule, exdate);
 
-        long[] out = rp.expand(dtstart, recur, rangeStart.toMillis(false /* use isDst */),
-                rangeEnd.toMillis(false /* use isDst */));
+        long[] out = rp.expand(dtstart, recur, rangeStart.toMillis(), rangeEnd.toMillis());
 
         if (METHOD_TRACE) {
             Debug.stopMethodTracing();
@@ -150,12 +147,12 @@
         if (lastOccur != -1) {
             outCal.set(lastOccur);
             lastStr = outCal.format2445();
-            lastMillis = outCal.toMillis(true /* ignore isDst */);
+            lastMillis = outCal.toMillis();
         }
         if (last != null && last.length() > 0) {
             Time expectedLast = new Time(tz);
             expectedLast.parse(last);
-            expectedMillis = expectedLast.toMillis(true /* ignore isDst */);
+            expectedMillis = expectedLast.toMillis();
         }
         if (lastMillis != expectedMillis) {
             if (SPEW) {
@@ -598,7 +595,7 @@
                             "20060219T100000"
                     }, "20060220T020001");
             fail("Bad UNTIL string failed to throw exception");
-        } catch (TimeFormatException e) {
+        } catch (IllegalArgumentException e) {
             // expected
         }
     }
@@ -2460,8 +2457,8 @@
         dtstart.parse("20010101T000000");
         rangeStart.parse("20010101T000000");
         rangeEnd.parse("20090101T000000");
-        long rangeStartMillis = rangeStart.toMillis(false /* use isDst */);
-        long rangeEndMillis = rangeEnd.toMillis(false /* use isDst */);
+        long rangeStartMillis = rangeStart.toMillis();
+        long rangeEndMillis = rangeEnd.toMillis();
 
         long startTime = System.currentTimeMillis();
         for (int iterations = 0; iterations < 5; iterations++) {
@@ -2504,12 +2501,12 @@
         long startTime = System.currentTimeMillis();
 
         for (int i = 0; i < ITERATIONS; i++) {
-            date.month += 1;
-            date.monthDay += 100;
-            date.normalize(true);
-            date.month -= 1;
-            date.monthDay -= 100;
-            date.normalize(true);
+            date.add(Time.MONTH, 1);
+            date.add(Time.MONTH_DAY, 100);
+            date.normalize();
+            date.add(Time.MONTH, -1);
+            date.add(Time.MONTH_DAY, -100);
+            date.normalize();
         }
 
         long endTime = System.currentTimeMillis();
@@ -2521,11 +2518,11 @@
         date.parse("20090404T100000");
         startTime = System.currentTimeMillis();
         for (int i = 0; i < ITERATIONS; i++) {
-            date.month += 1;
-            date.monthDay += 100;
+            date.add(Time.MONTH, 1);
+            date.add(Time.MONTH_DAY, 100);
             RecurrenceProcessor.unsafeNormalize(date);
-            date.month -= 1;
-            date.monthDay -= 100;
+            date.add(Time.MONTH, -1);
+            date.add(Time.MONTH_DAY, -100);
             RecurrenceProcessor.unsafeNormalize(date);
         }
 
diff --git a/tests/src/com/android/calendarcommon2/TimeTest.java b/tests/src/com/android/calendarcommon2/TimeTest.java
new file mode 100644
index 0000000..df27c4f
--- /dev/null
+++ b/tests/src/com/android/calendarcommon2/TimeTest.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed 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.android.calendarcommon2;
+
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.TimeFormatException;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for com.android.calendarcommon2.Time.
+ *
+ * Some of these tests are borrowed from android.text.format.TimeTest.
+ */
+public class TimeTest extends TestCase {
+
+    @SmallTest
+    public void testNullTimezone() {
+        try {
+            Time t = new Time(null);
+            fail("expected a null timezone to throw an exception.");
+        } catch (NullPointerException npe) {
+            // expected.
+        }
+    }
+
+    @SmallTest
+    public void testTimezone() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        assertEquals(Time.TIMEZONE_UTC, t.getTimezone());
+    }
+
+    @SmallTest
+    public void testSwitchTimezone() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        String newTimezone = "America/Los_Angeles";
+        t.switchTimezone(newTimezone);
+        assertEquals(newTimezone, t.getTimezone());
+    }
+
+    @SmallTest
+    public void testGetActualMaximum() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(1, 0, 2020);
+        assertEquals(59, t.getActualMaximum(Time.SECOND));
+        assertEquals(59, t.getActualMaximum(Time.MINUTE));
+        assertEquals(23, t.getActualMaximum(Time.HOUR));
+        assertEquals(31, t.getActualMaximum(Time.MONTH_DAY));
+        assertEquals(11, t.getActualMaximum(Time.MONTH));
+        assertEquals(7, t.getActualMaximum(Time.WEEK_DAY));
+        assertEquals(366, t.getActualMaximum(Time.YEAR_DAY)); // 2020 is a leap year
+        t.set(1, 0, 2019);
+        assertEquals(365, t.getActualMaximum(Time.YEAR_DAY));
+    }
+
+    @SmallTest
+    public void testAdd() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(0, 0, 0, 1, 0, 2020);
+        t.add(Time.SECOND, 1);
+        assertEquals(1, t.getSecond());
+        t.add(Time.MINUTE, 1);
+        assertEquals(1, t.getMinute());
+        t.add(Time.HOUR, 1);
+        assertEquals(1, t.getHour());
+        t.add(Time.MONTH_DAY, 1);
+        assertEquals(2, t.getDay());
+        t.add(Time.MONTH, 1);
+        assertEquals(1, t.getMonth());
+        t.add(Time.YEAR, 1);
+        assertEquals(2021, t.getYear());
+    }
+
+    @SmallTest
+    public void testClear() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.clear(Time.TIMEZONE_UTC);
+
+        assertEquals(Time.TIMEZONE_UTC, t.getTimezone());
+        assertFalse(t.isAllDay());
+        assertEquals(0, t.getSecond());
+        assertEquals(0, t.getMinute());
+        assertEquals(0, t.getHour());
+        assertEquals(1, t.getDay()); // default for Calendar is 1
+        assertEquals(0, t.getMonth());
+        assertEquals(1970, t.getYear());
+        assertEquals(4, t.getWeekDay()); // 1970 Jan 1 --> Thursday
+        assertEquals(0, t.getYearDay());
+        assertEquals(0, t.getGmtOffset());
+    }
+
+    @SmallTest
+    public void testCompare() {
+        Time a = new Time(Time.TIMEZONE_UTC);
+        Time b = new Time("America/Los_Angeles");
+        assertTrue(a.compareTo(b) < 0);
+
+        Time c = new Time("Asia/Calcutta");
+        assertTrue(a.compareTo(c) > 0);
+
+        Time d = new Time(Time.TIMEZONE_UTC);
+        assertEquals(0, a.compareTo(d));
+    }
+
+    @SmallTest
+    public void testFormat2445() {
+        Time t = new Time();
+        assertEquals("19700101T000000", t.format2445());
+        t.setTimezone(Time.TIMEZONE_UTC);
+        assertEquals("19700101T000000Z", t.format2445());
+        t.setAllDay(true);
+        assertEquals("19700101", t.format2445());
+    }
+
+    @SmallTest
+    public void testFormat3339() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        assertEquals("1970-01-01", t.format3339(true));
+        t.set(29, 1, 2020);
+        assertEquals("2020-02-29", t.format3339(true));
+    }
+
+    @SmallTest
+    public void testToMillis() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(1, 0, 0, 1, 0, 1970);
+        assertEquals(1000L, t.toMillis());
+
+        t.set(0, 0, 0, 1, 1, 2020);
+        assertEquals(1580515200000L, t.toMillis());
+        t.set(1, 0, 0, 1, 1, 2020);
+        assertEquals(1580515201000L, t.toMillis());
+
+        t.set(1, 0, 2020);
+        assertEquals(1577836800000L, t.toMillis());
+        t.set(1, 1, 2020);
+        assertEquals(1580515200000L, t.toMillis());
+    }
+
+    @SmallTest
+    public void testToMillis_overflow() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(32, 0, 2020);
+        assertEquals(1580515200000L, t.toMillis());
+        assertEquals(1, t.getDay());
+        assertEquals(1, t.getMonth());
+    }
+
+    @SmallTest
+    public void testParse() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.parse("20201010T160000Z");
+        assertEquals(2020, t.getYear());
+        assertEquals(9, t.getMonth());
+        assertEquals(10, t.getDay());
+        assertEquals(16, t.getHour());
+        assertEquals(0, t.getMinute());
+        assertEquals(0, t.getSecond());
+
+        t.parse("20200220");
+        assertEquals(2020, t.getYear());
+        assertEquals(1, t.getMonth());
+        assertEquals(20, t.getDay());
+        assertEquals(0, t.getHour());
+        assertEquals(0, t.getMinute());
+        assertEquals(0, t.getSecond());
+
+        try {
+            t.parse("invalid");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        try {
+            t.parse("20201010Z160000");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    @SmallTest
+    public void testParse3339() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+
+        t.parse3339("1980-05-23");
+        if (!t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23) {
+            fail("Did not parse all-day date correctly");
+        }
+
+        t.parse3339("1980-05-23T09:50:50");
+        if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23
+                || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50
+                || t.getGmtOffset() != 0) {
+            fail("Did not parse timezone-offset-less date correctly");
+        }
+
+        t.parse3339("1980-05-23T09:50:50Z");
+        if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23
+                || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50
+                || t.getGmtOffset() != 0) {
+            fail("Did not parse UTC date correctly");
+        }
+
+        t.parse3339("1980-05-23T09:50:50.0Z");
+        if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23
+                || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50
+                || t.getGmtOffset() != 0) {
+            fail("Did not parse UTC date correctly");
+        }
+
+        t.parse3339("1980-05-23T09:50:50.12Z");
+        if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23
+                || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50
+                || t.getGmtOffset() != 0) {
+            fail("Did not parse UTC date correctly");
+        }
+
+        t.parse3339("1980-05-23T09:50:50.123Z");
+        if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23
+                || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50
+                || t.getGmtOffset() != 0) {
+            fail("Did not parse UTC date correctly");
+        }
+
+        // the time should be normalized to UTC
+        t.parse3339("1980-05-23T09:50:50-01:05");
+        if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23
+                || t.getHour() != 10 || t.getMinute() != 55 || t.getSecond() != 50
+                || t.getGmtOffset() != 0) {
+            fail("Did not parse timezone-offset date correctly");
+        }
+
+        // the time should be normalized to UTC
+        t.parse3339("1980-05-23T09:50:50.123-01:05");
+        if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23
+                || t.getHour() != 10 || t.getMinute() != 55 || t.getSecond() != 50
+                || t.getGmtOffset() != 0) {
+            fail("Did not parse timezone-offset date correctly");
+        }
+
+        try {
+            t.parse3339("1980");
+            fail("Did not throw error on truncated input length");
+        } catch (TimeFormatException e) {
+            // successful
+        }
+
+        try {
+            t.parse3339("1980-05-23T09:50:50.123+");
+            fail("Did not throw error on truncated timezone offset");
+        } catch (TimeFormatException e1) {
+            // successful
+        }
+
+        try {
+            t.parse3339("1980-05-23T09:50:50.123+05:0");
+            fail("Did not throw error on truncated timezone offset");
+        } catch (TimeFormatException e1) {
+            // successful
+        }
+    }
+
+    @SmallTest
+    public void testSet_millis() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+
+        t.set(1000L);
+        assertEquals(1970, t.getYear());
+        assertEquals(1, t.getSecond());
+
+        t.set(2000L);
+        assertEquals(2, t.getSecond());
+        assertEquals(0, t.getMinute());
+
+        t.set(1000L * 60);
+        assertEquals(1, t.getMinute());
+        assertEquals(0, t.getHour());
+
+        t.set(1000L * 60 * 60);
+        assertEquals(1, t.getHour());
+        assertEquals(1, t.getDay());
+
+        t.set((1000L * 60 * 60 * 24) + 1000L);
+        assertEquals(2, t.getDay());
+        assertEquals(1970, t.getYear());
+    }
+
+    @SmallTest
+    public void testSet_dayMonthYear() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(1, 2, 2021);
+        assertEquals(1, t.getDay());
+        assertEquals(2, t.getMonth());
+        assertEquals(2021, t.getYear());
+    }
+
+    @SmallTest
+    public void testSet_secondMinuteHour() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(1, 2, 3, 4, 5, 2021);
+        assertEquals(1, t.getSecond());
+        assertEquals(2, t.getMinute());
+        assertEquals(3, t.getHour());
+        assertEquals(4, t.getDay());
+        assertEquals(5, t.getMonth());
+        assertEquals(2021, t.getYear());
+    }
+
+    @SmallTest
+    public void testSet_overflow() {
+        // Jan 32nd --> Feb 1st
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(32, 0, 2020);
+        assertEquals(1, t.getDay());
+        assertEquals(1, t.getMonth());
+        assertEquals(2020, t.getYear());
+
+        t = new Time(Time.TIMEZONE_UTC);
+        t.set(5, 10, 15, 32, 0, 2020);
+        assertEquals(5, t.getSecond());
+        assertEquals(10, t.getMinute());
+        assertEquals(15, t.getHour());
+        assertEquals(1, t.getDay());
+        assertEquals(1, t.getMonth());
+        assertEquals(2020, t.getYear());
+    }
+
+    @SmallTest
+    public void testSet_other() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(1, 2, 3, 4, 5, 2021);
+        Time t2 = new Time();
+        t2.set(t);
+        assertEquals(Time.TIMEZONE_UTC, t2.getTimezone());
+        assertEquals(1, t2.getSecond());
+        assertEquals(2, t2.getMinute());
+        assertEquals(3, t2.getHour());
+        assertEquals(4, t2.getDay());
+        assertEquals(5, t2.getMonth());
+        assertEquals(2021, t2.getYear());
+    }
+
+    @SmallTest
+    public void testSetToNow() {
+        long now = System.currentTimeMillis();
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(now);
+        long ms = t.toMillis();
+        // ensure time is within 1 second because of rounding errors
+        assertTrue("now: " + now + "; actual: " + ms, Math.abs(ms - now) < 1000);
+    }
+
+    @SmallTest
+    public void testGetWeekNumber() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(1000L);
+        assertEquals(1, t.getWeekNumber());
+        t.set(1, 1, 2020);
+        assertEquals(5, t.getWeekNumber());
+
+        // ensure ISO 8601 standards are met: weeks start on Monday and the first week has at least
+        // 4 days in it (the year's first Thursday or Jan 4th)
+        for (int i = 1; i <= 8; i++) {
+            t.set(i, 0, 2020);
+            // Jan 6th is the first Monday in 2020 so that would be week 2
+            assertEquals(i < 6 ? 1 : 2, t.getWeekNumber());
+        }
+    }
+
+    private static class DateTest {
+        public int year1;
+        public int month1;
+        public int day1;
+        public int hour1;
+        public int minute1;
+
+        public int offset;
+
+        public int year2;
+        public int month2;
+        public int day2;
+        public int hour2;
+        public int minute2;
+
+        public DateTest(int year1, int month1, int day1, int hour1, int minute1,
+                int offset, int year2, int month2, int day2, int hour2, int minute2) {
+            this.year1 = year1;
+            this.month1 = month1;
+            this.day1 = day1;
+            this.hour1 = hour1;
+            this.minute1 = minute1;
+            this.offset = offset;
+            this.year2 = year2;
+            this.month2 = month2;
+            this.day2 = day2;
+            this.hour2 = hour2;
+            this.minute2 = minute2;
+        }
+
+        public boolean equals(Time time) {
+            return time.getYear() == year2 && time.getMonth() == month2 && time.getDay() == day2
+                    && time.getHour() == hour2 && time.getMinute() == minute2;
+        }
+    }
+
+    @SmallTest
+    public void testNormalize() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.parse("20060432T010203");
+        assertEquals(1146531723000L, t.normalize());
+    }
+
+    /* These tests assume that DST changes on Nov 4, 2007 at 2am (to 1am). */
+
+    // The "offset" field in "dayTests" represents days.
+    // Note: the month numbers are 0-relative, so Jan=0, Feb=1,...Dec=11
+    private DateTest[] dayTests = {
+            // Nov 4, 12am + 0 day = Nov 4, 12am
+            new DateTest(2007, 10, 4, 0, 0, 0, 2007, 10, 4, 0, 0),
+            // Nov 5, 12am + 0 day = Nov 5, 12am
+            new DateTest(2007, 10, 5, 0, 0, 0, 2007, 10, 5, 0, 0),
+            // Nov 3, 12am + 1 day = Nov 4, 12am
+            new DateTest(2007, 10, 3, 0, 0, 1, 2007, 10, 4, 0, 0),
+            // Nov 4, 12am + 1 day = Nov 5, 12am
+            new DateTest(2007, 10, 4, 0, 0, 1, 2007, 10, 5, 0, 0),
+            // Nov 5, 12am + 1 day = Nov 6, 12am
+            new DateTest(2007, 10, 5, 0, 0, 1, 2007, 10, 6, 0, 0),
+            // Nov 3, 1am + 1 day = Nov 4, 1am
+            new DateTest(2007, 10, 3, 1, 0, 1, 2007, 10, 4, 1, 0),
+            // Nov 4, 1am + 1 day = Nov 5, 1am
+            new DateTest(2007, 10, 4, 1, 0, 1, 2007, 10, 5, 1, 0),
+            // Nov 5, 1am + 1 day = Nov 6, 1am
+            new DateTest(2007, 10, 5, 1, 0, 1, 2007, 10, 6, 1, 0),
+            // Nov 3, 2am + 1 day = Nov 4, 2am
+            new DateTest(2007, 10, 3, 2, 0, 1, 2007, 10, 4, 2, 0),
+            // Nov 4, 2am + 1 day = Nov 5, 2am
+            new DateTest(2007, 10, 4, 2, 0, 1, 2007, 10, 5, 2, 0),
+            // Nov 5, 2am + 1 day = Nov 6, 2am
+            new DateTest(2007, 10, 5, 2, 0, 1, 2007, 10, 6, 2, 0),
+    };
+
+    // The "offset" field in "minuteTests" represents minutes.
+    // Note: the month numbers are 0-relative, so Jan=0, Feb=1,...Dec=11
+    private DateTest[] minuteTests = {
+            // Nov 4, 12am + 0 minutes = Nov 4, 12am
+            new DateTest(2007, 10, 4, 0, 0, 0, 2007, 10, 4, 0, 0),
+            // Nov 4, 12am + 60 minutes = Nov 4, 1am
+            new DateTest(2007, 10, 4, 0, 0, 60, 2007, 10, 4, 1, 0),
+            // Nov 5, 12am + 0 minutes = Nov 5, 12am
+            new DateTest(2007, 10, 5, 0, 0, 0, 2007, 10, 5, 0, 0),
+            // Nov 3, 12am + 60 minutes = Nov 3, 1am
+            new DateTest(2007, 10, 3, 0, 0, 60, 2007, 10, 3, 1, 0),
+            // Nov 4, 12am + 60 minutes = Nov 4, 1am
+            new DateTest(2007, 10, 4, 0, 0, 60, 2007, 10, 4, 1, 0),
+            // Nov 5, 12am + 60 minutes = Nov 5, 1am
+            new DateTest(2007, 10, 5, 0, 0, 60, 2007, 10, 5, 1, 0),
+            // Nov 3, 1am + 60 minutes = Nov 3, 2am
+            new DateTest(2007, 10, 3, 1, 0, 60, 2007, 10, 3, 2, 0),
+            // Nov 4, 12:59am (PDT) + 2 minutes = Nov 4, 1:01am (PDT)
+            new DateTest(2007, 10, 4, 0, 59, 2, 2007, 10, 4, 1, 1),
+            // Nov 4, 12:59am (PDT) + 62 minutes = Nov 4, 1:01am (PST)
+            new DateTest(2007, 10, 4, 0, 59, 62, 2007, 10, 4, 1, 1),
+            // Nov 4, 12:30am (PDT) + 120 minutes = Nov 4, 1:30am (PST)
+            new DateTest(2007, 10, 4, 0, 30, 120, 2007, 10, 4, 1, 30),
+            // Nov 4, 12:30am (PDT) + 90 minutes = Nov 4, 1:00am (PST)
+            new DateTest(2007, 10, 4, 0, 30, 90, 2007, 10, 4, 1, 0),
+            // Nov 4, 1am (PDT) + 30 minutes = Nov 4, 1:30am (PDT)
+            new DateTest(2007, 10, 4, 1, 0, 30, 2007, 10, 4, 1, 30),
+            // Nov 4, 1:30am (PDT) + 15 minutes = Nov 4, 1:45am (PDT)
+            new DateTest(2007, 10, 4, 1, 30, 15, 2007, 10, 4, 1, 45),
+            // Mar 11, 1:30am (PST) + 30 minutes = Mar 11, 3am (PDT)
+            new DateTest(2007, 2, 11, 1, 30, 30, 2007, 2, 11, 3, 0),
+            // Nov 4, 1:30am (PST) + 15 minutes = Nov 4, 1:45am (PST)
+            new DateTest(2007, 10, 4, 1, 30, 15, 2007, 10, 4, 1, 45),
+            // Nov 4, 1:30am (PST) + 30 minutes = Nov 4, 2:00am (PST)
+            new DateTest(2007, 10, 4, 1, 30, 30, 2007, 10, 4, 2, 0),
+            // Nov 5, 1am + 60 minutes = Nov 5, 2am
+            new DateTest(2007, 10, 5, 1, 0, 60, 2007, 10, 5, 2, 0),
+            // Nov 3, 2am + 60 minutes = Nov 3, 3am
+            new DateTest(2007, 10, 3, 2, 0, 60, 2007, 10, 3, 3, 0),
+            // Nov 4, 2am + 30 minutes = Nov 4, 2:30am
+            new DateTest(2007, 10, 4, 2, 0, 30, 2007, 10, 4, 2, 30),
+            // Nov 4, 2am + 60 minutes = Nov 4, 3am
+            new DateTest(2007, 10, 4, 2, 0, 60, 2007, 10, 4, 3, 0),
+            // Nov 5, 2am + 60 minutes = Nov 5, 3am
+            new DateTest(2007, 10, 5, 2, 0, 60, 2007, 10, 5, 3, 0),
+            // NOTE: Calendar assumes 1am PDT == 1am PST, the two are not distinct, hence why the transition boundary itself has no tests
+    };
+
+    @MediumTest
+    public void testNormalize_dst() {
+        Time local = new Time("America/Los_Angeles");
+
+        int len = dayTests.length;
+        for (int index = 0; index < len; index++) {
+            DateTest test = dayTests[index];
+            local.set(0, test.minute1, test.hour1, test.day1, test.month1, test.year1);
+            local.add(Time.MONTH_DAY, test.offset);
+            if (!test.equals(local)) {
+                String expectedTime = String.format("%d-%02d-%02d %02d:%02d",
+                        test.year2, test.month2, test.day2, test.hour2, test.minute2);
+                String actualTime = String.format("%d-%02d-%02d %02d:%02d",
+                        local.getYear(), local.getMonth(), local.getDay(), local.getHour(),
+                        local.getMinute());
+                fail("Expected: " + expectedTime + "; Actual: " + actualTime);
+            }
+
+            local.set(0, test.minute1, test.hour1, test.day1, test.month1, test.year1);
+            local.add(Time.MONTH_DAY, test.offset);
+            if (!test.equals(local)) {
+                String expectedTime = String.format("%d-%02d-%02d %02d:%02d",
+                        test.year2, test.month2, test.day2, test.hour2, test.minute2);
+                String actualTime = String.format("%d-%02d-%02d %02d:%02d",
+                        local.getYear(), local.getMonth(), local.getDay(), local.getHour(),
+                        local.getMinute());
+                fail("Expected: " + expectedTime + "; Actual: " + actualTime);
+            }
+        }
+
+        len = minuteTests.length;
+        for (int index = 0; index < len; index++) {
+            DateTest test = minuteTests[index];
+            local.set(0, test.minute1, test.hour1, test.day1, test.month1, test.year1);
+            local.add(Time.MINUTE, test.offset);
+            if (!test.equals(local)) {
+                String expectedTime = String.format("%d-%02d-%02d %02d:%02d",
+                        test.year2, test.month2, test.day2, test.hour2, test.minute2);
+                String actualTime = String.format("%d-%02d-%02d %02d:%02d",
+                        local.getYear(), local.getMonth(), local.getDay(), local.getHour(),
+                        local.getMinute());
+                fail("Expected: " + expectedTime + "; Actual: " + actualTime);
+            }
+
+            local.set(0, test.minute1, test.hour1, test.day1, test.month1, test.year1);
+            local.add(Time.MINUTE, test.offset);
+            if (!test.equals(local)) {
+                String expectedTime = String.format("%d-%02d-%02d %02d:%02d",
+                        test.year2, test.month2, test.day2, test.hour2, test.minute2);
+                String actualTime = String.format("%d-%02d-%02d %02d:%02d",
+                        local.getYear(), local.getMonth(), local.getDay(), local.getHour(),
+                        local.getMinute());
+                fail("Expected: " + expectedTime + "; Actual: " + actualTime);
+            }
+        }
+    }
+
+    @SmallTest
+    public void testNormalize_overflow() {
+        Time t = new Time(Time.TIMEZONE_UTC);
+        t.set(32, 0, 2020);
+        t.normalize();
+        assertEquals(1, t.getDay());
+        assertEquals(1, t.getMonth());
+    }
+
+    @SmallTest
+    public void testDstBehavior_addDays_ignoreDst() {
+        Time time = new Time("America/Los_Angeles");
+        time.set(4, 10, 2007);  // set to Nov 4, 2007, 12am
+        assertEquals(1194159600000L, time.normalize());
+        time.add(Time.MONTH_DAY, 1); // changes to Nov 5, 2007, 12am
+        assertEquals(1194249600000L, time.toMillis());
+
+        time = new Time("America/Los_Angeles");
+        time.set(11, 2, 2007);  // set to Mar 11, 2007, 12am
+        assertEquals(1173600000000L, time.normalize());
+        time.add(Time.MONTH_DAY, 1); // changes to Mar 12, 2007, 12am
+        assertEquals(1173682800000L, time.toMillis());
+    }
+
+    @SmallTest
+    public void testDstBehavior_addDays_applyDst() {
+        if (!Time.APPLY_DST_CHANGE_LOGIC) {
+            return;
+        }
+        Time time = new Time("America/Los_Angeles");
+        time.set(4, 10, 2007);  // set to Nov 4, 2007, 12am
+        assertEquals(1194159600000L, time.normalizeApplyDst());
+        time.add(Time.MONTH_DAY, 1); // changes to Nov 4, 2007, 11pm (fall back)
+        assertEquals(1194246000000L, time.toMillisApplyDst());
+
+        time = new Time("America/Los_Angeles");
+        time.set(11, 2, 2007);  // set to Mar 11, 2007, 12am
+        assertEquals(1173600000000L, time.normalizeApplyDst());
+        time.add(Time.MONTH_DAY, 1); // changes to Mar 12, 2007, 1am (roll forward)
+        assertEquals(1173686400000L, time.toMillisApplyDst());
+    }
+
+    @SmallTest
+    public void testDstBehavior_addHours_ignoreDst() {
+        // Note: by default, Calendar applies DST logic if adding hours or minutes but not if adding
+        // days, hence in this test, only if the APPLY_DST_CHANGE_LOGIC flag is false, then the time
+        // is adjusted with DST
+        Time time = new Time("America/Los_Angeles");
+        time.set(4, 10, 2007);  // set to Nov 4, 2007, 12am
+        assertEquals(1194159600000L, time.normalize());
+        time.add(Time.HOUR, 24); // changes to Nov 5, 2007, 12am
+        assertEquals(Time.APPLY_DST_CHANGE_LOGIC ? 1194249600000L : 1194246000000L,
+                        time.toMillis());
+
+        time = new Time("America/Los_Angeles");
+        time.set(11, 2, 2007);  // set to Mar 11, 2007, 12am
+        assertEquals(1173600000000L, time.normalize());
+        time.add(Time.HOUR, 24); // changes to Mar 12, 2007, 12am
+        assertEquals(Time.APPLY_DST_CHANGE_LOGIC ? 1173682800000L : 1173686400000L,
+                        time.toMillis());
+    }
+
+    @SmallTest
+    public void testDstBehavior_addHours_applyDst() {
+        if (!Time.APPLY_DST_CHANGE_LOGIC) {
+            return;
+        }
+        Time time = new Time("America/Los_Angeles");
+        time.set(4, 10, 2007);  // set to Nov 4, 2007, 12am
+        assertEquals(1194159600000L, time.normalizeApplyDst());
+        time.add(Time.HOUR, 24); // changes to Nov 4, 2007, 11pm (fall back)
+        assertEquals(1194246000000L, time.toMillisApplyDst());
+
+        time = new Time("America/Los_Angeles");
+        time.set(11, 2, 2007);  // set to Mar 11, 2007, 12am
+        assertEquals(1173600000000L, time.normalizeApplyDst());
+        time.add(Time.HOUR, 24); // changes to Mar 12, 2007, 1am (roll forward)
+        assertEquals(1173686400000L, time.toMillisApplyDst());
+    }
+
+    // Timezones that cover the world.
+    // Some GMT offsets occur more than once in case some cities decide to change their GMT offset.
+    private static final String[] mTimeZones = {
+            "Pacific/Kiritimati",
+            "Pacific/Enderbury",
+            "Pacific/Fiji",
+            "Antarctica/South_Pole",
+            "Pacific/Norfolk",
+            "Pacific/Ponape",
+            "Asia/Magadan",
+            "Australia/Lord_Howe",
+            "Australia/Sydney",
+            "Australia/Adelaide",
+            "Asia/Tokyo",
+            "Asia/Seoul",
+            "Asia/Taipei",
+            "Asia/Singapore",
+            "Asia/Hong_Kong",
+            "Asia/Saigon",
+            "Asia/Bangkok",
+            "Indian/Cocos",
+            "Asia/Rangoon",
+            "Asia/Omsk",
+            "Antarctica/Mawson",
+            "Asia/Colombo",
+            "Asia/Calcutta",
+            "Asia/Oral",
+            "Asia/Kabul",
+            "Asia/Dubai",
+            "Asia/Tehran",
+            "Europe/Moscow",
+            "Asia/Baghdad",
+            "Africa/Mogadishu",
+            "Europe/Athens",
+            "Africa/Cairo",
+            "Europe/Rome",
+            "Europe/Berlin",
+            "Europe/Amsterdam",
+            "Africa/Tunis",
+            "Europe/London",
+            "Europe/Dublin",
+            "Atlantic/St_Helena",
+            "Africa/Monrovia",
+            "Africa/Accra",
+            "Atlantic/Azores",
+            "Atlantic/South_Georgia",
+            "America/Noronha",
+            "America/Sao_Paulo",
+            "America/Cayenne",
+            "America/St_Johns",
+            "America/Puerto_Rico",
+            "America/Aruba",
+            "America/New_York",
+            "America/Chicago",
+            "America/Denver",
+            "America/Los_Angeles",
+            "America/Anchorage",
+            "Pacific/Marquesas",
+            "America/Adak",
+            "Pacific/Honolulu",
+            "Pacific/Midway",
+    };
+
+    @MediumTest
+    public void testGetJulianDay() {
+        Time time = new Time(Time.TIMEZONE_UTC);
+
+        // for 30 random days in the year 2020 and for a random timezone, get the Julian day for
+        // 12am and then check that if we change the time we get the same Julian day.
+        for (int i = 0; i < 30; i++) {
+            int monthDay = (int) (Math.random() * 365) + 1;
+            int zoneIndex = (int) (Math.random() * mTimeZones.length);
+            time.setTimezone(mTimeZones[zoneIndex]);
+            time.set(0, 0, 0, monthDay, 0, 2020);
+
+            int julianDay = Time.getJulianDay(time.normalize(), time.getGmtOffset());
+
+            // change the time during the day and check that we get the same Julian day.
+            for (int hour = 0; hour < 24; hour++) {
+                for (int minute = 0; minute < 60; minute += 15) {
+                    time.set(0, minute, hour, monthDay, 0, 2020);
+                    int day = Time.getJulianDay(time.normalize(), time.getGmtOffset());
+                    assertEquals(day, julianDay);
+                    time.clear(Time.TIMEZONE_UTC);
+                }
+            }
+        }
+    }
+
+    @MediumTest
+    public void testSetJulianDay() {
+        Time time = new Time(Time.TIMEZONE_UTC);
+
+        // for each day in the year 2020, pick a random timezone, and verify that we can
+        // set the Julian day correctly.
+        for (int monthDay = 1; monthDay <= 366; monthDay++) {
+            int zoneIndex = (int) (Math.random() * mTimeZones.length);
+            // leave the "month" as zero because we are changing the "monthDay" from 1 to 366.
+            // the call to normalize() will then change the "month" (but we don't really care).
+            time.set(0, 0, 0, monthDay, 0, 2020);
+            time.setTimezone(mTimeZones[zoneIndex]);
+            long millis = time.normalize();
+            int julianDay = Time.getJulianDay(millis, time.getGmtOffset());
+
+            time.setJulianDay(julianDay);
+
+            // some places change daylight saving time at 12am and so there is no 12am on some days
+            // in some timezones - in those cases, the time is set to 1am.
+            // some examples: Africa/Cairo, America/Sao_Paulo, Atlantic/Azores
+            assertTrue(time.getHour() == 0 || time.getHour() == 1);
+            assertEquals(0, time.getMinute());
+            assertEquals(0, time.getSecond());
+
+            millis = time.toMillis();
+            int day = Time.getJulianDay(millis, time.getGmtOffset());
+            assertEquals(day, julianDay);
+            time.clear(Time.TIMEZONE_UTC);
+        }
+    }
+}