Persist delayed calls to factory reset...

...so the device is factory reset on reboot if necessary.

NOTE: this is same as 23bcae6f5bd8c08d6315dbaba7b9ab34f1c1bac9, but
without the extra, accidental parser.next() line that broke stuff.

Test: atest FrameworksServicesTests:DevicePolicyManagerTest
Test: atest FactoryResetterTest
Test: manual verification

Bug: 171603586
Bug: 177391800
Change-Id: Ic18ee40b04eaf68c9da278970c9586cca93eef9d
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
index 5e7f984..ba3ae45 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
@@ -16,6 +16,7 @@
 
 package com.android.server.devicepolicy;
 
+import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.app.admin.DeviceAdminInfo;
 import android.app.admin.DevicePolicyManager;
@@ -24,6 +25,7 @@
 import android.os.PersistableBundle;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.DebugUtils;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 import android.util.TypedXmlPullParser;
@@ -85,6 +87,15 @@
     static final String NEW_USER_DISCLAIMER_NOT_NEEDED = "not_needed";
     static final String NEW_USER_DISCLAIMER_NEEDED = "needed";
 
+    private static final String ATTR_FACTORY_RESET_FLAGS = "factory-reset-flags";
+    private static final String ATTR_FACTORY_RESET_REASON = "factory-reset-reason";
+
+    // NOTE: must be public because of DebugUtils.flagsToString()
+    public static final int FACTORY_RESET_FLAG_ON_BOOT = 1;
+    public static final int FACTORY_RESET_FLAG_WIPE_EXTERNAL_STORAGE = 2;
+    public static final int FACTORY_RESET_FLAG_WIPE_EUICC = 4;
+    public static final int FACTORY_RESET_FLAG_WIPE_FACTORY_RESET_PROTECTION = 8;
+
     private static final String TAG = DevicePolicyManagerService.LOG_TAG;
     private static final boolean VERBOSE_LOG = false; // DO NOT SUBMIT WITH TRUE
 
@@ -99,6 +110,9 @@
     int mUserProvisioningState;
     int mPermissionPolicy;
 
+    int mFactoryResetFlags;
+    String mFactoryResetReason;
+
     boolean mDeviceProvisioningConfigApplied = false;
 
     final ArrayMap<ComponentName, ActiveAdmin> mAdminMap = new ArrayMap<>();
@@ -200,6 +214,17 @@
                 out.attribute(null, ATTR_NEW_USER_DISCLAIMER, policyData.mNewUserDisclaimer);
             }
 
+            if (policyData.mFactoryResetFlags != 0) {
+                if (VERBOSE_LOG) {
+                    Slog.v(TAG, "Storing factory reset flags for user " + policyData.mUserId + ": "
+                            + factoryResetFlagsToString(policyData.mFactoryResetFlags));
+                }
+                out.attributeInt(null, ATTR_FACTORY_RESET_FLAGS, policyData.mFactoryResetFlags);
+            }
+            if (policyData.mFactoryResetReason != null) {
+                out.attribute(null, ATTR_FACTORY_RESET_REASON, policyData.mFactoryResetReason);
+            }
+
             // Serialize delegations.
             for (int i = 0; i < policyData.mDelegationMap.size(); ++i) {
                 final String delegatePackage = policyData.mDelegationMap.keyAt(i);
@@ -427,6 +452,13 @@
             }
             policy.mNewUserDisclaimer = parser.getAttributeValue(null, ATTR_NEW_USER_DISCLAIMER);
 
+            policy.mFactoryResetFlags = parser.getAttributeInt(null, ATTR_FACTORY_RESET_FLAGS, 0);
+            if (VERBOSE_LOG) {
+                Slog.v(TAG, "Restored factory reset flags for user " + policy.mUserId + ": "
+                        + factoryResetFlagsToString(policy.mFactoryResetFlags));
+            }
+            policy.mFactoryResetReason = parser.getAttributeValue(null, ATTR_FACTORY_RESET_REASON);
+
             int outerDepth = parser.getDepth();
             policy.mLockTaskPackages.clear();
             policy.mAdminList.clear();
@@ -572,6 +604,22 @@
         }
     }
 
+    void setDelayedFactoryReset(@NonNull String reason, boolean wipeExtRequested, boolean wipeEuicc,
+            boolean wipeResetProtectionData) {
+        mFactoryResetReason = reason;
+
+        mFactoryResetFlags = FACTORY_RESET_FLAG_ON_BOOT;
+        if (wipeExtRequested) {
+            mFactoryResetFlags |= FACTORY_RESET_FLAG_WIPE_EXTERNAL_STORAGE;
+        }
+        if (wipeEuicc) {
+            mFactoryResetFlags |= FACTORY_RESET_FLAG_WIPE_EUICC;
+        }
+        if (wipeResetProtectionData) {
+            mFactoryResetFlags |= FACTORY_RESET_FLAG_WIPE_FACTORY_RESET_PROTECTION;
+        }
+    }
+
     void dump(IndentingPrintWriter pw) {
         pw.println();
         pw.println("Enabled Device Admins (User " + mUserId + ", provisioningState: "
@@ -603,6 +651,19 @@
         pw.print("mUserSetupComplete="); pw.println(mUserSetupComplete);
         pw.print("mAffiliationIds="); pw.println(mAffiliationIds);
         pw.print("mNewUserDisclaimer="); pw.println(mNewUserDisclaimer);
+        if (mFactoryResetFlags != 0) {
+            pw.print("mFactoryResetFlags="); pw.print(mFactoryResetFlags);
+            pw.print(" (");
+            pw.print(factoryResetFlagsToString(mFactoryResetFlags));
+            pw.println(')');
+        }
+        if (mFactoryResetReason != null) {
+            pw.print("mFactoryResetReason="); pw.println(mFactoryResetReason);
+        }
         pw.decreaseIndent();
     }
+
+    static String factoryResetFlagsToString(int flags) {
+        return DebugUtils.flagsToString(DevicePolicyData.class, "FACTORY_RESET_FLAG_", flags);
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 8b575fb..83c5870 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1317,12 +1317,11 @@
             mContext.getSystemService(PowerManager.class).reboot(reason);
         }
 
-        void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force,
+        boolean recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force,
                 boolean wipeEuicc, boolean wipeExtRequested, boolean wipeResetProtectionData)
                         throws IOException {
-            FactoryResetter.newBuilder(mContext).setSafetyChecker(mSafetyChecker)
-                    .setReason(reason).setShutdown(shutdown)
-                    .setForce(force).setWipeEuicc(wipeEuicc)
+            return FactoryResetter.newBuilder(mContext).setSafetyChecker(mSafetyChecker)
+                    .setReason(reason).setShutdown(shutdown).setForce(force).setWipeEuicc(wipeEuicc)
                     .setWipeAdoptableStorage(wipeExtRequested)
                     .setWipeFactoryResetProtection(wipeResetProtectionData)
                     .build().factoryReset();
@@ -2784,6 +2783,10 @@
                 maybeStartSecurityLogMonitorOnActivityManagerReady();
                 break;
             case SystemService.PHASE_BOOT_COMPLETED:
+                // Ideally it should be done earlier, but currently it relies on RecoverySystem,
+                // which would hang on earlier phases
+                factoryResetIfDelayedEarlier();
+
                 ensureDeviceOwnerUserStarted(); // TODO Consider better place to do this.
                 break;
         }
@@ -6217,10 +6220,24 @@
             boolean wipeResetProtectionData) {
         wtfIfInLock();
         boolean success = false;
+
         try {
-            mInjector.recoverySystemRebootWipeUserData(
+            boolean delayed = !mInjector.recoverySystemRebootWipeUserData(
                     /* shutdown= */ false, reason, /* force= */ true, /* wipeEuicc= */ wipeEuicc,
                     wipeExtRequested, wipeResetProtectionData);
+            if (delayed) {
+                // Persist the request so the device is automatically factory-reset on next start if
+                // the system crashes or reboots before the {@code DevicePolicySafetyChecker} calls
+                // its callback.
+                Slog.i(LOG_TAG, String.format("Persisting factory reset request as it could be "
+                        + "delayed by %s", mSafetyChecker));
+                synchronized (getLockObject()) {
+                    DevicePolicyData policy = getUserData(UserHandle.USER_SYSTEM);
+                    policy.setDelayedFactoryReset(reason, wipeExtRequested, wipeEuicc,
+                            wipeResetProtectionData);
+                    saveSettingsLocked(UserHandle.USER_SYSTEM);
+                }
+            }
             success = true;
         } catch (IOException | SecurityException e) {
             Slog.w(LOG_TAG, "Failed requesting data wipe", e);
@@ -6229,6 +6246,40 @@
         }
     }
 
+    private void factoryResetIfDelayedEarlier() {
+        synchronized (getLockObject()) {
+            DevicePolicyData policy = getUserData(UserHandle.USER_SYSTEM);
+
+            if (policy.mFactoryResetFlags == 0) return;
+
+            if (policy.mFactoryResetReason == null) {
+                // Shouldn't happen.
+                Slog.e(LOG_TAG, "no persisted reason for factory resetting");
+                policy.mFactoryResetReason = "requested before boot";
+            }
+            FactoryResetter factoryResetter = FactoryResetter.newBuilder(mContext)
+                    .setReason(policy.mFactoryResetReason).setForce(true)
+                    .setWipeEuicc((policy.mFactoryResetFlags & DevicePolicyData
+                            .FACTORY_RESET_FLAG_WIPE_EUICC) != 0)
+                    .setWipeAdoptableStorage((policy.mFactoryResetFlags & DevicePolicyData
+                            .FACTORY_RESET_FLAG_WIPE_EXTERNAL_STORAGE) != 0)
+                    .setWipeFactoryResetProtection((policy.mFactoryResetFlags & DevicePolicyData
+                            .FACTORY_RESET_FLAG_WIPE_FACTORY_RESET_PROTECTION) != 0)
+                    .build();
+            Slog.i(LOG_TAG, "Factory resetting on boot using " + factoryResetter);
+            try {
+                if (!factoryResetter.factoryReset()) {
+                    // Shouldn't happen because FactoryResetter was created without a
+                    // DevicePolicySafetyChecker.
+                    Slog.wtf(LOG_TAG, "Factory reset using " + factoryResetter + " failed.");
+                }
+            } catch (IOException e) {
+                // Shouldn't happen.
+                Slog.wtf(LOG_TAG, "Could not factory reset using " + factoryResetter, e);
+            }
+        }
+    }
+
     private void forceWipeUser(int userId, String wipeReasonForUser, boolean wipeSilently) {
         boolean success = false;
         try {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/FactoryResetter.java b/services/devicepolicy/java/com/android/server/devicepolicy/FactoryResetter.java
index 564950b..457255b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/FactoryResetter.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/FactoryResetter.java
@@ -52,14 +52,17 @@
 
     /**
      * Factory reset the device according to the builder's arguments.
+     *
+     * @return {@code true} if device was factory reset, or {@code false} if it was delayed by the
+     * {@link DevicePolicySafetyChecker}.
      */
-    public void factoryReset() throws IOException {
+    public boolean factoryReset() throws IOException {
         Preconditions.checkCallAuthorization(mContext.checkCallingOrSelfPermission(
                 android.Manifest.permission.MASTER_CLEAR) == PackageManager.PERMISSION_GRANTED);
 
         if (mSafetyChecker == null) {
             factoryResetInternalUnchecked();
-            return;
+            return true;
         }
 
         IResultReceiver receiver = new IResultReceiver.Stub() {
@@ -77,6 +80,36 @@
         };
         Slog.i(TAG, String.format("Delaying factory reset until %s confirms", mSafetyChecker));
         mSafetyChecker.onFactoryReset(receiver);
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder("FactoryResetter[");
+        if (mReason == null) {
+            builder.append("no_reason");
+        } else {
+            builder.append("reason='").append(mReason).append("'");
+        }
+        if (mSafetyChecker != null) {
+            builder.append(",hasSafetyChecker");
+        }
+        if (mShutdown) {
+            builder.append(",shutdown");
+        }
+        if (mForce) {
+            builder.append(",force");
+        }
+        if (mWipeEuicc) {
+            builder.append(",wipeEuicc");
+        }
+        if (mWipeAdoptableStorage) {
+            builder.append(",wipeAdoptableStorage");
+        }
+        if (mWipeFactoryResetProtection) {
+            builder.append(",ipeFactoryResetProtection");
+        }
+        return builder.append(']').toString();
     }
 
     private void factoryResetInternalUnchecked() throws IOException {
diff --git a/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java b/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java
index c4d14f9..589a349 100644
--- a/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java
@@ -20,6 +20,8 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.never;
@@ -158,10 +160,11 @@
     public void testFactoryReset_storageOnly() throws Exception {
         allowFactoryReset();
 
-        FactoryResetter.newBuilder(mContext)
+        boolean success = FactoryResetter.newBuilder(mContext)
                 .setWipeAdoptableStorage(true).build()
                 .factoryReset();
 
+        assertThat(success).isTrue();
         verifyWipeAdoptableStorageCalled();
         verifyWipeFactoryResetProtectionNotCalled();
         verifyRebootWipeUserDataMinimumArgsCalled();
@@ -171,10 +174,11 @@
     public void testFactoryReset_frpOnly() throws Exception {
         allowFactoryReset();
 
-        FactoryResetter.newBuilder(mContext)
+        boolean success = FactoryResetter.newBuilder(mContext)
                 .setWipeFactoryResetProtection(true)
                 .build().factoryReset();
 
+        assertThat(success).isTrue();
         verifyWipeAdoptableStorageNotCalled();
         verifyWipeFactoryResetProtectionCalled();
         verifyRebootWipeUserDataMinimumArgsCalled();
@@ -184,7 +188,7 @@
     public void testFactoryReset_allArgs() throws Exception {
         allowFactoryReset();
 
-        FactoryResetter.newBuilder(mContext)
+        boolean success = FactoryResetter.newBuilder(mContext)
                 .setReason(REASON)
                 .setForce(true)
                 .setShutdown(true)
@@ -193,6 +197,7 @@
                 .setWipeFactoryResetProtection(true)
                 .build().factoryReset();
 
+        assertThat(success).isTrue();
         verifyWipeAdoptableStorageCalled();
         verifyWipeFactoryResetProtectionCalled();
         verifyRebootWipeUserDataAllArgsCalled();
@@ -202,9 +207,10 @@
     public void testFactoryReset_minimumArgs_safetyChecker_neverReplied() throws Exception {
         allowFactoryReset();
 
-        FactoryResetter.newBuilder(mContext).setSafetyChecker(mSafetyChecker).build()
-                .factoryReset();
+        boolean success = FactoryResetter.newBuilder(mContext)
+                .setSafetyChecker(mSafetyChecker).build().factoryReset();
 
+        assertThat(success).isFalse();
         verifyWipeAdoptableStorageNotCalled();
         verifyWipeFactoryResetProtectionNotCalled();
         verifyRebootWipeUserDataNotCalled();
@@ -221,7 +227,7 @@
             return null;
         }).when(mSafetyChecker).onFactoryReset(any());
 
-        FactoryResetter.newBuilder(mContext)
+        boolean success = FactoryResetter.newBuilder(mContext)
                 .setSafetyChecker(mSafetyChecker)
                 .setReason(REASON)
                 .setForce(true)
@@ -231,6 +237,7 @@
                 .setWipeFactoryResetProtection(true)
                 .build().factoryReset();
 
+        assertThat(success).isFalse();
         verifyWipeAdoptableStorageCalled();
         verifyWipeFactoryResetProtectionCalled();
         verifyRebootWipeUserDataAllArgsCalled();
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 9c28c99..f3576af 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -319,10 +319,10 @@
         }
 
         @Override
-        void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force,
+        boolean recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force,
                 boolean wipeEuicc, boolean wipeExtRequested, boolean wipeResetProtectionData)
                         throws IOException {
-            services.recoverySystem.rebootWipeUserData(shutdown, reason, force, wipeEuicc,
+            return services.recoverySystem.rebootWipeUserData(shutdown, reason, force, wipeEuicc,
                     wipeExtRequested, wipeResetProtectionData);
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
index 7d1de86..d8e0c5c 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
@@ -392,9 +392,10 @@
     }
 
     public static class RecoverySystemForMock {
-        public void rebootWipeUserData(boolean shutdown, String reason, boolean force,
+        public boolean rebootWipeUserData(boolean shutdown, String reason, boolean force,
                 boolean wipeEuicc, boolean wipeExtRequested, boolean wipeResetProtectionData)
                         throws IOException {
+            return false;
         }
     }