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