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  */
16 package android.app.admin;
17 
18 import android.app.admin.SystemUpdatePolicy.ValidationFailedException;
19 import android.util.Log;
20 import android.util.Pair;
21 
22 import java.time.LocalDate;
23 import java.time.MonthDay;
24 import java.time.format.DateTimeFormatter;
25 import java.util.ArrayList;
26 import 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  */
39 public 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      */
FreezePeriod(MonthDay start, MonthDay end)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      */
getStart()71     public MonthDay getStart() {
72         return mStart;
73     }
74 
75     /**
76      * Returns the end date (inclusive) of this freeze period.
77      */
getEnd()78     public MonthDay getEnd() {
79         return mEnd;
80     }
81 
82     /**
83      * @hide
84      */
FreezePeriod(int startDay, int endDay)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 */
getLength()93     int getLength() {
94         return getEffectiveEndDay() - mStartDay + 1;
95     }
96 
97     /** @hide */
isWrapped()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      */
getEffectiveEndDay()106     int getEffectiveEndDay() {
107         if (!isWrapped()) {
108             return mEndDay;
109         } else {
110             return mEndDay + DAYS_IN_YEAR;
111         }
112     }
113 
114     /** @hide */
contains(LocalDate localDate)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 */
after(LocalDate localDate)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      */
toCurrentOrFutureRealDates(LocalDate now)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
toString()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 */
dayOfYearToMonthDay(int dayOfYear)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      */
dayOfYearDisregardLeapYear(LocalDate date)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      */
distanceWithoutLeapYear(LocalDate first, LocalDate second)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      */
canonicalizePeriods(List<FreezePeriod> intervals)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      */
validatePeriods(List<FreezePeriod> periods)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      */
validateAgainstPreviousFreezePeriod(List<FreezePeriod> periods, LocalDate prevPeriodStart, LocalDate prevPeriodEnd, LocalDate now)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