diff options
Diffstat (limited to 'android/app/admin/FreezePeriod.java')
-rw-r--r-- | android/app/admin/FreezePeriod.java | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/android/app/admin/FreezePeriod.java b/android/app/admin/FreezePeriod.java new file mode 100644 index 00000000..657f0177 --- /dev/null +++ b/android/app/admin/FreezePeriod.java @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2018 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 android.app.admin; + +import android.app.admin.SystemUpdatePolicy.ValidationFailedException; +import android.util.Log; +import android.util.Pair; + +import java.time.LocalDate; +import java.time.MonthDay; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * A class that represents one freeze period which repeats <em>annually</em>. A freeze period has + * two {@link java.time#MonthDay} values that define the start and end dates of the period, both + * inclusive. If the end date is earlier than the start date, the period is considered wrapped + * around the year-end. As far as freeze period is concerned, leap year is disregarded and February + * 29th should be treated as if it were February 28th: so a freeze starting or ending on February + * 28th is identical to a freeze starting or ending on February 29th. When calulating the length of + * a freeze or the distance bewteen two freee periods, February 29th is also ignored. + * + * @see SystemUpdatePolicy#setFreezePeriods + */ +public class FreezePeriod { + private static final String TAG = "FreezePeriod"; + + private static final int DUMMY_YEAR = 2001; + static final int DAYS_IN_YEAR = 365; // 365 since DUMMY_YEAR is not a leap year + + private final MonthDay mStart; + private final MonthDay mEnd; + + /* + * Start and end dates represented by number of days since the beginning of the year. + * They are internal representations of mStart and mEnd with normalized Leap year days + * (Feb 29 == Feb 28 == 59th day of year). All internal calclations are based on + * these two values so that leap year days are disregarded. + */ + private final int mStartDay; // [1, 365] + private final int mEndDay; // [1, 365] + + /** + * Creates a freeze period by its start and end dates. If the end date is earlier than the start + * date, the freeze period is considered wrapping year-end. + */ + public FreezePeriod(MonthDay start, MonthDay end) { + mStart = start; + mStartDay = mStart.atYear(DUMMY_YEAR).getDayOfYear(); + mEnd = end; + mEndDay = mEnd.atYear(DUMMY_YEAR).getDayOfYear(); + } + + /** + * Returns the start date (inclusive) of this freeze period. + */ + public MonthDay getStart() { + return mStart; + } + + /** + * Returns the end date (inclusive) of this freeze period. + */ + public MonthDay getEnd() { + return mEnd; + } + + /** + * @hide + */ + private FreezePeriod(int startDay, int endDay) { + mStartDay = startDay; + mStart = dayOfYearToMonthDay(startDay); + mEndDay = endDay; + mEnd = dayOfYearToMonthDay(endDay); + } + + /** @hide */ + int getLength() { + return getEffectiveEndDay() - mStartDay + 1; + } + + /** @hide */ + boolean isWrapped() { + return mEndDay < mStartDay; + } + + /** + * Returns the effective end day, taking wrapping around year-end into consideration + * @hide + */ + int getEffectiveEndDay() { + if (!isWrapped()) { + return mEndDay; + } else { + return mEndDay + DAYS_IN_YEAR; + } + } + + /** @hide */ + boolean contains(LocalDate localDate) { + final int daysOfYear = dayOfYearDisregardLeapYear(localDate); + if (!isWrapped()) { + // ---[start---now---end]--- + return (mStartDay <= daysOfYear) && (daysOfYear <= mEndDay); + } else { + // ---end]---[start---now--- + // or ---now---end]---[start--- + return (mStartDay <= daysOfYear) || (daysOfYear <= mEndDay); + } + } + + /** @hide */ + boolean after(LocalDate localDate) { + return mStartDay > dayOfYearDisregardLeapYear(localDate); + } + + /** + * Instantiate the current interval to real calendar dates, given a calendar date + * {@code now}. If the interval contains now, the returned calendar dates should be the + * current interval (in real calendar dates) that includes now. If the interval does not + * include now, the returned dates represents the next future interval. + * The result will always have the same month and dayOfMonth value as the non-instantiated + * interval itself. + * @hide + */ + Pair<LocalDate, LocalDate> toCurrentOrFutureRealDates(LocalDate now) { + final int nowDays = dayOfYearDisregardLeapYear(now); + final int startYearAdjustment, endYearAdjustment; + if (contains(now)) { + // current interval + if (mStartDay <= nowDays) { + // ----------[start---now---end]--- + // or ---end]---[start---now---------- + startYearAdjustment = 0; + endYearAdjustment = isWrapped() ? 1 : 0; + } else /* nowDays <= mEndDay */ { + // or ---now---end]---[start---------- + startYearAdjustment = -1; + endYearAdjustment = 0; + } + } else { + // next interval + if (mStartDay > nowDays) { + // ----------now---[start---end]--- + // or ---end]---now---[start---------- + startYearAdjustment = 0; + endYearAdjustment = isWrapped() ? 1 : 0; + } else /* mStartDay <= nowDays */ { + // or ---[start---end]---now---------- + startYearAdjustment = 1; + endYearAdjustment = 1; + } + } + final LocalDate startDate = LocalDate.ofYearDay(DUMMY_YEAR, mStartDay).withYear( + now.getYear() + startYearAdjustment); + final LocalDate endDate = LocalDate.ofYearDay(DUMMY_YEAR, mEndDay).withYear( + now.getYear() + endYearAdjustment); + return new Pair<>(startDate, endDate); + } + + @Override + public String toString() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd"); + return LocalDate.ofYearDay(DUMMY_YEAR, mStartDay).format(formatter) + " - " + + LocalDate.ofYearDay(DUMMY_YEAR, mEndDay).format(formatter); + } + + /** @hide */ + private static MonthDay dayOfYearToMonthDay(int dayOfYear) { + LocalDate date = LocalDate.ofYearDay(DUMMY_YEAR, dayOfYear); + return MonthDay.of(date.getMonth(), date.getDayOfMonth()); + } + + /** + * Treat the supplied date as in a non-leap year and return its day of year. + * @hide + */ + private static int dayOfYearDisregardLeapYear(LocalDate date) { + return date.withYear(DUMMY_YEAR).getDayOfYear(); + } + + /** + * Compute the number of days between first (inclusive) and second (exclusive), + * treating all years in between as non-leap. + * @hide + */ + public static int distanceWithoutLeapYear(LocalDate first, LocalDate second) { + return dayOfYearDisregardLeapYear(first) - dayOfYearDisregardLeapYear(second) + + DAYS_IN_YEAR * (first.getYear() - second.getYear()); + } + + /** + * Sort, de-duplicate and merge an interval list + * + * Instead of using any fancy logic for merging intervals which has loads of corner cases, + * simply flatten the interval onto a list of 365 calendar days and recreate the interval list + * from that. + * + * This method should return a list of intervals with the following post-conditions: + * 1. Interval.startDay in strictly ascending order + * 2. No two intervals should overlap or touch + * 3. At most one wrapped Interval remains, and it will be at the end of the list + * @hide + */ + static List<FreezePeriod> canonicalizePeriods(List<FreezePeriod> intervals) { + boolean[] taken = new boolean[DAYS_IN_YEAR]; + // First convert the intervals into flat array + for (FreezePeriod interval : intervals) { + for (int i = interval.mStartDay; i <= interval.getEffectiveEndDay(); i++) { + taken[(i - 1) % DAYS_IN_YEAR] = true; + } + } + // Then reconstruct intervals from the array + List<FreezePeriod> result = new ArrayList<>(); + int i = 0; + while (i < DAYS_IN_YEAR) { + if (!taken[i]) { + i++; + continue; + } + final int intervalStart = i + 1; + while (i < DAYS_IN_YEAR && taken[i]) i++; + result.add(new FreezePeriod(intervalStart, i)); + } + // Check if the last entry can be merged to the first entry to become one single + // wrapped interval + final int lastIndex = result.size() - 1; + if (lastIndex > 0 && result.get(lastIndex).mEndDay == DAYS_IN_YEAR + && result.get(0).mStartDay == 1) { + FreezePeriod wrappedInterval = new FreezePeriod(result.get(lastIndex).mStartDay, + result.get(0).mEndDay); + result.set(lastIndex, wrappedInterval); + result.remove(0); + } + return result; + } + + /** + * Verifies if the supplied freeze periods satisfies the constraints set out in + * {@link SystemUpdatePolicy#setFreezePeriods(List)}, and in particular, any single freeze + * period cannot exceed {@link SystemUpdatePolicy#FREEZE_PERIOD_MAX_LENGTH} days, and two freeze + * periods need to be at least {@link SystemUpdatePolicy#FREEZE_PERIOD_MIN_SEPARATION} days + * apart. + * + * @hide + */ + static void validatePeriods(List<FreezePeriod> periods) { + List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods); + if (allPeriods.size() != periods.size()) { + throw SystemUpdatePolicy.ValidationFailedException.duplicateOrOverlapPeriods(); + } + for (int i = 0; i < allPeriods.size(); i++) { + FreezePeriod current = allPeriods.get(i); + if (current.getLength() > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) { + throw SystemUpdatePolicy.ValidationFailedException.freezePeriodTooLong("Freeze " + + "period " + current + " is too long: " + current.getLength() + " days"); + } + FreezePeriod previous = i > 0 ? allPeriods.get(i - 1) + : allPeriods.get(allPeriods.size() - 1); + if (previous != current) { + final int separation; + if (i == 0 && !previous.isWrapped()) { + // -->[current]---[-previous-]<--- + separation = current.mStartDay + + (DAYS_IN_YEAR - previous.mEndDay) - 1; + } else { + // --[previous]<--->[current]--------- + // OR ----prev---]<--->[current]---[prev- + separation = current.mStartDay - previous.mEndDay - 1; + } + if (separation < SystemUpdatePolicy.FREEZE_PERIOD_MIN_SEPARATION) { + throw SystemUpdatePolicy.ValidationFailedException.freezePeriodTooClose("Freeze" + + " periods " + previous + " and " + current + " are too close " + + "together: " + separation + " days apart"); + } + } + } + } + + /** + * Verifies that the current freeze periods are still legal, considering the previous freeze + * periods the device went through. In particular, when combined with the previous freeze + * period, the maximum freeze length or the minimum freeze separation should not be violated. + * + * @hide + */ + static void validateAgainstPreviousFreezePeriod(List<FreezePeriod> periods, + LocalDate prevPeriodStart, LocalDate prevPeriodEnd, LocalDate now) { + if (periods.size() == 0 || prevPeriodStart == null || prevPeriodEnd == null) { + return; + } + if (prevPeriodStart.isAfter(now) || prevPeriodEnd.isAfter(now)) { + Log.w(TAG, "Previous period (" + prevPeriodStart + "," + prevPeriodEnd + ") is after" + + " current date " + now); + // Clock was adjusted backwards. We can continue execution though, the separation + // and length validation below still works under this condition. + } + List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods); + // Given current time now, find the freeze period that's either current, or the one + // that's immediately afterwards. For the later case, it might be after the year-end, + // but this can only happen if there is only one freeze period. + FreezePeriod curOrNextFreezePeriod = allPeriods.get(0); + for (FreezePeriod interval : allPeriods) { + if (interval.contains(now) + || interval.mStartDay > FreezePeriod.dayOfYearDisregardLeapYear(now)) { + curOrNextFreezePeriod = interval; + break; + } + } + Pair<LocalDate, LocalDate> curOrNextFreezeDates = curOrNextFreezePeriod + .toCurrentOrFutureRealDates(now); + if (now.isAfter(curOrNextFreezeDates.first)) { + curOrNextFreezeDates = new Pair<>(now, curOrNextFreezeDates.second); + } + if (curOrNextFreezeDates.first.isAfter(curOrNextFreezeDates.second)) { + throw new IllegalStateException("Current freeze dates inverted: " + + curOrNextFreezeDates.first + "-" + curOrNextFreezeDates.second); + } + // Now validate [prevPeriodStart, prevPeriodEnd] against curOrNextFreezeDates + final String periodsDescription = "Prev: " + prevPeriodStart + "," + prevPeriodEnd + + "; cur: " + curOrNextFreezeDates.first + "," + curOrNextFreezeDates.second; + long separation = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.first, + prevPeriodEnd) - 1; + if (separation > 0) { + // Two intervals do not overlap, check separation + if (separation < SystemUpdatePolicy.FREEZE_PERIOD_MIN_SEPARATION) { + throw ValidationFailedException.combinedPeriodTooClose("Previous freeze period " + + "too close to new period: " + separation + ", " + periodsDescription); + } + } else { + // Two intervals overlap, check combined length + long length = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.second, + prevPeriodStart) + 1; + if (length > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) { + throw ValidationFailedException.combinedPeriodTooLong("Combined freeze period " + + "exceeds maximum days: " + length + ", " + periodsDescription); + } + } + } +} |