diff --git a/core/java/android/service/notification/ScheduleCalendar.java b/core/java/android/service/notification/ScheduleCalendar.java index 314c97db4e7b0..1e5ff3a536cee 100644 --- a/core/java/android/service/notification/ScheduleCalendar.java +++ b/core/java/android/service/notification/ScheduleCalendar.java @@ -20,6 +20,8 @@ import android.service.notification.ZenModeConfig.ScheduleInfo; import android.util.ArraySet; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + import java.util.Calendar; import java.util.Objects; import java.util.TimeZone; @@ -92,16 +94,22 @@ public class ScheduleCalendar { */ public long getNextChangeTime(long now) { if (mSchedule == null) return 0; - final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute); - final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute); + final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute, true); + final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute, false); long nextScheduleTime = Math.min(nextStart, nextEnd); return nextScheduleTime; } - private long getNextTime(long now, int hr, int min) { - final long time = getTime(now, hr, min); - return time <= now ? addDays(time, 1) : time; + private long getNextTime(long now, int hr, int min, boolean adjust) { + // The adjust parameter indicates whether to potentially adjust the time to the closest + // actual time if the indicated time is one skipped due to daylight time. + final long time = adjust ? getClosestActualTime(now, hr, min) : getTime(now, hr, min); + if (time <= now) { + final long tomorrow = addDays(time, 1); + return adjust ? getClosestActualTime(tomorrow, hr, min) : getTime(tomorrow, hr, min); + } + return time; } private long getTime(long millis, int hour, int min) { @@ -119,7 +127,7 @@ public class ScheduleCalendar { */ public boolean isInSchedule(long time) { if (mSchedule == null || mDays.size() == 0) return false; - final long start = getTime(time, mSchedule.startHour, mSchedule.startMinute); + final long start = getClosestActualTime(time, mSchedule.startHour, mSchedule.startMinute); long end = getTime(time, mSchedule.endHour, mSchedule.endMinute); if (end <= start) { end = addDays(end, 1); @@ -134,7 +142,7 @@ public class ScheduleCalendar { */ public boolean isAlarmInSchedule(long alarm, long now) { if (mSchedule == null || mDays.size() == 0) return false; - final long start = getTime(alarm, mSchedule.startHour, mSchedule.startMinute); + final long start = getClosestActualTime(alarm, mSchedule.startHour, mSchedule.startMinute); long end = getTime(alarm, mSchedule.endHour, mSchedule.endMinute); if (end <= start) { end = addDays(end, 1); @@ -186,4 +194,41 @@ public class ScheduleCalendar { mCalendar.add(Calendar.DATE, days); return mCalendar.getTimeInMillis(); } + + /** + * This function returns the closest "actual" time to the provided hour/minute relative to the + * reference time. For most times this will behave exactly the same as getTime, but for any time + * during the hour skipped forward for daylight savings time (for instance, 02:xx when the + * clock is set to 03:00 after 01:59), this method will return the time when the clock changes + * (in this example, 03:00). + * + * Assumptions made in this implementation: + * - Time is moved forward on an hour boundary (minute 0) by exactly 1hr when clocks shift + * - a lenient Calendar implementation will interpret 02:xx on a day when 2-3AM is skipped + * as 03:xx + * - The skipped hour is never 11PM / 23:00. + * + * @hide + */ + @VisibleForTesting + public long getClosestActualTime(long refTime, int hour, int min) { + long resTime = getTime(refTime, hour, min); + if (!mCalendar.getTimeZone().observesDaylightTime()) { + // Do nothing if the timezone doesn't observe daylight time at all. + return resTime; + } + + // Approach to identifying whether the time is "skipped": get the result from starting with + // refTime and setting hour and minute, then re-extract the hour and minute of the resulting + // moment in time. If the hour is exactly one more than the passed-in hour and the minute is + // the same, then the provided hour is likely a skipped one. If the time doesn't fall into + // this category, return the unmodified time instead. + mCalendar.setTimeInMillis(resTime); + int resHr = mCalendar.get(Calendar.HOUR_OF_DAY); + int resMin = mCalendar.get(Calendar.MINUTE); + if (resHr == hour + 1 && resMin == min) { + return getTime(refTime, resHr, 0); + } + return resTime; + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java index 11162043bb27f..9ad007d6a840b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java @@ -36,6 +36,7 @@ import org.junit.runner.RunWith; import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.TimeZone; @SmallTest @RunWith(AndroidJUnit4.class) @@ -170,6 +171,119 @@ public class ScheduleCalendarTest extends UiServiceTestCase { actualMs); } + @Test + public void testGetNextChangeTime_startTomorrowInDaylight() { + // Test that the correct thing happens when the next start time would be tomorrow, during + // a schedule start time that doesn't exist that day. Consistent with "start times" as + // implemented in isInSchedule, this should get adjusted to the closest actual time. + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); + + // "today" = the day before the skipped hour for daylight savings. + Calendar today = getDaylightSavingsForwardDay(); + today.set(Calendar.HOUR_OF_DAY, 23); + today.set(Calendar.MINUTE, 15); + Calendar tomorrow = getDaylightSavingsForwardDay(); + tomorrow.add(Calendar.DATE, 1); + mScheduleInfo.days = new int[] {today.get(Calendar.DAY_OF_WEEK), + tomorrow.get(Calendar.DAY_OF_WEEK)}; + mScheduleInfo.startHour = 2; + mScheduleInfo.endHour = 4; + mScheduleInfo.startMinute = 15; + mScheduleInfo.endMinute = 15; + mScheduleInfo.exitAtAlarm = false; + mScheduleCalendar.setSchedule(mScheduleInfo); + + // The expected next change time should be tomorrow, 3AM as 2:15AM doesn't exist. + Calendar expected = new GregorianCalendar(TimeZone.getTimeZone("America/New_York")); + expected.setTimeInMillis(tomorrow.getTimeInMillis()); + expected.set(Calendar.HOUR_OF_DAY, 3); + expected.set(Calendar.MINUTE, 0); + expected.set(Calendar.SECOND, 0); + expected.set(Calendar.MILLISECOND, 0); + + long actualMs = mScheduleCalendar.getNextChangeTime(today.getTimeInMillis()); + GregorianCalendar actual = new GregorianCalendar(TimeZone.getTimeZone("America/New_York")); + actual.setTimeInMillis(actualMs); + assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(), + actualMs); + } + + @Test + public void testGetNextChangeTime_startTomorrowWhenTodayIsDaylight() { + // Test that the correct thing happens when the next start time would be tomorrow, but + // today is the day when daylight time switches over (so the "schedule start time" today + // may not exist). + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); + + // "today" = the day with the skipped hour for daylight savings. + Calendar today = getDaylightSavingsForwardDay(); + today.add(Calendar.DATE, 1); + today.set(Calendar.HOUR_OF_DAY, 23); + today.set(Calendar.MINUTE, 15); + Calendar tomorrow = getDaylightSavingsForwardDay(); + tomorrow.add(Calendar.DATE, 2); + mScheduleInfo.days = new int[] {today.get(Calendar.DAY_OF_WEEK), + tomorrow.get(Calendar.DAY_OF_WEEK)}; + mScheduleInfo.startHour = 2; + mScheduleInfo.endHour = 4; + mScheduleInfo.startMinute = 15; + mScheduleInfo.endMinute = 15; + mScheduleInfo.exitAtAlarm = false; + mScheduleCalendar.setSchedule(mScheduleInfo); + + // The expected next change time should be tomorrow, 2:15AM. + Calendar expected = new GregorianCalendar(TimeZone.getTimeZone("America/New_York")); + expected.setTimeInMillis(tomorrow.getTimeInMillis()); + expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.startHour); + expected.set(Calendar.MINUTE, mScheduleInfo.startMinute); + expected.set(Calendar.SECOND, 0); + expected.set(Calendar.MILLISECOND, 0); + + long actualMs = mScheduleCalendar.getNextChangeTime(today.getTimeInMillis()); + GregorianCalendar actual = new GregorianCalendar(TimeZone.getTimeZone("America/New_York")); + actual.setTimeInMillis(actualMs); + assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(), + actualMs); + } + + @Test + public void testGetNextChangeTime_startTomorrowWhenTodayIsDaylightBackward() { + // Test that the correct thing happens when the next start time would be tomorrow, but + // today is the day when clocks are adjusted backwards (so the "schedule start time" today + // exists twice). + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); + + // "today" = the day with the extra hour for daylight savings. + Calendar today = getDaylightSavingsBackwardDay(); + today.add(Calendar.DATE, 1); + today.set(Calendar.HOUR_OF_DAY, 23); + today.set(Calendar.MINUTE, 15); + Calendar tomorrow = getDaylightSavingsBackwardDay(); + tomorrow.add(Calendar.DATE, 2); + mScheduleInfo.days = new int[] {today.get(Calendar.DAY_OF_WEEK), + tomorrow.get(Calendar.DAY_OF_WEEK)}; + mScheduleInfo.startHour = 1; + mScheduleInfo.endHour = 4; + mScheduleInfo.startMinute = 15; + mScheduleInfo.endMinute = 15; + mScheduleInfo.exitAtAlarm = false; + mScheduleCalendar.setSchedule(mScheduleInfo); + + // The expected next change time should be tomorrow, 1:15AM. + Calendar expected = new GregorianCalendar(TimeZone.getTimeZone("America/New_York")); + expected.setTimeInMillis(tomorrow.getTimeInMillis()); + expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.startHour); + expected.set(Calendar.MINUTE, mScheduleInfo.startMinute); + expected.set(Calendar.SECOND, 0); + expected.set(Calendar.MILLISECOND, 0); + + long actualMs = mScheduleCalendar.getNextChangeTime(today.getTimeInMillis()); + GregorianCalendar actual = new GregorianCalendar(TimeZone.getTimeZone("America/New_York")); + actual.setTimeInMillis(actualMs); + assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(), + actualMs); + } + @Test public void testShouldExitForAlarm_settingOff() { mScheduleInfo.exitAtAlarm = false; @@ -415,23 +529,265 @@ public class ScheduleCalendarTest extends UiServiceTestCase { assertFalse(mScheduleCalendar.isInSchedule(cal.getTimeInMillis())); } + @Test + public void testIsInSchedule_daylightSavingsForward_startDuringChange() { + // Test that if the start time of a ScheduleCalendar is during the nonexistent + // hour of daylight savings forward time, the evaluation of whether a time is in the + // schedule still works. + + // Set timezone to make sure we're evaluating the correct days. + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); + + // Set up schedule for 2:30AM - 4:00AM. + final Calendar dstYesterday = getDaylightSavingsForwardDay(); + final Calendar dstToday = getDaylightSavingsForwardDay(); + dstToday.add(Calendar.DATE, 1); + mScheduleInfo.days = new int[] {dstYesterday.get(Calendar.DAY_OF_WEEK), + dstToday.get(Calendar.DAY_OF_WEEK)}; + mScheduleInfo.startHour = 2; + mScheduleInfo.startMinute = 30; + mScheduleInfo.endHour = 4; + mScheduleCalendar.setSchedule(mScheduleInfo); + + // Test cases: there are 2 "on" periods. These cover: before the first schedule + // (1AM previous day), during the first schedule (2:30AM), two between the two schedules + // (one on each calendar day), during the second (3:30AM), and after the second (4:30AM) + Calendar out1 = getDaylightSavingsForwardDay(); + out1.set(Calendar.HOUR_OF_DAY, 1); + out1.set(Calendar.MINUTE, 00); + out1.set(Calendar.SECOND, 0); + out1.set(Calendar.MILLISECOND, 0); + + Calendar in1 = getDaylightSavingsForwardDay(); + in1.set(Calendar.HOUR_OF_DAY, 2); + in1.set(Calendar.MINUTE, 45); + in1.set(Calendar.SECOND, 0); + in1.set(Calendar.MILLISECOND, 0); + + Calendar midOut1 = getDaylightSavingsForwardDay(); + midOut1.set(Calendar.HOUR_OF_DAY, 7); + midOut1.set(Calendar.MINUTE, 30); + midOut1.set(Calendar.SECOND, 0); + midOut1.set(Calendar.MILLISECOND, 0); + + Calendar midOut2 = getDaylightSavingsForwardDay(); + midOut2.add(Calendar.DATE, 1); + midOut2.set(Calendar.HOUR_OF_DAY, 1); + midOut2.set(Calendar.MINUTE, 30); + midOut2.set(Calendar.SECOND, 0); + midOut2.set(Calendar.MILLISECOND, 0); + + // Question: should 3:15AM be in the 2:30-4 schedule on a day when 2:30-3 doesn't exist? + Calendar in2 = getDaylightSavingsForwardDay(); + in2.add(Calendar.DATE, 1); + in2.set(Calendar.HOUR_OF_DAY, 3); + in2.set(Calendar.MINUTE, 30); + in2.set(Calendar.SECOND, 0); + in2.set(Calendar.MILLISECOND, 0); + + Calendar out2 = getDaylightSavingsForwardDay(); + out2.add(Calendar.DATE, 1); + out2.set(Calendar.HOUR_OF_DAY, 4); + out2.set(Calendar.MINUTE, 30); + out2.set(Calendar.SECOND, 0); + out2.set(Calendar.MILLISECOND, 0); + + assertFalse(mScheduleCalendar.isInSchedule(out1.getTimeInMillis())); + assertTrue(mScheduleCalendar.isInSchedule(in1.getTimeInMillis())); + assertFalse(mScheduleCalendar.isInSchedule(midOut1.getTimeInMillis())); + assertFalse(mScheduleCalendar.isInSchedule(midOut2.getTimeInMillis())); + assertTrue(mScheduleCalendar.isInSchedule(in2.getTimeInMillis())); + assertFalse(mScheduleCalendar.isInSchedule(out2.getTimeInMillis())); + } + + @Test + public void testIsInSchedule_daylightSavingsForward_endDuringChange() { + // Test that if the end time of a ScheduleCalendar is during the nonexistent + // hour of daylight savings forward time, the evaluation of whether a time is in the + // schedule still works. + + // Set timezone to make sure we're evaluating the correct days. + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); + + // Set up schedule for 11:00PM - 2:30AM. On the day when 2AM doesn't exist, this should + // effectively finish at 3:30AM(?) + final Calendar dstYesterday = getDaylightSavingsForwardDay(); + final Calendar dstToday = getDaylightSavingsForwardDay(); + dstToday.add(Calendar.DATE, 1); + mScheduleInfo.days = new int[] {dstYesterday.get(Calendar.DAY_OF_WEEK), + dstToday.get(Calendar.DAY_OF_WEEK)}; + mScheduleInfo.startHour = 23; + mScheduleInfo.endHour = 2; + mScheduleInfo.endMinute = 30; + mScheduleCalendar.setSchedule(mScheduleInfo); + + // Test cases: before the time period on the previous day; during the time period when + // the calendar day is still the previous day; during the time period when the calendar + // day is the change day; afterwards. + Calendar out1 = getDaylightSavingsForwardDay(); + out1.set(Calendar.HOUR_OF_DAY, 22); + out1.set(Calendar.MINUTE, 00); + out1.set(Calendar.SECOND, 0); + out1.set(Calendar.MILLISECOND, 0); + + Calendar in1 = getDaylightSavingsForwardDay(); + in1.set(Calendar.HOUR_OF_DAY, 23); + in1.set(Calendar.MINUTE, 30); + in1.set(Calendar.SECOND, 0); + in1.set(Calendar.MILLISECOND, 0); + + Calendar in2 = getDaylightSavingsForwardDay(); + in2.add(Calendar.DATE, 1); + in2.set(Calendar.HOUR_OF_DAY, 1); + in2.set(Calendar.MINUTE, 30); + in2.set(Calendar.SECOND, 0); + in2.set(Calendar.MILLISECOND, 0); + + // Question: Should 3:15AM be out of the schedule on a day when 2-3 doesn't exist? + Calendar out2 = getDaylightSavingsForwardDay(); + out2.add(Calendar.DATE, 1); + out2.set(Calendar.HOUR_OF_DAY, 3); + out2.set(Calendar.MINUTE, 45); + out2.set(Calendar.SECOND, 0); + out2.set(Calendar.MILLISECOND, 0); + + assertFalse(mScheduleCalendar.isInSchedule(out1.getTimeInMillis())); + assertTrue(mScheduleCalendar.isInSchedule(in1.getTimeInMillis())); + assertTrue(mScheduleCalendar.isInSchedule(in2.getTimeInMillis())); + assertFalse(mScheduleCalendar.isInSchedule(out2.getTimeInMillis())); + } + + @Test + public void testIsInSchedule_daylightSavingsBackward_startDuringChange() { + // Test that if the start time of a ScheduleCalendar is during the duplicated + // hour of daylight savings backward time, the evaluation of whether a time is in the + // schedule still works. It's not clear what correct behavior is during the duplicated + // 1:00->1:59->1:00->1:59 time period, but times outside that should still work. + + // Set timezone to make sure we're evaluating the correct days. + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); + + // Set up schedule for 1:15AM - 4:00AM. + final Calendar dstYesterday = getDaylightSavingsBackwardDay(); + final Calendar dstToday = getDaylightSavingsBackwardDay(); + dstToday.add(Calendar.DATE, 1); + mScheduleInfo.days = new int[] {dstYesterday.get(Calendar.DAY_OF_WEEK), + dstToday.get(Calendar.DAY_OF_WEEK)}; + mScheduleInfo.startHour = 1; + mScheduleInfo.startMinute = 15; + mScheduleInfo.endHour = 4; + mScheduleCalendar.setSchedule(mScheduleInfo); + + // Test cases: there are 2 "on" periods. These cover: before the first schedule + // (1AM previous day), during the first schedule (2:30AM), two between the two schedules + // (one on each calendar day), during the second (2:30AM), and after the second (4:30AM) + Calendar out1 = getDaylightSavingsBackwardDay(); + out1.set(Calendar.HOUR_OF_DAY, 1); + out1.set(Calendar.MINUTE, 00); + out1.set(Calendar.SECOND, 0); + out1.set(Calendar.MILLISECOND, 0); + + Calendar in1 = getDaylightSavingsBackwardDay(); + in1.set(Calendar.HOUR_OF_DAY, 2); + in1.set(Calendar.MINUTE, 30); + in1.set(Calendar.SECOND, 0); + in1.set(Calendar.MILLISECOND, 0); + + Calendar midOut1 = getDaylightSavingsBackwardDay(); + midOut1.set(Calendar.HOUR_OF_DAY, 7); + midOut1.set(Calendar.MINUTE, 30); + midOut1.set(Calendar.SECOND, 0); + midOut1.set(Calendar.MILLISECOND, 0); + + Calendar midOut2 = getDaylightSavingsBackwardDay(); + midOut2.add(Calendar.DATE, 1); + midOut2.set(Calendar.HOUR_OF_DAY, 0); + midOut2.set(Calendar.MINUTE, 30); + midOut2.set(Calendar.SECOND, 0); + midOut2.set(Calendar.MILLISECOND, 0); + + Calendar in2 = getDaylightSavingsBackwardDay(); + in2.add(Calendar.DATE, 1); + in2.set(Calendar.HOUR_OF_DAY, 2); + in2.set(Calendar.MINUTE, 30); + in2.set(Calendar.SECOND, 0); + in2.set(Calendar.MILLISECOND, 0); + + Calendar out2 = getDaylightSavingsBackwardDay(); + out2.add(Calendar.DATE, 1); + out2.set(Calendar.HOUR_OF_DAY, 4); + out2.set(Calendar.MINUTE, 30); + out2.set(Calendar.SECOND, 0); + out2.set(Calendar.MILLISECOND, 0); + + assertFalse(mScheduleCalendar.isInSchedule(out1.getTimeInMillis())); + assertTrue(mScheduleCalendar.isInSchedule(in1.getTimeInMillis())); + assertFalse(mScheduleCalendar.isInSchedule(midOut1.getTimeInMillis())); + assertFalse(mScheduleCalendar.isInSchedule(midOut2.getTimeInMillis())); + assertTrue(mScheduleCalendar.isInSchedule(in2.getTimeInMillis())); + assertFalse(mScheduleCalendar.isInSchedule(out2.getTimeInMillis())); + } + + @Test + public void testIsInSchedule_daylightSavings_flippedSchedule() { + // This test is for the unlikely edge case where the skipped hour due to daylight savings + // causes the evaluated start time to be "later" than the schedule's end time on that day, + // for instance if the schedule is 2:30AM-3:15AM; 2:30AM may evaluate to 3:30AM on the day + // of daylight change. + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); + + // Set up schedule for 2:30AM - 3:15AM. + final Calendar dstYesterday = getDaylightSavingsForwardDay(); + final Calendar dstToday = getDaylightSavingsForwardDay(); + dstToday.add(Calendar.DATE, 1); + mScheduleInfo.days = new int[] {dstYesterday.get(Calendar.DAY_OF_WEEK), + dstToday.get(Calendar.DAY_OF_WEEK)}; + mScheduleInfo.startHour = 2; + mScheduleInfo.startMinute = 30; + mScheduleInfo.endHour = 3; + mScheduleInfo.endMinute = 15; + mScheduleCalendar.setSchedule(mScheduleInfo); + + // It may not be well-defined what times around the 2-3AM range one might expect to be + // included or not included on the weird day when 2AM doesn't exist, but other unrelated + // times of day (here, 3PM) should definitely be out. + Calendar out1 = getDaylightSavingsForwardDay(); + out1.set(Calendar.HOUR_OF_DAY, 15); + out1.set(Calendar.MINUTE, 0); + out1.set(Calendar.SECOND, 0); + out1.set(Calendar.MILLISECOND, 0); + + Calendar out2 = getDaylightSavingsForwardDay(); + out2.add(Calendar.DATE, 1); + out2.set(Calendar.HOUR_OF_DAY, 15); + out2.set(Calendar.MINUTE, 0); + out2.set(Calendar.SECOND, 0); + out2.set(Calendar.MILLISECOND, 0); + + assertFalse(mScheduleCalendar.isInSchedule(out1.getTimeInMillis())); + assertFalse(mScheduleCalendar.isInSchedule(out2.getTimeInMillis())); + } + @Test public void testIsAlarmInSchedule_alarmAndNowInSchedule_sameScheduleTrigger_daylightSavings() { - Calendar alarm = getDaylightSavingsDay(); + // Need to set the time zone explicitly to a US one so that the daylight savings time day is + // correct. + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); + Calendar alarm = getDaylightSavingsForwardDay(); alarm.set(Calendar.HOUR_OF_DAY, 23); alarm.set(Calendar.MINUTE, 15); alarm.set(Calendar.SECOND, 0); alarm.set(Calendar.MILLISECOND, 0); - Calendar now = getDaylightSavingsDay(); + Calendar now = getDaylightSavingsForwardDay(); now.set(Calendar.HOUR_OF_DAY, 2); now.set(Calendar.MINUTE, 10); now.set(Calendar.SECOND, 0); now.set(Calendar.MILLISECOND, 0); now.add(Calendar.DATE, 1); // add a day, on daylight savings this becomes 3:10am - final Calendar tempToday = getDaylightSavingsDay(); - final Calendar tempTomorrow = getDaylightSavingsDay(); + final Calendar tempToday = getDaylightSavingsForwardDay(); + final Calendar tempTomorrow = getDaylightSavingsForwardDay(); tempTomorrow.add(Calendar.DATE, 1); mScheduleInfo.days = new int[] {tempToday.get(Calendar.DAY_OF_WEEK), tempTomorrow.get(Calendar.DAY_OF_WEEK)}; @@ -506,6 +862,80 @@ public class ScheduleCalendarTest extends UiServiceTestCase { now.getTimeInMillis())); } + @Test + public void testClosestActualTime_regularTimesAndSkippedTime() { + // Make sure we're operating in the relevant time zone for the assumed Daylight Savings day + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); + Calendar day = getDaylightSavingsForwardDay(); + day.set(Calendar.HOUR_OF_DAY, 15); + day.set(Calendar.MINUTE, 25); + day.set(Calendar.SECOND, 0); + day.set(Calendar.MILLISECOND, 0); + assertEquals(day.getTimeInMillis(), + mScheduleCalendar.getClosestActualTime(day.getTimeInMillis(), 15, 25)); + + // Check a skipped time + day.add(Calendar.DATE, 1); + day.set(Calendar.HOUR_OF_DAY, 3); + day.set(Calendar.MINUTE, 0); + day.set(Calendar.SECOND, 0); + day.set(Calendar.MILLISECOND, 0); + assertEquals(day.getTimeInMillis(), + mScheduleCalendar.getClosestActualTime(day.getTimeInMillis(), 2, 15)); + + // Check a non-skipped time after the clocks have moved forward + day.set(Calendar.HOUR_OF_DAY, 15); + day.set(Calendar.MINUTE, 25); + day.set(Calendar.SECOND, 0); + day.set(Calendar.MILLISECOND, 0); + assertEquals(day.getTimeInMillis(), + mScheduleCalendar.getClosestActualTime(day.getTimeInMillis(), 15, 25)); + } + + @Test + public void testClosestActualTime_otherTimeZones() { + // Make sure this doesn't only work for US/Eastern time. + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("Europe/London")); + Calendar ukDstDay = new GregorianCalendar(TimeZone.getTimeZone("Europe/London")); + ukDstDay.set(2021, Calendar.MARCH, 28); + + // Check a skipped time, which is 01:xx on that day in the UK + ukDstDay.set(Calendar.HOUR_OF_DAY, 2); + ukDstDay.set(Calendar.MINUTE, 0); + ukDstDay.set(Calendar.SECOND, 0); + ukDstDay.set(Calendar.MILLISECOND, 0); + assertEquals(ukDstDay.getTimeInMillis(), + mScheduleCalendar.getClosestActualTime(ukDstDay.getTimeInMillis(), 1, 25)); + + // Check a non-skipped time + ukDstDay.set(Calendar.HOUR_OF_DAY, 11); + ukDstDay.set(Calendar.MINUTE, 23); + ukDstDay.set(Calendar.SECOND, 0); + ukDstDay.set(Calendar.MILLISECOND, 0); + assertEquals(ukDstDay.getTimeInMillis(), + mScheduleCalendar.getClosestActualTime(ukDstDay.getTimeInMillis(), 11, 23)); + + mScheduleCalendar.setTimeZone(TimeZone.getTimeZone("Europe/Paris")); + Calendar frDstDay = new GregorianCalendar(TimeZone.getTimeZone("Europe/Paris")); + frDstDay.set(2021, Calendar.MARCH, 28); + + // Check a skipped time, which is 02:xx on that day in France + frDstDay.set(Calendar.HOUR_OF_DAY, 3); + frDstDay.set(Calendar.MINUTE, 0); + frDstDay.set(Calendar.SECOND, 0); + frDstDay.set(Calendar.MILLISECOND, 0); + assertEquals(frDstDay.getTimeInMillis(), + mScheduleCalendar.getClosestActualTime(frDstDay.getTimeInMillis(), 2, 25)); + + // Check a regular time + frDstDay.set(Calendar.HOUR_OF_DAY, 14); + frDstDay.set(Calendar.MINUTE, 59); + frDstDay.set(Calendar.SECOND, 0); + frDstDay.set(Calendar.MILLISECOND, 0); + assertEquals(frDstDay.getTimeInMillis(), + mScheduleCalendar.getClosestActualTime(frDstDay.getTimeInMillis(), 14, 59)); + } + private int getTodayDay() { return new GregorianCalendar().get(Calendar.DAY_OF_WEEK); } @@ -517,9 +947,23 @@ public class ScheduleCalendarTest extends UiServiceTestCase { } - private Calendar getDaylightSavingsDay() { - // the day before daylight savings in the US - March 9, 2019 - Calendar daylightSavingsDay = new GregorianCalendar(2019, 2, 9); + private Calendar getDaylightSavingsForwardDay() { + // the day before daylight savings rolls forward in the US - March 9, 2019 + // 2AM March 10, 2019 does not exist -- goes straight from 1:59 to 3:00 + // Specifically set to US/Eastern time zone rather than relying on a default time zone + // to make sure the date is the correct one, since DST changes vary by region. + Calendar daylightSavingsDay = new GregorianCalendar( + TimeZone.getTimeZone("America/New_York")); + daylightSavingsDay.set(2019, Calendar.MARCH, 9); + return daylightSavingsDay; + } + + private Calendar getDaylightSavingsBackwardDay() { + // the day before daylight savings rolls backward in the US - November 2, 2019 + // In this instance, 1AM November 3 2019 is repeated twice; 1:00->1:59->1:00->1:59->2:00 + Calendar daylightSavingsDay = new GregorianCalendar( + TimeZone.getTimeZone("America/New_York")); + daylightSavingsDay.set(2019, Calendar.NOVEMBER, 2); return daylightSavingsDay; } }