diff options
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; + } + } +} |