Merge "Adds unit tests for RescueParty."
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
index 6e5d316..18c2722 100644
--- a/services/core/java/com/android/server/RescueParty.java
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -36,6 +36,7 @@
 import android.util.SparseArray;
 import android.util.StatsLog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.utils.FlagNamespaceUtils;
 
@@ -51,21 +52,35 @@
  * @hide
  */
 public class RescueParty {
-    private static final String TAG = "RescueParty";
+    @VisibleForTesting
+    static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue";
+    @VisibleForTesting
+    static final int TRIGGER_COUNT = 5;
+    @VisibleForTesting
+    static final String PROP_RESCUE_LEVEL = "sys.rescue_level";
+    @VisibleForTesting
+    static final int LEVEL_NONE = 0;
+    @VisibleForTesting
+    static final int LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 1;
+    @VisibleForTesting
+    static final int LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 2;
+    @VisibleForTesting
+    static final int LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 3;
+    @VisibleForTesting
+    static final int LEVEL_FACTORY_RESET = 4;
+    @VisibleForTesting
+    static final String PROP_RESCUE_BOOT_COUNT = "sys.rescue_boot_count";
+    @VisibleForTesting
+    static final long BOOT_TRIGGER_WINDOW_MILLIS = 300 * DateUtils.SECOND_IN_MILLIS;
+    @VisibleForTesting
+    static final long PERSISTENT_APP_CRASH_TRIGGER_WINDOW_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS;
+    @VisibleForTesting
+    static final String TAG = "RescueParty";
 
-    private static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue";
     private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue";
-    private static final String PROP_RESCUE_LEVEL = "sys.rescue_level";
-    private static final String PROP_RESCUE_BOOT_COUNT = "sys.rescue_boot_count";
     private static final String PROP_RESCUE_BOOT_START = "sys.rescue_boot_start";
     private static final String PROP_VIRTUAL_DEVICE = "ro.hardware.virtual_device";
 
-    private static final int LEVEL_NONE = 0;
-    private static final int LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 1;
-    private static final int LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 2;
-    private static final int LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 3;
-    private static final int LEVEL_FACTORY_RESET = 4;
-
     /** Threshold for boot loops */
     private static final Threshold sBoot = new BootThreshold();
     /** Threshold for app crash loops */
@@ -139,6 +154,29 @@
     }
 
     /**
+     * Called when {@code SettingsProvider} has been published, which is a good
+     * opportunity to reset any settings depending on our rescue level.
+     */
+    public static void onSettingsProviderPublished(Context context) {
+        executeRescueLevel(context);
+    }
+
+    @VisibleForTesting
+    static void resetAllThresholds() {
+        sBoot.reset();
+
+        for (int i = 0; i < sApps.size(); i++) {
+            Threshold appThreshold = sApps.get(sApps.keyAt(i));
+            appThreshold.reset();
+        }
+    }
+
+    @VisibleForTesting
+    static long getElapsedRealtime() {
+        return SystemClock.elapsedRealtime();
+    }
+
+    /**
      * Escalate to the next rescue level. After incrementing the level you'll
      * probably want to call {@link #executeRescueLevel(Context)}.
      */
@@ -153,14 +191,6 @@
                 + levelToString(level) + " triggered by UID " + triggerUid);
     }
 
-    /**
-     * Called when {@code SettingsProvider} has been published, which is a good
-     * opportunity to reset any settings depending on our rescue level.
-     */
-    public static void onSettingsProviderPublished(Context context) {
-        executeRescueLevel(context);
-    }
-
     private static void executeRescueLevel(Context context) {
         final int level = SystemProperties.getInt(PROP_RESCUE_LEVEL, LEVEL_NONE);
         if (level == LEVEL_NONE) return;
@@ -255,7 +285,7 @@
          * @return if this threshold has been triggered
          */
         public boolean incrementAndTest() {
-            final long now = SystemClock.elapsedRealtime();
+            final long now = getElapsedRealtime();
             final long window = now - getStart();
             if (window > triggerWindow) {
                 setCount(1);
@@ -278,10 +308,10 @@
      */
     private static class BootThreshold extends Threshold {
         public BootThreshold() {
-            // We're interested in 5 events in any 300 second period; this
-            // window is super relaxed because booting can take a long time if
-            // forced to dexopt things.
-            super(android.os.Process.ROOT_UID, 5, 300 * DateUtils.SECOND_IN_MILLIS);
+            // We're interested in TRIGGER_COUNT events in any
+            // BOOT_TRIGGER_WINDOW_MILLIS second period; this window is super relaxed because
+            // booting can take a long time if forced to dexopt things.
+            super(android.os.Process.ROOT_UID, TRIGGER_COUNT, BOOT_TRIGGER_WINDOW_MILLIS);
         }
 
         @Override
@@ -314,9 +344,10 @@
         private long start;
 
         public AppThreshold(int uid) {
-            // We're interested in 5 events in any 30 second period; apps crash
-            // pretty quickly so we can keep a tight leash on them.
-            super(uid, 5, 30 * DateUtils.SECOND_IN_MILLIS);
+            // We're interested in TRIGGER_COUNT events in any
+            // PERSISTENT_APP_CRASH_TRIGGER_WINDOW_MILLIS second period; apps crash pretty quickly
+            // so we can keep a tight leash on them.
+            super(uid, TRIGGER_COUNT, PERSISTENT_APP_CRASH_TRIGGER_WINDOW_MILLIS);
         }
 
         @Override public int getCount() { return count; }
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
new file mode 100644
index 0000000..b13735c
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2019 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;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyString;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.RecoverySystem;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
+
+import java.util.HashMap;
+
+/**
+ * Test RescueParty.
+ */
+public class RescuePartyTest {
+    private static final int PERSISTENT_APP_UID = 12;
+    private static final long CURRENT_NETWORK_TIME_MILLIS = 0L;
+
+    private MockitoSession mSession;
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private Context mMockContext;
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private ContentResolver mMockContentResolver;
+
+    private HashMap<String, String> mSystemSettingsMap;
+
+    @Before
+    public void setUp() throws Exception {
+        mSession =
+                ExtendedMockito.mockitoSession().initMocks(
+                        this)
+                        .strictness(Strictness.LENIENT)
+                        .spyStatic(SystemProperties.class)
+                        .spyStatic(Settings.Global.class)
+                        .spyStatic(Settings.Secure.class)
+                        .spyStatic(RecoverySystem.class)
+                        .spyStatic(RescueParty.class)
+                        .startMocking();
+        mSystemSettingsMap = new HashMap<>();
+
+        when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver);
+
+
+        // Mock SystemProperties setter and various getters
+        doAnswer((Answer<Void>) invocationOnMock -> {
+                    String key = invocationOnMock.getArgument(0);
+                    String value = invocationOnMock.getArgument(1);
+
+                    mSystemSettingsMap.put(key, value);
+                    return null;
+                }
+        ).when(() -> SystemProperties.set(anyString(), anyString()));
+
+        doAnswer((Answer<Boolean>) invocationOnMock -> {
+                    String key = invocationOnMock.getArgument(0);
+                    boolean defaultValue = invocationOnMock.getArgument(1);
+
+                    String storedValue = mSystemSettingsMap.get(key);
+                    return storedValue == null ? defaultValue : Boolean.parseBoolean(storedValue);
+                }
+        ).when(() -> SystemProperties.getBoolean(anyString(), anyBoolean()));
+
+        doAnswer((Answer<Integer>) invocationOnMock -> {
+                    String key = invocationOnMock.getArgument(0);
+                    int defaultValue = invocationOnMock.getArgument(1);
+
+                    String storedValue = mSystemSettingsMap.get(key);
+                    return storedValue == null ? defaultValue : Integer.parseInt(storedValue);
+                }
+        ).when(() -> SystemProperties.getInt(anyString(), anyInt()));
+
+        doAnswer((Answer<Long>) invocationOnMock -> {
+                    String key = invocationOnMock.getArgument(0);
+                    long defaultValue = invocationOnMock.getArgument(1);
+
+                    String storedValue = mSystemSettingsMap.get(key);
+                    return storedValue == null ? defaultValue : Long.parseLong(storedValue);
+                }
+        ).when(() -> SystemProperties.getLong(anyString(), anyLong()));
+
+        doReturn(CURRENT_NETWORK_TIME_MILLIS).when(() -> RescueParty.getElapsedRealtime());
+        RescueParty.resetAllThresholds();
+
+        SystemProperties.set(RescueParty.PROP_RESCUE_LEVEL,
+                Integer.toString(RescueParty.LEVEL_NONE));
+        SystemProperties.set(RescueParty.PROP_RESCUE_BOOT_COUNT, Integer.toString(0));
+        SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(true));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSession.finishMocking();
+    }
+
+    @Test
+    public void testBootLoopDetectionWithExecutionForAllRescueLevels() {
+        noteBoot(RescueParty.TRIGGER_COUNT);
+
+        verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+        assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+
+        noteBoot(RescueParty.TRIGGER_COUNT);
+
+        verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_CHANGES);
+        assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+
+        noteBoot(RescueParty.TRIGGER_COUNT);
+
+        verifySettingsResets(Settings.RESET_MODE_TRUSTED_DEFAULTS);
+        assertEquals(RescueParty.LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+
+        noteBoot(RescueParty.TRIGGER_COUNT);
+
+        verify(() -> RecoverySystem.rebootPromptAndWipeUserData(mMockContext, RescueParty.TAG));
+        assertEquals(RescueParty.LEVEL_FACTORY_RESET,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+    }
+
+    @Test
+    public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevels() {
+        notePersistentAppCrash(RescueParty.TRIGGER_COUNT);
+
+        verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+        assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+
+        notePersistentAppCrash(RescueParty.TRIGGER_COUNT);
+
+        verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_CHANGES);
+        assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+
+        notePersistentAppCrash(RescueParty.TRIGGER_COUNT);
+
+        verifySettingsResets(Settings.RESET_MODE_TRUSTED_DEFAULTS);
+        assertEquals(RescueParty.LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+
+        notePersistentAppCrash(RescueParty.TRIGGER_COUNT);
+
+        verify(() -> RecoverySystem.rebootPromptAndWipeUserData(mMockContext, RescueParty.TAG));
+        assertEquals(RescueParty.LEVEL_FACTORY_RESET,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+    }
+
+    @Test
+    public void testBootLoopDetectionWithWrongInterval() {
+        noteBoot(RescueParty.TRIGGER_COUNT - 1);
+
+        // last boot is just outside of the boot loop detection window
+        doReturn(CURRENT_NETWORK_TIME_MILLIS + RescueParty.BOOT_TRIGGER_WINDOW_MILLIS + 1).when(
+                () -> RescueParty.getElapsedRealtime());
+        noteBoot(/*numTimes=*/1);
+
+        assertEquals(RescueParty.LEVEL_NONE,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+    }
+
+    @Test
+    public void testPersistentAppCrashDetectionWithWrongInterval() {
+        notePersistentAppCrash(RescueParty.TRIGGER_COUNT - 1);
+
+        // last persistent app crash is just outside of the boot loop detection window
+        doReturn(CURRENT_NETWORK_TIME_MILLIS
+                + RescueParty.PERSISTENT_APP_CRASH_TRIGGER_WINDOW_MILLIS + 1)
+                .when(() -> RescueParty.getElapsedRealtime());
+        notePersistentAppCrash(/*numTimes=*/1);
+
+        assertEquals(RescueParty.LEVEL_NONE,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+    }
+
+    @Test
+    public void testBootLoopDetectionWithProperInterval() {
+        noteBoot(RescueParty.TRIGGER_COUNT - 1);
+
+        // last boot is just inside of the boot loop detection window
+        doReturn(CURRENT_NETWORK_TIME_MILLIS + RescueParty.BOOT_TRIGGER_WINDOW_MILLIS).when(
+                () -> RescueParty.getElapsedRealtime());
+        noteBoot(/*numTimes=*/1);
+
+        verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+        assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+    }
+
+    @Test
+    public void testPersistentAppCrashDetectionWithProperInterval() {
+        notePersistentAppCrash(RescueParty.TRIGGER_COUNT - 1);
+
+        // last persistent app crash is just inside of the boot loop detection window
+        doReturn(CURRENT_NETWORK_TIME_MILLIS
+                + RescueParty.PERSISTENT_APP_CRASH_TRIGGER_WINDOW_MILLIS)
+                .when(() -> RescueParty.getElapsedRealtime());
+        notePersistentAppCrash(/*numTimes=*/1);
+
+        verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+        assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+    }
+
+    @Test
+    public void testBootLoopDetectionWithWrongTriggerCount() {
+        noteBoot(RescueParty.TRIGGER_COUNT - 1);
+        assertEquals(RescueParty.LEVEL_NONE,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+    }
+
+    @Test
+    public void testPersistentAppCrashDetectionWithWrongTriggerCount() {
+        notePersistentAppCrash(RescueParty.TRIGGER_COUNT - 1);
+        assertEquals(RescueParty.LEVEL_NONE,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+    }
+
+    @Test
+    public void testIsAttemptingFactoryReset() {
+        noteBoot(RescueParty.TRIGGER_COUNT * 4);
+
+        verify(() -> RecoverySystem.rebootPromptAndWipeUserData(mMockContext, RescueParty.TAG));
+        assertTrue(RescueParty.isAttemptingFactoryReset());
+    }
+
+    @Test
+    public void testOnSettingsProviderPublishedExecutesRescueLevels() {
+        SystemProperties.set(RescueParty.PROP_RESCUE_LEVEL, Integer.toString(1));
+
+        RescueParty.onSettingsProviderPublished(mMockContext);
+
+        verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+        assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
+                SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
+    }
+
+    private void verifySettingsResets(int resetMode) {
+        verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null,
+                resetMode,
+                UserHandle.USER_SYSTEM));
+        verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(),
+                eq(resetMode), anyInt()));
+    }
+
+    private void noteBoot(int numTimes) {
+        for (int i = 0; i < numTimes; i++) {
+            RescueParty.noteBoot(mMockContext);
+        }
+    }
+
+    private void notePersistentAppCrash(int numTimes) {
+        for (int i = 0; i < numTimes; i++) {
+            RescueParty.notePersistentAppCrash(mMockContext, PERSISTENT_APP_UID);
+        }
+    }
+}