summaryrefslogtreecommitdiff
path: root/android/app/admin/FreezePeriod.java
diff options
context:
space:
mode:
Diffstat (limited to 'android/app/admin/FreezePeriod.java')
-rw-r--r--android/app/admin/FreezePeriod.java355
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);
+ }
+ }
+ }
+}