Loading services/core/java/com/android/server/RescueParty.java +57 −26 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import android.util.Slog; 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; Loading @@ -51,21 +52,35 @@ import java.io.File; * @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 */ Loading Loading @@ -138,6 +153,29 @@ public class RescueParty { return SystemProperties.getInt(PROP_RESCUE_LEVEL, LEVEL_NONE) == LEVEL_FACTORY_RESET; } /** * 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)}. Loading @@ -153,14 +191,6 @@ public class RescueParty { + 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; Loading Loading @@ -255,7 +285,7 @@ public class RescueParty { * @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); Loading @@ -278,10 +308,10 @@ public class RescueParty { */ 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 Loading Loading @@ -314,9 +344,10 @@ public class RescueParty { 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; } Loading services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java 0 → 100644 +300 −0 Original line number Diff line number Diff line /* * 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); } } } Loading
services/core/java/com/android/server/RescueParty.java +57 −26 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import android.util.Slog; 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; Loading @@ -51,21 +52,35 @@ import java.io.File; * @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 */ Loading Loading @@ -138,6 +153,29 @@ public class RescueParty { return SystemProperties.getInt(PROP_RESCUE_LEVEL, LEVEL_NONE) == LEVEL_FACTORY_RESET; } /** * 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)}. Loading @@ -153,14 +191,6 @@ public class RescueParty { + 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; Loading Loading @@ -255,7 +285,7 @@ public class RescueParty { * @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); Loading @@ -278,10 +308,10 @@ public class RescueParty { */ 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 Loading Loading @@ -314,9 +344,10 @@ public class RescueParty { 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; } Loading
services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java 0 → 100644 +300 −0 Original line number Diff line number Diff line /* * 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); } } }