Handle several daylight savings related edge cases in ScheduleCalendar.

The core functionality changes that were previously broken that this change fixes are:
- a schedule such as 02:30-03:15 would incorrectly span the next full day on the day 2AM is skipped
- getNextChangeTime would return an incorrect time when called on the day clocks change when the next change is on the next day

The new behavior changes only when a schedule's start time is during a skipped hour; in those cases, the schedule then "begins" at the next valid time (3AM in the case of something that would start at 2:xx).

This change also adds a bunch of unit tests surrounding daylight savings time: schedule starts and ends on both daylight adjustment periods, and time adjustment in various time zones.

Test: atest ScheduleCalendarTest
Bug: 74521742
Change-Id: Ia59ef296a3b54de5cadccab82d87cbb81ded0733
This commit is contained in:
Yuri Lin
2021-06-14 10:51:08 -04:00
parent 2f087e93c7
commit 36fd35b4ee
2 changed files with 503 additions and 14 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}