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);
+ }
+ }
+}