blob: 657f0177097eae4689a880b0048b3e4aca4e3475 [file] [log] [blame]
/*
* 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);
}
}
}
}