Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 36fd35b4 authored by Yuri Lin's avatar Yuri Lin
Browse files

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
parent 2f087e93
Loading
Loading
Loading
Loading
+52 −7
Original line number Original line Diff line number Diff line
@@ -20,6 +20,8 @@ import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.util.ArraySet;
import android.util.ArraySet;
import android.util.Log;
import android.util.Log;


import com.android.internal.annotations.VisibleForTesting;

import java.util.Calendar;
import java.util.Calendar;
import java.util.Objects;
import java.util.Objects;
import java.util.TimeZone;
import java.util.TimeZone;
@@ -92,16 +94,22 @@ public class ScheduleCalendar {
     */
     */
    public long getNextChangeTime(long now) {
    public long getNextChangeTime(long now) {
        if (mSchedule == null) return 0;
        if (mSchedule == null) return 0;
        final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute);
        final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute, true);
        final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute);
        final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute, false);
        long nextScheduleTime = Math.min(nextStart, nextEnd);
        long nextScheduleTime = Math.min(nextStart, nextEnd);


        return nextScheduleTime;
        return nextScheduleTime;
    }
    }


    private long getNextTime(long now, int hr, int min) {
    private long getNextTime(long now, int hr, int min, boolean adjust) {
        final long time = getTime(now, hr, min);
        // The adjust parameter indicates whether to potentially adjust the time to the closest
        return time <= now ? addDays(time, 1) : time;
        // 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) {
    private long getTime(long millis, int hour, int min) {
@@ -119,7 +127,7 @@ public class ScheduleCalendar {
     */
     */
    public boolean isInSchedule(long time) {
    public boolean isInSchedule(long time) {
        if (mSchedule == null || mDays.size() == 0) return false;
        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);
        long end = getTime(time, mSchedule.endHour, mSchedule.endMinute);
        if (end <= start) {
        if (end <= start) {
            end = addDays(end, 1);
            end = addDays(end, 1);
@@ -134,7 +142,7 @@ public class ScheduleCalendar {
     */
     */
    public boolean isAlarmInSchedule(long alarm, long now) {
    public boolean isAlarmInSchedule(long alarm, long now) {
        if (mSchedule == null || mDays.size() == 0) return false;
        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);
        long end = getTime(alarm, mSchedule.endHour, mSchedule.endMinute);
        if (end <= start) {
        if (end <= start) {
            end = addDays(end, 1);
            end = addDays(end, 1);
@@ -186,4 +194,41 @@ public class ScheduleCalendar {
        mCalendar.add(Calendar.DATE, days);
        mCalendar.add(Calendar.DATE, days);
        return mCalendar.getTimeInMillis();
        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;
    }
}
}
+451 −7

File changed.

Preview size limit exceeded, changes collapsed.