content: /*
   * 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);
              }
          }
      }
  }
  