FreezePeriod.java revision 1b2f37401818b04cf4908d5aa9017eab44fe5662
1/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package android.app.admin;
17
18import android.app.admin.SystemUpdatePolicy.ValidationFailedException;
19import android.util.Log;
20import android.util.Pair;
21
22import java.time.LocalDate;
23import java.time.MonthDay;
24import java.time.format.DateTimeFormatter;
25import java.util.ArrayList;
26import java.util.List;
27
28/**
29 * A class that represents one freeze period which repeats <em>annually</em>. A freeze period has
30 * two {@link java.time#MonthDay} values that define the start and end dates of the period, both
31 * inclusive. If the end date is earlier than the start date, the period is considered wrapped
32 * around the year-end. As far as freeze period is concerned, leap year is disregarded and February
33 * 29th should be treated as if it were February 28th: so a freeze starting or ending on February
34 * 28th is identical to a freeze starting or ending on February 29th. When calulating the length of
35 * a freeze or the distance bewteen two freee periods, February 29th is also ignored.
36 *
37 * @see SystemUpdatePolicy#setFreezePeriods
38 */
39public class FreezePeriod {
40    private static final String TAG = "FreezePeriod";
41
42    private static final int DUMMY_YEAR = 2001;
43    static final int DAYS_IN_YEAR = 365; // 365 since DUMMY_YEAR is not a leap year
44
45    private final MonthDay mStart;
46    private final MonthDay mEnd;
47
48    /*
49     * Start and end dates represented by number of days since the beginning of the year.
50     * They are internal representations of mStart and mEnd with normalized Leap year days
51     * (Feb 29 == Feb 28 == 59th day of year). All internal calclations are based on
52     * these two values so that leap year days are disregarded.
53     */
54    private final int mStartDay; // [1, 365]
55    private final int mEndDay; // [1, 365]
56
57    /**
58     * Creates a freeze period by its start and end dates. If the end date is earlier than the start
59     * date, the freeze period is considered wrapping year-end.
60     */
61    public FreezePeriod(MonthDay start, MonthDay end) {
62        mStart = start;
63        mStartDay = mStart.atYear(DUMMY_YEAR).getDayOfYear();
64        mEnd = end;
65        mEndDay = mEnd.atYear(DUMMY_YEAR).getDayOfYear();
66    }
67
68    /**
69     * Returns the start date (inclusive) of this freeze period.
70     */
71    public MonthDay getStart() {
72        return mStart;
73    }
74
75    /**
76     * Returns the end date (inclusive) of this freeze period.
77     */
78    public MonthDay getEnd() {
79        return mEnd;
80    }
81
82    /**
83     * @hide
84     */
85    private FreezePeriod(int startDay, int endDay) {
86        mStartDay = startDay;
87        mStart = dayOfYearToMonthDay(startDay);
88        mEndDay = endDay;
89        mEnd = dayOfYearToMonthDay(endDay);
90    }
91
92    /** @hide */
93    int getLength() {
94        return getEffectiveEndDay() - mStartDay + 1;
95    }
96
97    /** @hide */
98    boolean isWrapped() {
99        return mEndDay < mStartDay;
100    }
101
102    /**
103     * Returns the effective end day, taking wrapping around year-end into consideration
104     * @hide
105     */
106    int getEffectiveEndDay() {
107        if (!isWrapped()) {
108            return mEndDay;
109        } else {
110            return mEndDay + DAYS_IN_YEAR;
111        }
112    }
113
114    /** @hide */
115    boolean contains(LocalDate localDate) {
116        final int daysOfYear = dayOfYearDisregardLeapYear(localDate);
117        if (!isWrapped()) {
118            // ---[start---now---end]---
119            return (mStartDay <= daysOfYear) && (daysOfYear <= mEndDay);
120        } else {
121            //    ---end]---[start---now---
122            // or ---now---end]---[start---
123            return (mStartDay <= daysOfYear) || (daysOfYear <= mEndDay);
124        }
125    }
126
127    /** @hide */
128    boolean after(LocalDate localDate) {
129        return mStartDay > dayOfYearDisregardLeapYear(localDate);
130    }
131
132    /**
133     * Instantiate the current interval to real calendar dates, given a calendar date
134     * {@code now}. If the interval contains now, the returned calendar dates should be the
135     * current interval (in real calendar dates) that includes now. If the interval does not
136     * include now, the returned dates represents the next future interval.
137     * The result will always have the same month and dayOfMonth value as the non-instantiated
138     * interval itself.
139     * @hide
140     */
141    Pair<LocalDate, LocalDate> toCurrentOrFutureRealDates(LocalDate now) {
142        final int nowDays = dayOfYearDisregardLeapYear(now);
143        final int startYearAdjustment, endYearAdjustment;
144        if (contains(now)) {
145            // current interval
146            if (mStartDay <= nowDays) {
147                //    ----------[start---now---end]---
148                // or ---end]---[start---now----------
149                startYearAdjustment = 0;
150                endYearAdjustment = isWrapped() ? 1 : 0;
151            } else /* nowDays <= mEndDay */ {
152                // or ---now---end]---[start----------
153                startYearAdjustment = -1;
154                endYearAdjustment = 0;
155            }
156        } else {
157            // next interval
158            if (mStartDay > nowDays) {
159                //    ----------now---[start---end]---
160                // or ---end]---now---[start----------
161                startYearAdjustment = 0;
162                endYearAdjustment = isWrapped() ? 1 : 0;
163            } else /* mStartDay <= nowDays */ {
164                // or ---[start---end]---now----------
165                startYearAdjustment = 1;
166                endYearAdjustment = 1;
167            }
168        }
169        final LocalDate startDate = LocalDate.ofYearDay(DUMMY_YEAR, mStartDay).withYear(
170                now.getYear() + startYearAdjustment);
171        final LocalDate endDate = LocalDate.ofYearDay(DUMMY_YEAR, mEndDay).withYear(
172                now.getYear() + endYearAdjustment);
173        return new Pair<>(startDate, endDate);
174    }
175
176    @Override
177    public String toString() {
178        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd");
179        return LocalDate.ofYearDay(DUMMY_YEAR, mStartDay).format(formatter) + " - "
180                + LocalDate.ofYearDay(DUMMY_YEAR, mEndDay).format(formatter);
181    }
182
183    /** @hide */
184    private static MonthDay dayOfYearToMonthDay(int dayOfYear) {
185        LocalDate date = LocalDate.ofYearDay(DUMMY_YEAR, dayOfYear);
186        return MonthDay.of(date.getMonth(), date.getDayOfMonth());
187    }
188
189    /**
190     * Treat the supplied date as in a non-leap year and return its day of year.
191     * @hide
192     */
193    private static int dayOfYearDisregardLeapYear(LocalDate date) {
194        return date.withYear(DUMMY_YEAR).getDayOfYear();
195    }
196
197    /**
198     * Compute the number of days between first (inclusive) and second (exclusive),
199     * treating all years in between as non-leap.
200     * @hide
201     */
202    public static int distanceWithoutLeapYear(LocalDate first, LocalDate second) {
203        return dayOfYearDisregardLeapYear(first) - dayOfYearDisregardLeapYear(second)
204                + DAYS_IN_YEAR * (first.getYear() - second.getYear());
205    }
206
207    /**
208     * Sort, de-duplicate and merge an interval list
209     *
210     * Instead of using any fancy logic for merging intervals which has loads of corner cases,
211     * simply flatten the interval onto a list of 365 calendar days and recreate the interval list
212     * from that.
213     *
214     * This method should return a list of intervals with the following post-conditions:
215     *     1. Interval.startDay in strictly ascending order
216     *     2. No two intervals should overlap or touch
217     *     3. At most one wrapped Interval remains, and it will be at the end of the list
218     * @hide
219     */
220    static List<FreezePeriod> canonicalizePeriods(List<FreezePeriod> intervals) {
221        boolean[] taken = new boolean[DAYS_IN_YEAR];
222        // First convert the intervals into flat array
223        for (FreezePeriod interval : intervals) {
224            for (int i = interval.mStartDay; i <= interval.getEffectiveEndDay(); i++) {
225                taken[(i - 1) % DAYS_IN_YEAR] = true;
226            }
227        }
228        // Then reconstruct intervals from the array
229        List<FreezePeriod> result = new ArrayList<>();
230        int i = 0;
231        while (i < DAYS_IN_YEAR) {
232            if (!taken[i]) {
233                i++;
234                continue;
235            }
236            final int intervalStart = i + 1;
237            while (i < DAYS_IN_YEAR && taken[i]) i++;
238            result.add(new FreezePeriod(intervalStart, i));
239        }
240        // Check if the last entry can be merged to the first entry to become one single
241        // wrapped interval
242        final int lastIndex = result.size() - 1;
243        if (lastIndex > 0 && result.get(lastIndex).mEndDay == DAYS_IN_YEAR
244                && result.get(0).mStartDay == 1) {
245            FreezePeriod wrappedInterval = new FreezePeriod(result.get(lastIndex).mStartDay,
246                    result.get(0).mEndDay);
247            result.set(lastIndex, wrappedInterval);
248            result.remove(0);
249        }
250        return result;
251    }
252
253    /**
254     * Verifies if the supplied freeze periods satisfies the constraints set out in
255     * {@link SystemUpdatePolicy#setFreezePeriods(List)}, and in particular, any single freeze
256     * period cannot exceed {@link SystemUpdatePolicy#FREEZE_PERIOD_MAX_LENGTH} days, and two freeze
257     * periods need to be at least {@link SystemUpdatePolicy#FREEZE_PERIOD_MIN_SEPARATION} days
258     * apart.
259     *
260     * @hide
261     */
262    static void validatePeriods(List<FreezePeriod> periods) {
263        List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods);
264        if (allPeriods.size() != periods.size()) {
265            throw SystemUpdatePolicy.ValidationFailedException.duplicateOrOverlapPeriods();
266        }
267        for (int i = 0; i < allPeriods.size(); i++) {
268            FreezePeriod current = allPeriods.get(i);
269            if (current.getLength() > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) {
270                throw SystemUpdatePolicy.ValidationFailedException.freezePeriodTooLong("Freeze "
271                        + "period " + current + " is too long: " + current.getLength() + " days");
272            }
273            FreezePeriod previous = i > 0 ? allPeriods.get(i - 1)
274                    : allPeriods.get(allPeriods.size() - 1);
275            if (previous != current) {
276                final int separation;
277                if (i == 0 && !previous.isWrapped()) {
278                    // -->[current]---[-previous-]<---
279                    separation = current.mStartDay
280                            + (DAYS_IN_YEAR - previous.mEndDay) - 1;
281                } else {
282                    //    --[previous]<--->[current]---------
283                    // OR ----prev---]<--->[current]---[prev-
284                    separation = current.mStartDay - previous.mEndDay - 1;
285                }
286                if (separation < SystemUpdatePolicy.FREEZE_PERIOD_MIN_SEPARATION) {
287                    throw SystemUpdatePolicy.ValidationFailedException.freezePeriodTooClose("Freeze"
288                            + " periods " + previous + " and " + current + " are too close "
289                            + "together: " + separation + " days apart");
290                }
291            }
292        }
293    }
294
295    /**
296     * Verifies that the current freeze periods are still legal, considering the previous freeze
297     * periods the device went through. In particular, when combined with the previous freeze
298     * period, the maximum freeze length or the minimum freeze separation should not be violated.
299     *
300     * @hide
301     */
302    static void validateAgainstPreviousFreezePeriod(List<FreezePeriod> periods,
303            LocalDate prevPeriodStart, LocalDate prevPeriodEnd, LocalDate now) {
304        if (periods.size() == 0 || prevPeriodStart == null || prevPeriodEnd == null) {
305            return;
306        }
307        if (prevPeriodStart.isAfter(now) || prevPeriodEnd.isAfter(now)) {
308            Log.w(TAG, "Previous period (" + prevPeriodStart + "," + prevPeriodEnd + ") is after"
309                    + " current date " + now);
310            // Clock was adjusted backwards. We can continue execution though, the separation
311            // and length validation below still works under this condition.
312        }
313        List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods);
314        // Given current time now, find the freeze period that's either current, or the one
315        // that's immediately afterwards. For the later case, it might be after the year-end,
316        // but this can only happen if there is only one freeze period.
317        FreezePeriod curOrNextFreezePeriod = allPeriods.get(0);
318        for (FreezePeriod interval : allPeriods) {
319            if (interval.contains(now)
320                    || interval.mStartDay > FreezePeriod.dayOfYearDisregardLeapYear(now)) {
321                curOrNextFreezePeriod = interval;
322                break;
323            }
324        }
325        Pair<LocalDate, LocalDate> curOrNextFreezeDates = curOrNextFreezePeriod
326                .toCurrentOrFutureRealDates(now);
327        if (now.isAfter(curOrNextFreezeDates.first)) {
328            curOrNextFreezeDates = new Pair<>(now, curOrNextFreezeDates.second);
329        }
330        if (curOrNextFreezeDates.first.isAfter(curOrNextFreezeDates.second)) {
331            throw new IllegalStateException("Current freeze dates inverted: "
332                    + curOrNextFreezeDates.first + "-" + curOrNextFreezeDates.second);
333        }
334        // Now validate [prevPeriodStart, prevPeriodEnd] against curOrNextFreezeDates
335        final String periodsDescription = "Prev: " + prevPeriodStart + "," + prevPeriodEnd
336                + "; cur: " + curOrNextFreezeDates.first + "," + curOrNextFreezeDates.second;
337        long separation = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.first,
338                prevPeriodEnd) - 1;
339        if (separation > 0) {
340            // Two intervals do not overlap, check separation
341            if (separation < SystemUpdatePolicy.FREEZE_PERIOD_MIN_SEPARATION) {
342                throw ValidationFailedException.combinedPeriodTooClose("Previous freeze period "
343                        + "too close to new period: " + separation + ", " + periodsDescription);
344            }
345        } else {
346            // Two intervals overlap, check combined length
347            long length = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.second,
348                    prevPeriodStart) + 1;
349            if (length > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) {
350                throw ValidationFailedException.combinedPeriodTooLong("Combined freeze period "
351                        + "exceeds maximum days: " + length + ", " + periodsDescription);
352            }
353        }
354    }
355}
356