Home | History | Annotate | Download | only in admin
      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      */
     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