summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt14
-rw-r--r--cmds/dpm/src/com/android/commands/dpm/Dpm.java15
-rw-r--r--core/java/android/app/admin/DevicePolicyManager.java31
-rw-r--r--core/java/android/app/admin/FreezeInterval.java299
-rw-r--r--core/java/android/app/admin/IDevicePolicyManager.aidl1
-rw-r--r--core/java/android/app/admin/SystemUpdatePolicy.java310
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java3
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java100
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/Owners.java81
-rw-r--r--services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java371
10 files changed, 1210 insertions, 15 deletions
diff --git a/api/current.txt b/api/current.txt
index fbcedc093cb7..42dfcf8eb105 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6759,9 +6759,11 @@ package android.app.admin {
method public static android.app.admin.SystemUpdatePolicy createPostponeInstallPolicy();
method public static android.app.admin.SystemUpdatePolicy createWindowedInstallPolicy(int, int);
method public int describeContents();
+ method public java.util.List<android.util.Pair<java.lang.Integer, java.lang.Integer>> getFreezePeriods();
method public int getInstallWindowEnd();
method public int getInstallWindowStart();
method public int getPolicyType();
+ method public android.app.admin.SystemUpdatePolicy setFreezePeriods(java.util.List<android.util.Pair<java.lang.Integer, java.lang.Integer>>);
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.app.admin.SystemUpdatePolicy> CREATOR;
field public static final int TYPE_INSTALL_AUTOMATIC = 1; // 0x1
@@ -6769,6 +6771,18 @@ package android.app.admin {
field public static final int TYPE_POSTPONE = 3; // 0x3
}
+ public static final class SystemUpdatePolicy.ValidationFailedException extends java.lang.IllegalArgumentException implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getErrorCode();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.app.admin.SystemUpdatePolicy.ValidationFailedException> CREATOR;
+ field public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE = 5; // 0x5
+ field public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG = 4; // 0x4
+ field public static final int ERROR_DUPLICATE_OR_OVERLAP = 1; // 0x1
+ field public static final int ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE = 3; // 0x3
+ field public static final int ERROR_NEW_FREEZE_PERIOD_TOO_LONG = 2; // 0x2
+ }
+
}
package android.app.assist {
diff --git a/cmds/dpm/src/com/android/commands/dpm/Dpm.java b/cmds/dpm/src/com/android/commands/dpm/Dpm.java
index 3ac70d668198..47581e10e937 100644
--- a/cmds/dpm/src/com/android/commands/dpm/Dpm.java
+++ b/cmds/dpm/src/com/android/commands/dpm/Dpm.java
@@ -45,6 +45,7 @@ public final class Dpm extends BaseCommand {
private static final String COMMAND_SET_DEVICE_OWNER = "set-device-owner";
private static final String COMMAND_SET_PROFILE_OWNER = "set-profile-owner";
private static final String COMMAND_REMOVE_ACTIVE_ADMIN = "remove-active-admin";
+ private static final String COMMAND_CLEAR_FREEZE_PERIOD_RECORD = "clear-freeze-period-record";
private IDevicePolicyManager mDevicePolicyManager;
private int mUserId = UserHandle.USER_SYSTEM;
@@ -75,7 +76,11 @@ public final class Dpm extends BaseCommand {
"\n" +
"dpm remove-active-admin: Disables an active admin, the admin must have declared" +
" android:testOnly in the application in its manifest. This will also remove" +
- " device and profile owners\n");
+ " device and profile owners\n" +
+ "\n" +
+ "dpm " + COMMAND_CLEAR_FREEZE_PERIOD_RECORD + ": clears framework-maintained " +
+ "record of past freeze periods that the device went through. For use during " +
+ "feature development to prevent triggering restriction on setting freeze periods");
}
@Override
@@ -101,6 +106,9 @@ public final class Dpm extends BaseCommand {
case COMMAND_REMOVE_ACTIVE_ADMIN:
runRemoveActiveAdmin();
break;
+ case COMMAND_CLEAR_FREEZE_PERIOD_RECORD:
+ runClearFreezePeriodRecord();
+ break;
default:
throw new IllegalArgumentException ("unknown command '" + command + "'");
}
@@ -190,6 +198,11 @@ public final class Dpm extends BaseCommand {
+ mComponent.toShortString() + " for user " + mUserId);
}
+ private void runClearFreezePeriodRecord() throws RemoteException {
+ mDevicePolicyManager.clearSystemUpdatePolicyFreezePeriodRecord();
+ System.out.println("Success");
+ }
+
private ComponentName parseComponentName(String component) {
ComponentName cn = ComponentName.unflattenFromString(component);
if (cn == null) {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 8f76032b4c62..4c3e31f791f5 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -7540,13 +7540,28 @@ public class DevicePolicyManager {
/**
* Called by device owners to set a local system update policy. When a new policy is set,
* {@link #ACTION_SYSTEM_UPDATE_POLICY_CHANGED} is broadcasted.
+ * <p>
+ * If the supplied system update policy has freeze periods set but the freeze periods do not
+ * meet 90-day maximum length or 60-day minimum separation requirement set out in
+ * {@link SystemUpdatePolicy#setFreezePeriods},
+ * {@link SystemUpdatePolicy.ValidationFailedException} will the thrown. Note that the system
+ * keeps a record of freeze periods the device experienced previously, and combines them with
+ * the new freeze periods to be set when checking the maximum freeze length and minimum freeze
+ * separation constraints. As a result, freeze periods that passed validation during
+ * {@link SystemUpdatePolicy#setFreezePeriods} might fail the additional checks here due to
+ * the freeze period history. If this is causing issues during development,
+ * {@code adb shell dpm clear-freeze-period-record} can be used to clear the record.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with. All
* components in the device owner package can set system update policies and the most
* recent policy takes effect.
* @param policy the new policy, or {@code null} to clear the current policy.
* @throws SecurityException if {@code admin} is not a device owner.
+ * @throws IllegalArgumentException if the policy type or maintenance window is not valid.
+ * @throws SystemUpdatePolicy.ValidationFailedException if the policy's freeze period does not
+ * meet the requirement.
* @see SystemUpdatePolicy
+ * @see SystemUpdatePolicy#setFreezePeriods(List)
*/
public void setSystemUpdatePolicy(@NonNull ComponentName admin, SystemUpdatePolicy policy) {
throwIfParentInstance("setSystemUpdatePolicy");
@@ -7577,6 +7592,22 @@ public class DevicePolicyManager {
}
/**
+ * Reset record of previous system update freeze period the device went through.
+ * Only callable by ADB.
+ * @hide
+ */
+ public void clearSystemUpdatePolicyFreezePeriodRecord() {
+ throwIfParentInstance("clearSystemUpdatePolicyFreezePeriodRecord");
+ if (mService == null) {
+ return;
+ }
+ try {
+ mService.clearSystemUpdatePolicyFreezePeriodRecord();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ /**
* Called by a device owner or profile owner of secondary users that is affiliated with the
* device to disable the keyguard altogether.
* <p>
diff --git a/core/java/android/app/admin/FreezeInterval.java b/core/java/android/app/admin/FreezeInterval.java
new file mode 100644
index 000000000000..7acdfc8fe100
--- /dev/null
+++ b/core/java/android/app/admin/FreezeInterval.java
@@ -0,0 +1,299 @@
+/*
+ * 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.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An interval representing one freeze period which repeats annually. We use the number of days
+ * since the start of (non-leap) year to define the start and end dates of an interval, both
+ * inclusive. If the end date is smaller than the start date, the interval is considered wrapped
+ * around the year-end. As far as an interval is concerned, February 29th should be treated as
+ * if it were February 28th: so an interval starting or ending on February 28th are not
+ * distinguishable from an interval on February 29th. When calulating interval length or
+ * distance between two dates, February 29th is also disregarded.
+ *
+ * @see SystemUpdatePolicy#setFreezePeriods
+ * @hide
+ */
+public class FreezeInterval {
+ private static final String TAG = "FreezeInterval";
+
+ private static final int DUMMY_YEAR = 2001;
+ static final int DAYS_IN_YEAR = 365; // 365 since DUMMY_YEAR is not a leap year
+
+ final int mStartDay; // [1,365]
+ final int mEndDay; // [1,365]
+
+ FreezeInterval(int startDay, int endDay) {
+ if (startDay < 1 || startDay > 365 || endDay < 1 || endDay > 365) {
+ throw new RuntimeException("Bad dates for Interval: " + startDay + "," + endDay);
+ }
+ mStartDay = startDay;
+ mEndDay = endDay;
+ }
+
+ int getLength() {
+ return getEffectiveEndDay() - mStartDay + 1;
+ }
+
+ boolean isWrapped() {
+ return mEndDay < mStartDay;
+ }
+
+ /**
+ * Returns the effective end day, taking wrapping around year-end into consideration
+ */
+ int getEffectiveEndDay() {
+ if (!isWrapped()) {
+ return mEndDay;
+ } else {
+ return mEndDay + DAYS_IN_YEAR;
+ }
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ 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);
+ }
+
+ // Treat the supplied date as in a non-leap year and return its day of year.
+ 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.
+ */
+ 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
+ */
+ private static List<FreezeInterval> canonicalizeIntervals(List<FreezeInterval> intervals) {
+ boolean[] taken = new boolean[DAYS_IN_YEAR];
+ // First convert the intervals into flat array
+ for (FreezeInterval 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<FreezeInterval> 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 FreezeInterval(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) {
+ FreezeInterval wrappedInterval = new FreezeInterval(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
+ */
+ protected static void validatePeriods(List<FreezeInterval> periods) {
+ List<FreezeInterval> allPeriods = FreezeInterval.canonicalizeIntervals(periods);
+ if (allPeriods.size() != periods.size()) {
+ throw SystemUpdatePolicy.ValidationFailedException.duplicateOrOverlapPeriods();
+ }
+ for (int i = 0; i < allPeriods.size(); i++) {
+ FreezeInterval 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");
+ }
+ FreezeInterval 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
+ */
+ protected static void validateAgainstPreviousFreezePeriod(List<FreezeInterval> 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<FreezeInterval> allPeriods = FreezeInterval.canonicalizeIntervals(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.
+ FreezeInterval curOrNextFreezePeriod = allPeriods.get(0);
+ for (FreezeInterval interval : allPeriods) {
+ if (interval.contains(now)
+ || interval.mStartDay > FreezeInterval.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 = FreezeInterval.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 = FreezeInterval.distanceWithoutLeapYear(curOrNextFreezeDates.second,
+ prevPeriodStart) + 1;
+ if (length > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) {
+ throw ValidationFailedException.combinedPeriodTooLong("Combined freeze period "
+ + "exceeds maximum days: " + length + ", " + periodsDescription);
+ }
+ }
+ }
+}
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index daee6b41a365..2afaaa782c7f 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -295,6 +295,7 @@ interface IDevicePolicyManager {
void setSystemUpdatePolicy(in ComponentName who, in SystemUpdatePolicy policy);
SystemUpdatePolicy getSystemUpdatePolicy();
+ void clearSystemUpdatePolicyFreezePeriodRecord();
boolean setKeyguardDisabled(in ComponentName admin, boolean disabled);
boolean setStatusBarDisabled(in ComponentName who, boolean disabled);
diff --git a/core/java/android/app/admin/SystemUpdatePolicy.java b/core/java/android/app/admin/SystemUpdatePolicy.java
index 232a688762de..05d3fd9c632c 100644
--- a/core/java/android/app/admin/SystemUpdatePolicy.java
+++ b/core/java/android/app/admin/SystemUpdatePolicy.java
@@ -16,16 +16,27 @@
package android.app.admin;
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.END_TAG;
+import static org.xmlpull.v1.XmlPullParser.TEXT;
+
import android.annotation.IntDef;
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.Log;
+import android.util.Pair;
import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
/**
* A class that represents a local system update policy set by the device owner.
@@ -34,6 +45,7 @@ import java.lang.annotation.RetentionPolicy;
* @see DevicePolicyManager#getSystemUpdatePolicy
*/
public class SystemUpdatePolicy implements Parcelable {
+ private static final String TAG = "SystemUpdatePolicy";
/** @hide */
@IntDef(prefix = { "TYPE_" }, value = {
@@ -94,20 +106,157 @@ public class SystemUpdatePolicy implements Parcelable {
private static final String KEY_POLICY_TYPE = "policy_type";
private static final String KEY_INSTALL_WINDOW_START = "install_window_start";
private static final String KEY_INSTALL_WINDOW_END = "install_window_end";
+ private static final String KEY_FREEZE_TAG = "freeze";
+ private static final String KEY_FREEZE_START = "start";
+ private static final String KEY_FREEZE_END = "end";
+
/**
* The upper boundary of the daily maintenance window: 24 * 60 minutes.
*/
private static final int WINDOW_BOUNDARY = 24 * 60;
+ /**
+ * The maximum length of a single freeze period: 90 days.
+ */
+ static final int FREEZE_PERIOD_MAX_LENGTH = 90;
+
+ /**
+ * The minimum allowed time between two adjacent freeze period (from the end of the first
+ * freeze period to the start of the second freeze period, both exclusive): 60 days.
+ */
+ static final int FREEZE_PERIOD_MIN_SEPARATION = 60;
+
+
+ /**
+ * An exception class that represents various validation errors thrown from
+ * {@link SystemUpdatePolicy#setFreezePeriods} and
+ * {@link DevicePolicyManager#setSystemUpdatePolicy}
+ */
+ public static final class ValidationFailedException extends IllegalArgumentException
+ implements Parcelable {
+
+ /** @hide */
+ @IntDef(prefix = { "ERROR_" }, value = {
+ ERROR_NONE,
+ ERROR_DUPLICATE_OR_OVERLAP,
+ ERROR_NEW_FREEZE_PERIOD_TOO_LONG,
+ ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE,
+ ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG,
+ ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ValidationFailureType {}
+
+ /** @hide */
+ public static final int ERROR_NONE = 0;
+
+ /**
+ * The freeze periods contains duplicates, periods that overlap with each
+ * other or periods whose start and end joins.
+ */
+ public static final int ERROR_DUPLICATE_OR_OVERLAP = 1;
+
+ /**
+ * There exists at least one freeze period whose length exceeds 90 days.
+ */
+ public static final int ERROR_NEW_FREEZE_PERIOD_TOO_LONG = 2;
+
+ /**
+ * There exists some freeze period which starts within 60 days of the preceding period's
+ * end time.
+ */
+ public static final int ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE = 3;
+
+ /**
+ * The device has been in a freeze period and when combining with the new freeze period
+ * to be set, it will result in the total freeze period being longer than 90 days.
+ */
+ public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG = 4;
+
+ /**
+ * The device has been in a freeze period and some new freeze period to be set is less
+ * than 60 days from the end of the last freeze period the device went through.
+ */
+ public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE = 5;
+
+ @ValidationFailureType
+ private final int mErrorCode;
+
+ private ValidationFailedException(int errorCode, String message) {
+ super(message);
+ mErrorCode = errorCode;
+ }
+
+ /**
+ * Returns the type of validation error associated with this exception.
+ */
+ public @ValidationFailureType int getErrorCode() {
+ return mErrorCode;
+ }
+
+ /** @hide */
+ public static ValidationFailedException duplicateOrOverlapPeriods() {
+ return new ValidationFailedException(ERROR_DUPLICATE_OR_OVERLAP,
+ "Found duplicate or overlapping periods");
+ }
+
+ /** @hide */
+ public static ValidationFailedException freezePeriodTooLong(String message) {
+ return new ValidationFailedException(ERROR_NEW_FREEZE_PERIOD_TOO_LONG, message);
+ }
+
+ /** @hide */
+ public static ValidationFailedException freezePeriodTooClose(String message) {
+ return new ValidationFailedException(ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE, message);
+ }
+
+ /** @hide */
+ public static ValidationFailedException combinedPeriodTooLong(String message) {
+ return new ValidationFailedException(ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG, message);
+ }
+
+ /** @hide */
+ public static ValidationFailedException combinedPeriodTooClose(String message) {
+ return new ValidationFailedException(ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE, message);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mErrorCode);
+ dest.writeString(getMessage());
+ }
+
+ public static final Parcelable.Creator<ValidationFailedException> CREATOR =
+ new Parcelable.Creator<ValidationFailedException>() {
+ @Override
+ public ValidationFailedException createFromParcel(Parcel source) {
+ return new ValidationFailedException(source.readInt(), source.readString());
+ }
+
+ @Override
+ public ValidationFailedException[] newArray(int size) {
+ return new ValidationFailedException[size];
+ }
+
+ };
+ }
+
@SystemUpdatePolicyType
private int mPolicyType;
private int mMaintenanceWindowStart;
private int mMaintenanceWindowEnd;
+ private final ArrayList<FreezeInterval> mFreezePeriods;
private SystemUpdatePolicy() {
mPolicyType = TYPE_UNKNOWN;
+ mFreezePeriods = new ArrayList<>();
}
/**
@@ -206,24 +355,129 @@ public class SystemUpdatePolicy implements Parcelable {
}
/**
- * Return if this object represents a valid policy.
+ * Return if this object represents a valid policy with:
+ * 1. Correct type
+ * 2. Valid maintenance window if applicable
+ * 3. Valid freeze periods
* @hide
*/
public boolean isValid() {
- if (mPolicyType == TYPE_INSTALL_AUTOMATIC || mPolicyType == TYPE_POSTPONE) {
+ try {
+ validateType();
+ validateFreezePeriods();
return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Validate the type and maintenance window (if applicable) of this policy object,
+ * throws {@link IllegalArgumentException} if it's invalid.
+ * @hide
+ */
+ public void validateType() {
+ if (mPolicyType == TYPE_INSTALL_AUTOMATIC || mPolicyType == TYPE_POSTPONE) {
+ return;
} else if (mPolicyType == TYPE_INSTALL_WINDOWED) {
- return mMaintenanceWindowStart >= 0 && mMaintenanceWindowStart < WINDOW_BOUNDARY
- && mMaintenanceWindowEnd >= 0 && mMaintenanceWindowEnd < WINDOW_BOUNDARY;
+ if (!(mMaintenanceWindowStart >= 0 && mMaintenanceWindowStart < WINDOW_BOUNDARY
+ && mMaintenanceWindowEnd >= 0 && mMaintenanceWindowEnd < WINDOW_BOUNDARY)) {
+ throw new IllegalArgumentException("Invalid maintenance window");
+ }
} else {
- return false;
+ throw new IllegalArgumentException("Invalid system update policy type.");
+ }
+ }
+
+ /**
+ * Configure a list of freeze periods on top of the current policy. When the device's clock is
+ * within any of the freeze periods, all incoming system updates including security patches will
+ * be blocked and cannot be installed. When the device is outside the freeze periods, the normal
+ * policy behavior will apply.
+ * <p>
+ * Each freeze period is defined by a starting and finishing date (both inclusive). Since the
+ * freeze period repeats annually, both of these dates are simply represented by integers
+ * counting the number of days since year start, similar to {@link LocalDate#getDayOfYear()}. We
+ * do not consider leap year when handling freeze period so the valid range of the integer is
+ * always [1,365] (see last section for more details on leap year). If the finishing date is
+ * smaller than the starting date, the freeze period is considered to be spanning across
+ * year-end.
+ * <p>
+ * Each individual freeze period is allowed to be at most 90 days long, and adjacent freeze
+ * periods need to be at least 60 days apart. Also, the list of freeze periods should not
+ * contain duplicates or overlap with each other. If any of these conditions is not met, a
+ * {@link ValidationFailedException} will be thrown.
+ * <p>
+ * Handling of leap year: we do not consider leap year when handling freeze period, in
+ * particular,
+ * <ul>
+ * <li>When a freeze period is defined by the day of year, February 29th does not count as one
+ * day, so day 59 is February 28th while day 60 is March 1st.</li>
+ * <li>When applying freeze period behavior to the device, a system clock of February 29th is
+ * treated as if it were February 28th</li>
+ * <li>When calculating the number of days of a freeze period or separation between two freeze
+ * periods, February 29th is also ignored and not counted as one day.</li>
+ * </ul>
+ *
+ * @param freezePeriods the list of freeze periods
+ * @throws ValidationFailedException if the supplied freeze periods do not meet the
+ * requirement set above
+ * @return this instance
+ */
+ public SystemUpdatePolicy setFreezePeriods(List<Pair<Integer, Integer>> freezePeriods) {
+ List<FreezeInterval> newPeriods = freezePeriods.stream().map(
+ p -> new FreezeInterval(p.first, p.second)).collect(Collectors.toList());
+ FreezeInterval.validatePeriods(newPeriods);
+ mFreezePeriods.clear();
+ mFreezePeriods.addAll(newPeriods);
+ return this;
+ }
+
+ /**
+ * Returns the list of freeze periods previously set on this system update policy object.
+ *
+ * @return the list of freeze periods, or an empty list if none was set.
+ */
+ public List<Pair<Integer, Integer>> getFreezePeriods() {
+ List<Pair<Integer, Integer>> result = new ArrayList<>(mFreezePeriods.size());
+ for (FreezeInterval interval : mFreezePeriods) {
+ result.add(new Pair<>(interval.mStartDay, interval.mEndDay));
}
+ return result;
+ }
+
+ /**
+ * Returns the real calendar dates of the current freeze period, or null if the device
+ * is not in a freeze period at the moment.
+ * @hide
+ */
+ public Pair<LocalDate, LocalDate> getCurrentFreezePeriod(LocalDate now) {
+ for (FreezeInterval interval : mFreezePeriods) {
+ if (interval.contains(now)) {
+ return interval.toCurrentOrFutureRealDates(now);
+ }
+ }
+ return null;
+ }
+
+ /** @hide */
+ public void validateFreezePeriods() {
+ FreezeInterval.validatePeriods(mFreezePeriods);
+ }
+
+ /** @hide */
+ public void validateAgainstPreviousFreezePeriod(LocalDate prevPeriodStart,
+ LocalDate prevPeriodEnd, LocalDate now) {
+ FreezeInterval.validateAgainstPreviousFreezePeriod(mFreezePeriods, prevPeriodStart,
+ prevPeriodEnd, now);
}
@Override
public String toString() {
- return String.format("SystemUpdatePolicy (type: %d, windowStart: %d, windowEnd: %d)",
- mPolicyType, mMaintenanceWindowStart, mMaintenanceWindowEnd);
+ return String.format("SystemUpdatePolicy (type: %d, windowStart: %d, windowEnd: %d, "
+ + "freezes: [%s])",
+ mPolicyType, mMaintenanceWindowStart, mMaintenanceWindowEnd,
+ mFreezePeriods.stream().map(n -> n.toString()).collect(Collectors.joining(",")));
}
@Override
@@ -236,6 +490,13 @@ public class SystemUpdatePolicy implements Parcelable {
dest.writeInt(mPolicyType);
dest.writeInt(mMaintenanceWindowStart);
dest.writeInt(mMaintenanceWindowEnd);
+ int freezeCount = mFreezePeriods.size();
+ dest.writeInt(freezeCount);
+ for (int i = 0; i < freezeCount; i++) {
+ FreezeInterval interval = mFreezePeriods.get(i);
+ dest.writeInt(interval.mStartDay);
+ dest.writeInt(interval.mEndDay);
+ }
}
public static final Parcelable.Creator<SystemUpdatePolicy> CREATOR =
@@ -247,6 +508,12 @@ public class SystemUpdatePolicy implements Parcelable {
policy.mPolicyType = source.readInt();
policy.mMaintenanceWindowStart = source.readInt();
policy.mMaintenanceWindowEnd = source.readInt();
+ int freezeCount = source.readInt();
+ policy.mFreezePeriods.ensureCapacity(freezeCount);
+ for (int i = 0; i < freezeCount; i++) {
+ policy.mFreezePeriods.add(
+ new FreezeInterval(source.readInt(), source.readInt()));
+ }
return policy;
}
@@ -256,8 +523,10 @@ public class SystemUpdatePolicy implements Parcelable {
}
};
-
/**
+ * Restore a previously saved SystemUpdatePolicy from XML. No need to validate
+ * the reconstructed policy since the XML is supposed to be created by the
+ * system server from a validated policy object previously.
* @hide
*/
public static SystemUpdatePolicy restoreFromXml(XmlPullParser parser) {
@@ -275,10 +544,26 @@ public class SystemUpdatePolicy implements Parcelable {
if (value != null) {
policy.mMaintenanceWindowEnd = Integer.parseInt(value);
}
+
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != END_DOCUMENT
+ && (type != END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == END_TAG || type == TEXT) {
+ continue;
+ }
+ if (!parser.getName().equals(KEY_FREEZE_TAG)) {
+ continue;
+ }
+ policy.mFreezePeriods.add(new FreezeInterval(
+ Integer.parseInt(parser.getAttributeValue(null, KEY_FREEZE_START)),
+ Integer.parseInt(parser.getAttributeValue(null, KEY_FREEZE_END))));
+ }
return policy;
}
- } catch (NumberFormatException e) {
+ } catch (NumberFormatException | XmlPullParserException | IOException e) {
// Fail through
+ Log.w(TAG, "Load xml failed", e);
}
return null;
}
@@ -290,6 +575,13 @@ public class SystemUpdatePolicy implements Parcelable {
out.attribute(null, KEY_POLICY_TYPE, Integer.toString(mPolicyType));
out.attribute(null, KEY_INSTALL_WINDOW_START, Integer.toString(mMaintenanceWindowStart));
out.attribute(null, KEY_INSTALL_WINDOW_END, Integer.toString(mMaintenanceWindowEnd));
+ for (int i = 0; i < mFreezePeriods.size(); i++) {
+ FreezeInterval interval = mFreezePeriods.get(i);
+ out.startTag(null, KEY_FREEZE_TAG);
+ out.attribute(null, KEY_FREEZE_START, Integer.toString(interval.mStartDay));
+ out.attribute(null, KEY_FREEZE_END, Integer.toString(interval.mEndDay));
+ out.endTag(null, KEY_FREEZE_TAG);
+ }
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
index a8e8237854cd..552942614db7 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
@@ -175,4 +175,7 @@ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub {
public boolean isOverrideApnEnabled(ComponentName admin) {
return false;
}
+
+ public void clearSystemUpdatePolicyFreezePeriodRecord() {
+ }
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 99712a5173db..d7a70d8f9a5d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -238,6 +238,7 @@ import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
+import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -725,7 +726,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
handlePackagesChanged(intent.getData().getSchemeSpecificPart(), userHandle);
} else if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(action)) {
clearWipeProfileNotification();
+ } else if (Intent.ACTION_DATE_CHANGED.equals(action)
+ || Intent.ACTION_TIME_CHANGED.equals(action)) {
+ // Update freeze period record when clock naturally progresses to the next day
+ // (ACTION_DATE_CHANGED), or when manual clock adjustment is made
+ // (ACTION_TIME_CHANGED)
+ updateSystemUpdateFreezePeriodsRecord(/* saveIfChanged */ true);
}
+
}
private void sendDeviceOwnerUserCommand(String action, int userHandle) {
@@ -2113,6 +2121,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler);
filter = new IntentFilter();
filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_DATE_CHANGED);
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler);
LocalServices.addService(DevicePolicyManagerInternal.class, mLocalService);
@@ -3353,6 +3363,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
deleteTransferOwnershipMetadataFileLocked();
deleteTransferOwnershipBundleLocked(metadata.userId);
}
+ updateSystemUpdateFreezePeriodsRecord(/* saveIfChanged */ true);
}
private void ensureDeviceOwnerUserStarted() {
@@ -10422,8 +10433,15 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void setSystemUpdatePolicy(ComponentName who, SystemUpdatePolicy policy) {
- if (policy != null && !policy.isValid()) {
- throw new IllegalArgumentException("Invalid system update policy.");
+ if (policy != null) {
+ // throws exception if policy type is invalid
+ policy.validateType();
+ // throws exception if freeze period is invalid
+ policy.validateFreezePeriods();
+ Pair<LocalDate, LocalDate> record = mOwners.getSystemUpdateFreezePeriodRecord();
+ // throws exception if freeze period is incompatible with previous freeze period record
+ policy.validateAgainstPreviousFreezePeriod(record.first, record.second,
+ LocalDate.now());
}
synchronized (this) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -10431,6 +10449,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mOwners.clearSystemUpdatePolicy();
} else {
mOwners.setSystemUpdatePolicy(policy);
+ updateSystemUpdateFreezePeriodsRecord(/* saveIfChanged */ false);
}
mOwners.writeDeviceOwner();
}
@@ -10451,6 +10470,83 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
+ private static boolean withinRange(Pair<LocalDate, LocalDate> range, LocalDate date) {
+ return (!date.isBefore(range.first) && !date.isAfter(range.second));
+ }
+
+ /**
+ * keeps track of the last continuous period when the system is under OTA freeze.
+ *
+ * DPMS keeps track of the previous dates during which OTA was freezed as a result of an
+ * system update policy with freeze periods in effect. This is needed to make robust
+ * validation on new system update polices, for example to prevent the OTA from being
+ * frozen for more than 90 days if the DPC keeps resetting a new 24-hour freeze period
+ * on midnight everyday, or having freeze periods closer than 60 days apart by DPC resetting
+ * a new freeze period after a few days.
+ *
+ * @param saveIfChanged whether to persist the result on disk if freeze period record is
+ * updated. This should only be set to {@code false} if there is a guaranteed
+ * mOwners.writeDeviceOwner() later in the control flow to reduce the number of
+ * disk writes. Otherwise you risk inconsistent on-disk state.
+ *
+ * @see SystemUpdatePolicy#validateAgainstPreviousFreezePeriod
+ */
+ private void updateSystemUpdateFreezePeriodsRecord(boolean saveIfChanged) {
+ Slog.d(LOG_TAG, "updateSystemUpdateFreezePeriodsRecord");
+ synchronized (this) {
+ final SystemUpdatePolicy policy = mOwners.getSystemUpdatePolicy();
+ if (policy == null) {
+ return;
+ }
+ final LocalDate now = LocalDate.now();
+ final Pair<LocalDate, LocalDate> currentPeriod = policy.getCurrentFreezePeriod(now);
+ if (currentPeriod == null) {
+ return;
+ }
+ final Pair<LocalDate, LocalDate> record = mOwners.getSystemUpdateFreezePeriodRecord();
+ final LocalDate start = record.first;
+ final LocalDate end = record.second;
+ final boolean changed;
+ if (end == null || start == null) {
+ // Start a new period if there is none at the moment
+ changed = mOwners.setSystemUpdateFreezePeriodRecord(now, now);
+ } else if (now.equals(end.plusDays(1))) {
+ // Extend the existing period
+ changed = mOwners.setSystemUpdateFreezePeriodRecord(start, now);
+ } else if (now.isAfter(end.plusDays(1))) {
+ if (withinRange(currentPeriod, start) && withinRange(currentPeriod, end)) {
+ // The device might be off for some period. If the past freeze record
+ // is within range of the current freeze period, assume the device was off
+ // during the period [end, now] and extend the freeze record to [start, now].
+ changed = mOwners.setSystemUpdateFreezePeriodRecord(start, now);
+ } else {
+ changed = mOwners.setSystemUpdateFreezePeriodRecord(now, now);
+ }
+ } else if (now.isBefore(start)) {
+ // Systm clock was adjusted backwards, restart record
+ changed = mOwners.setSystemUpdateFreezePeriodRecord(now, now);
+ } else /* start <= now <= end */ {
+ changed = false;
+ }
+ if (changed && saveIfChanged) {
+ mOwners.writeDeviceOwner();
+ }
+ }
+ }
+
+ @Override
+ public void clearSystemUpdatePolicyFreezePeriodRecord() {
+ enforceShell("clearSystemUpdatePolicyFreezePeriodRecord");
+ synchronized (this) {
+ // Print out current record to help diagnosed CTS failures
+ Slog.i(LOG_TAG, "Clear freeze period record: "
+ + mOwners.getSystemUpdateFreezePeriodRecordAsString());
+ if (mOwners.setSystemUpdateFreezePeriodRecord(null, null)) {
+ mOwners.writeDeviceOwner();
+ }
+ }
+ }
+
/**
* Checks if the caller of the method is the device owner app.
*
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
index d2151ed8ae5e..0268519795ba 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
@@ -47,6 +47,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -78,6 +79,7 @@ class Owners {
private static final String TAG_DEVICE_OWNER = "device-owner";
private static final String TAG_DEVICE_INITIALIZER = "device-initializer";
private static final String TAG_SYSTEM_UPDATE_POLICY = "system-update-policy";
+ private static final String TAG_FREEZE_PERIOD_RECORD = "freeze-record";
private static final String TAG_PENDING_OTA_INFO = "pending-ota-info";
private static final String TAG_PROFILE_OWNER = "profile-owner";
// Holds "context" for device-owner, this must not be show up before device-owner.
@@ -90,6 +92,8 @@ class Owners {
private static final String ATTR_REMOTE_BUGREPORT_HASH = "remoteBugreportHash";
private static final String ATTR_USERID = "userId";
private static final String ATTR_USER_RESTRICTIONS_MIGRATED = "userRestrictionsMigrated";
+ private static final String ATTR_FREEZE_RECORD_START = "start";
+ private static final String ATTR_FREEZE_RECORD_END = "end";
private final UserManager mUserManager;
private final UserManagerInternal mUserManagerInternal;
@@ -105,6 +109,8 @@ class Owners {
// Local system update policy controllable by device owner.
private SystemUpdatePolicy mSystemUpdatePolicy;
+ private LocalDate mSystemUpdateFreezeStart;
+ private LocalDate mSystemUpdateFreezeEnd;
// Pending OTA info if there is one.
@Nullable
@@ -355,6 +361,47 @@ class Owners {
}
}
+ Pair<LocalDate, LocalDate> getSystemUpdateFreezePeriodRecord() {
+ synchronized (mLock) {
+ return new Pair<>(mSystemUpdateFreezeStart, mSystemUpdateFreezeEnd);
+ }
+ }
+
+ String getSystemUpdateFreezePeriodRecordAsString() {
+ StringBuilder freezePeriodRecord = new StringBuilder();
+ freezePeriodRecord.append("start: ");
+ if (mSystemUpdateFreezeStart != null) {
+ freezePeriodRecord.append(mSystemUpdateFreezeStart.toString());
+ } else {
+ freezePeriodRecord.append("null");
+ }
+ freezePeriodRecord.append("; end: ");
+ if (mSystemUpdateFreezeEnd != null) {
+ freezePeriodRecord.append(mSystemUpdateFreezeEnd.toString());
+ } else {
+ freezePeriodRecord.append("null");
+ }
+ return freezePeriodRecord.toString();
+ }
+
+ /**
+ * Returns {@code true} if the freeze period record is changed, {@code false} otherwise.
+ */
+ boolean setSystemUpdateFreezePeriodRecord(LocalDate start, LocalDate end) {
+ boolean changed = false;
+ synchronized (mLock) {
+ if (!Objects.equals(mSystemUpdateFreezeStart, start)) {
+ mSystemUpdateFreezeStart = start;
+ changed = true;
+ }
+ if (!Objects.equals(mSystemUpdateFreezeEnd, end)) {
+ mSystemUpdateFreezeEnd = end;
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
boolean hasDeviceOwner() {
synchronized (mLock) {
return mDeviceOwner != null;
@@ -676,9 +723,16 @@ class Owners {
mSystemUpdatePolicy.saveToXml(out);
out.endTag(null, TAG_SYSTEM_UPDATE_POLICY);
}
-
- if (mSystemUpdateInfo != null) {
- mSystemUpdateInfo.writeToXml(out, TAG_PENDING_OTA_INFO);
+ if (mSystemUpdateFreezeStart != null || mSystemUpdateFreezeEnd != null) {
+ out.startTag(null, TAG_FREEZE_PERIOD_RECORD);
+ if (mSystemUpdateFreezeStart != null) {
+ out.attribute(null, ATTR_FREEZE_RECORD_START,
+ mSystemUpdateFreezeStart.toString());
+ }
+ if (mSystemUpdateFreezeEnd != null) {
+ out.attribute(null, ATTR_FREEZE_RECORD_END, mSystemUpdateFreezeEnd.toString());
+ }
+ out.endTag(null, TAG_FREEZE_PERIOD_RECORD);
}
}
@@ -711,6 +765,19 @@ class Owners {
case TAG_PENDING_OTA_INFO:
mSystemUpdateInfo = SystemUpdateInfo.readFromXml(parser);
break;
+ case TAG_FREEZE_PERIOD_RECORD:
+ String startDate = parser.getAttributeValue(null, ATTR_FREEZE_RECORD_START);
+ String endDate = parser.getAttributeValue(null, ATTR_FREEZE_RECORD_END);
+ if (startDate != null && endDate != null) {
+ mSystemUpdateFreezeStart = LocalDate.parse(startDate);
+ mSystemUpdateFreezeEnd = LocalDate.parse(endDate);
+ if (mSystemUpdateFreezeStart.isAfter(mSystemUpdateFreezeEnd)) {
+ Slog.e(TAG, "Invalid system update freeze record loaded");
+ mSystemUpdateFreezeStart = null;
+ mSystemUpdateFreezeEnd = null;
+ }
+ }
+ break;
default:
Slog.e(TAG, "Unexpected tag: " + tag);
return false;
@@ -879,6 +946,14 @@ class Owners {
pw.println(prefix + "Pending System Update: " + mSystemUpdateInfo);
needBlank = true;
}
+ if (mSystemUpdateFreezeStart != null || mSystemUpdateFreezeEnd != null) {
+ if (needBlank) {
+ pw.println();
+ }
+ pw.println(prefix + "System update freeze record: "
+ + getSystemUpdateFreezePeriodRecordAsString());
+ needBlank = true;
+ }
}
@VisibleForTesting
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java
new file mode 100644
index 000000000000..98c428dccf7a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java
@@ -0,0 +1,371 @@
+/*
+ * 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 com.android.server.devicepolicy;
+
+import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE;
+import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG;
+import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_DUPLICATE_OR_OVERLAP;
+import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE;
+import static android.app.admin.SystemUpdatePolicy.ValidationFailedException.ERROR_NEW_FREEZE_PERIOD_TOO_LONG;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.admin.FreezeInterval;
+import android.app.admin.SystemUpdatePolicy;
+import android.os.Parcel;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+import android.util.Xml;
+
+import com.android.internal.util.FastXmlSerializer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link android.app.admin.SystemUpdatePolicy}.
+ * Throughout this test, we use "MM-DD" format to denote dates without year.
+ *
+ * atest com.android.server.devicepolicy.SystemUpdatePolicyTest
+ * runtest -c com.android.server.devicepolicy.SystemUpdatePolicyTest frameworks-services
+ */
+@RunWith(AndroidJUnit4.class)
+public final class SystemUpdatePolicyTest {
+
+ private static final int DUPLICATE_OR_OVERLAP = ERROR_DUPLICATE_OR_OVERLAP;
+ private static final int TOO_LONG = ERROR_NEW_FREEZE_PERIOD_TOO_LONG;
+ private static final int TOO_CLOSE = ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE;
+ private static final int COMBINED_TOO_LONG = ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG;
+ private static final int COMBINED_TOO_CLOSE = ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE;
+
+ @Test
+ public void testSimplePeriod() throws Exception {
+ testFreezePeriodsSucceeds("01-01", "01-02");
+ testFreezePeriodsSucceeds("01-31", "01-31");
+ testFreezePeriodsSucceeds("11-01", "01-15");
+ testFreezePeriodsSucceeds("02-01", "02-29"); // Leap year
+ testFreezePeriodsSucceeds("02-01", "03-01");
+ testFreezePeriodsSucceeds("12-01", "01-30"); // Wrapped Period
+ testFreezePeriodsSucceeds("11-02", "01-30", "04-01", "04-30"); // Wrapped Period
+ }
+
+ @Test
+ public void testCanonicalizationValidation() throws Exception {
+ testFreezePeriodsSucceeds("03-01", "03-31", "09-01", "09-30");
+ testFreezePeriodsSucceeds("06-01", "07-01", "09-01", "09-30");
+ testFreezePeriodsSucceeds("10-01", "10-31", "12-31", "01-31");
+ testFreezePeriodsSucceeds("01-01", "01-30", "04-01", "04-30");
+ testFreezePeriodsSucceeds("01-01", "02-28", "05-01", "06-30", "09-01", "10-31");
+
+ // One interval fully covers the other
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "03-01", "03-31", "03-15", "03-31");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "03-01", "03-31", "03-15", "03-16");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "11-15", "01-31", "12-01", "12-31");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "01-31", "01-01", "01-15");
+
+ // Partial overlap
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "03-01", "03-31", "03-15", "01-01");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "11-15", "01-31", "12-01", "02-28");
+
+ // No gap between two intervals
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "01-31", "01-31", "02-01", "02-01");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "12-15", "12-15", "02-01");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "12-15", "12-16", "02-01");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "01-15", "01-15", "02-01");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "01-15", "01-16", "02-01");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "01-01", "01-30", "12-01", "12-31");
+ testFreezePeriodsFails(DUPLICATE_OR_OVERLAP, "12-01", "12-31", "04-01", "04-01",
+ "01-01", "01-30");
+ }
+
+ @Test
+ public void testLengthValidation() throws Exception {
+ testFreezePeriodsSucceeds("03-01", "03-31");
+ testFreezePeriodsSucceeds("03-03", "03-03", "12-31", "01-01");
+ testFreezePeriodsSucceeds("01-01", "03-31", "06-01", "08-29");
+ // entire year
+ testFreezePeriodsFails(TOO_LONG, "01-01", "12-31");
+ // long period spanning across year end
+ testFreezePeriodsSucceeds("11-01", "01-29");
+ testFreezePeriodsFails(TOO_LONG, "11-01", "01-30");
+ // Leap year handling
+ testFreezePeriodsSucceeds("12-01", "02-28");
+ testFreezePeriodsSucceeds("12-01", "02-29");
+ testFreezePeriodsFails(TOO_LONG, "12-01", "03-01");
+ // Regular long period
+ testFreezePeriodsSucceeds("01-01", "03-31", "06-01", "08-29");
+ testFreezePeriodsFails(TOO_LONG, "01-01", "03-31", "06-01", "08-30");
+ }
+
+ @Test
+ public void testSeparationValidation() throws Exception {
+ testFreezePeriodsSucceeds("01-01", "03-31", "06-01", "08-29");
+ testFreezePeriodsFails(TOO_CLOSE, "01-01", "01-01", "01-03", "01-03");
+ testFreezePeriodsFails(TOO_CLOSE, "03-01", "03-31", "05-01", "05-31");
+ // Short interval spans across end of year
+ testFreezePeriodsSucceeds("01-31", "03-01", "11-01", "12-01");
+ testFreezePeriodsFails(TOO_CLOSE, "01-30", "03-01", "11-01", "12-01");
+ // Short separation is after wrapped period
+ testFreezePeriodsSucceeds("03-03", "03-31", "12-31", "01-01");
+ testFreezePeriodsFails(TOO_CLOSE, "03-02", "03-31", "12-31", "01-01");
+ // Short separation including Feb 29
+ testFreezePeriodsSucceeds("12-01", "01-15", "03-17", "04-01");
+ testFreezePeriodsFails(TOO_CLOSE, "12-01", "01-15", "03-16", "04-01");
+ // Short separation including Feb 29
+ testFreezePeriodsSucceeds("01-01", "02-28", "04-30", "06-01");
+ testFreezePeriodsSucceeds("01-01", "02-29", "04-30", "06-01");
+ testFreezePeriodsFails(TOO_CLOSE, "01-01", "03-01", "04-30", "06-01");
+ }
+
+ @Test
+ public void testValidateTotalLengthWithPreviousPeriods() throws Exception {
+ testPrevFreezePeriodSucceeds("2018-01-19", "2018-01-19", /* now */"2018-01-19",
+ "07-01", "07-31", "10-01", "11-30");
+ testPrevFreezePeriodSucceeds("2018-01-01", "2018-01-19", /* now */"2018-01-19",
+ "01-01", "03-30");
+ testPrevFreezePeriodSucceeds("2018-01-01", "2018-02-01", /* now */"2018-02-01",
+ "11-01", "12-31");
+
+ testPrevFreezePeriodSucceeds("2017-11-01", "2018-01-02", /* now */"2018-01-02",
+ "01-01", "01-29");
+ testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-11-01", "2018-01-02", "2018-01-02",
+ "01-01", "01-30");
+ testPrevFreezePeriodSucceeds("2017-11-01", "2018-01-02", /* now */"2018-01-01",
+ "01-02", "01-29");
+ testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-11-01", "2018-01-02", "2018-01-01",
+ "01-02", "01-30");
+
+ testPrevFreezePeriodSucceeds("2017-11-01", "2017-12-01", /* now */"2017-12-01",
+ "11-15", "01-29");
+ testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-11-01", "2017-12-01", "2017-12-01",
+ "11-15", "01-30");
+
+ testPrevFreezePeriodSucceeds("2017-11-01", "2018-01-01", /* now */"2018-01-01",
+ "11-15", "01-29");
+ testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-11-01", "2018-01-01", "2018-01-01",
+ "11-15", "01-30");
+
+ testPrevFreezePeriodSucceeds("2018-03-01", "2018-03-31", /* now */"2018-03-31",
+ "04-01", "05-29");
+ testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2018-03-01", "2018-03-31", "2018-03-31",
+ "04-01", "05-30");
+
+ // Leap year handing
+ testPrevFreezePeriodSucceeds("2017-12-01", "2018-01-02", /* now */"2018-01-02",
+ "01-01", "02-28");
+ testPrevFreezePeriodSucceeds("2017-12-01", "2018-01-02", /* now */"2018-01-02",
+ "01-01", "02-29");
+ testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2017-12-01", "2018-01-02", "2018-01-02",
+ "01-01", "03-01");
+
+ testPrevFreezePeriodSucceeds("2016-01-01", "2016-02-28", /* now */"2016-02-28",
+ "02-01", "03-31");
+ testPrevFreezePeriodSucceeds("2016-01-01", "2016-02-28", /* now */"2016-02-29",
+ "02-01", "03-31");
+ testPrevFreezePeriodFails(COMBINED_TOO_LONG, "2016-01-01", "2016-02-28", "2016-02-29",
+ "02-01", "04-01");
+
+ }
+
+ @Test
+ public void testValidateSeparationWithPreviousPeriods() throws Exception {
+ testPrevFreezePeriodSucceeds("2018-01-01", "2018-01-02", /* now */"2018-03-04",
+ "01-01", "03-30");
+ testPrevFreezePeriodSucceeds("2018-01-01", "2018-01-02", /* now */"2018-01-19",
+ "04-01", "06-29");
+ testPrevFreezePeriodSucceeds("2017-01-01", "2017-03-30", /* now */"2018-12-01",
+ "01-01", "03-30");
+
+ testPrevFreezePeriodSucceeds("2018-01-01", "2018-02-01", "2018-02-01",
+ "04-03", "06-01");
+ testPrevFreezePeriodFails(COMBINED_TOO_CLOSE, "2018-01-01", "2018-02-01", "2018-02-01",
+ "04-02", "06-01");
+
+ testPrevFreezePeriodSucceeds("2018-04-01", "2018-06-01", "2018-08-01",
+ "07-01", "08-30");
+ testPrevFreezePeriodFails(COMBINED_TOO_CLOSE, "2018-04-01", "2018-06-01", "2018-07-30",
+ "07-01", "08-30");
+
+
+ testPrevFreezePeriodSucceeds("2018-03-01", "2018-04-01", "2018-06-01",
+ "05-01", "07-01");
+ testPrevFreezePeriodFails(COMBINED_TOO_CLOSE, "2018-03-01", "2018-04-01", "2018-05-31",
+ "05-01", "07-01");
+ }
+
+ @Test
+ public void testDistanceWithoutLeapYear() {
+ assertEquals(364, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2016, 12, 31), LocalDate.of(2016, 1, 1)));
+ assertEquals(365, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2017, 1, 1), LocalDate.of(2016, 1, 1)));
+ assertEquals(365, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2017, 2, 28), LocalDate.of(2016, 2, 29)));
+ assertEquals(-365, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2016, 1, 1), LocalDate.of(2017, 1, 1)));
+ assertEquals(1, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2016, 3, 1), LocalDate.of(2016, 2, 29)));
+ assertEquals(1, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2016, 3, 1), LocalDate.of(2016, 2, 28)));
+ assertEquals(0, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2016, 2, 29), LocalDate.of(2016, 2, 28)));
+ assertEquals(0, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2016, 2, 28), LocalDate.of(2016, 2, 28)));
+
+ assertEquals(59, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2016, 3, 1), LocalDate.of(2016, 1, 1)));
+ assertEquals(59, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2017, 3, 1), LocalDate.of(2017, 1, 1)));
+
+ assertEquals(365 * 40, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2040, 1, 1), LocalDate.of(2000, 1, 1)));
+
+ assertEquals(365 * 2, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2019, 3, 1), LocalDate.of(2017, 3, 1)));
+ assertEquals(365 * 2, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2018, 3, 1), LocalDate.of(2016, 3, 1)));
+ assertEquals(365 * 2, FreezeInterval.distanceWithoutLeapYear(
+ LocalDate.of(2017, 3, 1), LocalDate.of(2015, 3, 1)));
+
+ }
+
+ private void testFreezePeriodsSucceeds(String...dates) throws Exception {
+ SystemUpdatePolicy p = SystemUpdatePolicy.createPostponeInstallPolicy();
+ setFreezePeriods(p, dates);
+ }
+
+ private void testFreezePeriodsFails(int expectedError, String... dates) throws Exception {
+ SystemUpdatePolicy p = SystemUpdatePolicy.createPostponeInstallPolicy();
+ try {
+ setFreezePeriods(p, dates);
+ fail("Invalid periods (" + expectedError + ") not flagged: " + String.join(" ", dates));
+ } catch (SystemUpdatePolicy.ValidationFailedException e) {
+ assertTrue("Exception not expected: " + e.getMessage(),
+ e.getErrorCode() == expectedError);
+ }
+ }
+
+ private void testPrevFreezePeriodSucceeds(String prevStart, String prevEnd, String now,
+ String... dates) throws Exception {
+ createPrevFreezePeriod(prevStart, prevEnd, now, dates);
+ }
+
+ private void testPrevFreezePeriodFails(int expectedError, String prevStart, String prevEnd,
+ String now, String... dates) throws Exception {
+ try {
+ createPrevFreezePeriod(prevStart, prevEnd, now, dates);
+ fail("Invalid period (" + expectedError + ") not flagged: " + String.join(" ", dates));
+ } catch (SystemUpdatePolicy.ValidationFailedException e) {
+ assertTrue("Exception not expected: " + e.getMessage(),
+ e.getErrorCode() == expectedError);
+ }
+ }
+
+ private void createPrevFreezePeriod(String prevStart, String prevEnd, String now,
+ String... dates) throws Exception {
+ SystemUpdatePolicy p = SystemUpdatePolicy.createPostponeInstallPolicy();
+ setFreezePeriods(p, dates);
+ p.validateAgainstPreviousFreezePeriod(parseDate(prevStart), parseDate(prevEnd),
+ parseDate(now));
+ }
+
+ // "MM-DD" format for date
+ private void setFreezePeriods(SystemUpdatePolicy policy, String... dates) throws Exception {
+ List<Pair<Integer, Integer>> periods = new ArrayList<>();
+ LocalDate lastDate = null;
+ for (String date : dates) {
+ LocalDate currentDate = parseDate(date);
+ if (lastDate != null) {
+ periods.add(new Pair<>(lastDate.getDayOfYear(), currentDate.getDayOfYear()));
+ lastDate = null;
+ } else {
+ lastDate = currentDate;
+ }
+ }
+ policy.setFreezePeriods(periods);
+ testSerialization(policy, periods);
+ }
+
+ private void testSerialization(SystemUpdatePolicy policy,
+ List<Pair<Integer, Integer>> expectedPeriods) throws Exception {
+ // Test parcel / unparcel
+ Parcel parcel = Parcel.obtain();
+ policy.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ SystemUpdatePolicy q = SystemUpdatePolicy.CREATOR.createFromParcel(parcel);
+ checkFreezePeriods(q, expectedPeriods);
+ parcel.recycle();
+
+ // Test XML serialization
+ ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+ final XmlSerializer outXml = new FastXmlSerializer();
+ outXml.setOutput(outStream, StandardCharsets.UTF_8.name());
+ outXml.startDocument(null, true);
+ outXml.startTag(null, "ota");
+ policy.saveToXml(outXml);
+ outXml.endTag(null, "ota");
+ outXml.endDocument();
+ outXml.flush();
+
+ ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray());
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(new InputStreamReader(inStream));
+ assertEquals(XmlPullParser.START_TAG, parser.next());
+ checkFreezePeriods(SystemUpdatePolicy.restoreFromXml(parser), expectedPeriods);
+ }
+
+ private void checkFreezePeriods(SystemUpdatePolicy policy,
+ List<Pair<Integer, Integer>> expectedPeriods) {
+ int i = 0;
+ for (Pair<Integer, Integer> period : policy.getFreezePeriods()) {
+ assertEquals(expectedPeriods.get(i).first, period.first);
+ assertEquals(expectedPeriods.get(i).second, period.second);
+ i++;
+ }
+ }
+
+ private LocalDate parseDate(String date) {
+ // Use leap year when parsing date string to handle "02-29", but force round down
+ // to Feb 28th by overriding the year to non-leap year.
+ final int year;
+ boolean monthDateOnly = false;
+ if (date.length() == 5) {
+ year = 2000;
+ monthDateOnly = true;
+ } else {
+ year = Integer.parseInt(date.substring(0, 4));
+ date = date.substring(5);
+ }
+ LocalDate result = LocalDate.of(year, Integer.parseInt(date.substring(0, 2)),
+ Integer.parseInt(date.substring(3, 5)));
+ if (monthDateOnly) {
+ return result.withYear(2001);
+ } else {
+ return result;
+ }
+ }
+}