blob: 2080fdf2e40dc21cd3802e0d6d8a73c1ae8a3b2c [file] [log] [blame]
/*
* 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 com.android.server.RescueParty.LEVEL_FACTORY_RESET;
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 static org.mockito.Mockito.times;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.VersionedPackage;
import android.os.Bundle;
import android.os.RecoverySystem;
import android.os.RemoteCallback;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.Settings;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
import com.android.server.RescueParty.RescuePartyObserver;
import com.android.server.am.SettingsToPropertiesMapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
import org.mockito.stubbing.Answer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/**
* Test RescueParty.
*/
public class RescuePartyTest {
private static final long CURRENT_NETWORK_TIME_MILLIS = 0L;
private static final String FAKE_NATIVE_NAMESPACE1 = "native1";
private static final String FAKE_NATIVE_NAMESPACE2 = "native2";
private static final String[] FAKE_RESET_NATIVE_NAMESPACES =
{FAKE_NATIVE_NAMESPACE1, FAKE_NATIVE_NAMESPACE2};
private static VersionedPackage sFailingPackage = new VersionedPackage("com.package.name", 1);
private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue";
private static final String CALLING_PACKAGE1 = "com.package.name1";
private static final String CALLING_PACKAGE2 = "com.package.name2";
private static final String NAMESPACE1 = "namespace1";
private static final String NAMESPACE2 = "namespace2";
private MockitoSession mSession;
private HashMap<String, String> mSystemSettingsMap;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mMockContext;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private PackageWatchdog mMockPackageWatchdog;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ContentResolver mMockContentResolver;
@Captor
private ArgumentCaptor<RemoteCallback> mMonitorCallbackCaptor;
@Captor
private ArgumentCaptor<List<String>> mPackageListCaptor;
@Before
public void setUp() throws Exception {
mSession =
ExtendedMockito.mockitoSession().initMocks(
this)
.strictness(Strictness.LENIENT)
.spyStatic(DeviceConfig.class)
.spyStatic(SystemProperties.class)
.spyStatic(Settings.Global.class)
.spyStatic(Settings.Secure.class)
.spyStatic(Settings.Config.class)
.spyStatic(SettingsToPropertiesMapper.class)
.spyStatic(RecoverySystem.class)
.spyStatic(RescueParty.class)
.spyStatic(PackageWatchdog.class)
.startMocking();
mSystemSettingsMap = new HashMap<>();
when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver);
// Reset observer instance to get new mock context on every run
RescuePartyObserver.reset();
// 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()));
// Mock DeviceConfig
doAnswer((Answer<Boolean>) invocationOnMock -> true)
.when(() -> DeviceConfig.setProperty(anyString(), anyString(), anyString(),
anyBoolean()));
doAnswer((Answer<Void>) invocationOnMock -> null)
.when(() -> DeviceConfig.resetToDefaults(anyInt(), anyString()));
// Mock PackageWatchdog
doAnswer((Answer<PackageWatchdog>) invocationOnMock -> mMockPackageWatchdog)
.when(() -> PackageWatchdog.getInstance(mMockContext));
doReturn(CURRENT_NETWORK_TIME_MILLIS).when(() -> RescueParty.getElapsedRealtime());
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();
verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_DEFAULTS, /*resetNamespaces=*/ null);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
noteBoot();
verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_CHANGES, /*resetNamespaces=*/ null);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
noteBoot();
verifySettingsResets(Settings.RESET_MODE_TRUSTED_DEFAULTS, /*resetNamespaces=*/ null);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
noteBoot();
verify(() -> RecoverySystem.rebootPromptAndWipeUserData(mMockContext, RescueParty.TAG));
assertEquals(LEVEL_FACTORY_RESET,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
}
@Test
public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevels() {
notePersistentAppCrash();
verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_DEFAULTS, /*resetNamespaces=*/ null);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
notePersistentAppCrash();
verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_CHANGES, /*resetNamespaces=*/ null);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
notePersistentAppCrash();
verifySettingsResets(Settings.RESET_MODE_TRUSTED_DEFAULTS, /*resetNamespaces=*/ null);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
notePersistentAppCrash();
verify(() -> RecoverySystem.rebootPromptAndWipeUserData(mMockContext, RescueParty.TAG));
assertEquals(LEVEL_FACTORY_RESET,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
}
@Test
public void testNonPersistentAppCrashDetectionWithScopedResets() {
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> Settings.Config.registerMonitorCallback(eq(mMockContentResolver),
mMonitorCallbackCaptor.capture()));
// Record DeviceConfig accesses
RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
RemoteCallback monitorCallback = mMonitorCallbackCaptor.getValue();
monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE1, NAMESPACE1));
monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE1, NAMESPACE2));
monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE2, NAMESPACE2));
// Fake DeviceConfig value changes
monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE1));
verify(mMockPackageWatchdog).startObservingHealth(observer,
Arrays.asList(CALLING_PACKAGE1), RescueParty.DEFAULT_OBSERVING_DURATION_MS);
monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE2));
verify(mMockPackageWatchdog, times(2)).startObservingHealth(eq(observer),
mPackageListCaptor.capture(),
eq(RescueParty.DEFAULT_OBSERVING_DURATION_MS));
assertTrue(mPackageListCaptor.getValue().containsAll(
Arrays.asList(CALLING_PACKAGE1, CALLING_PACKAGE2)));
// Perform and verify scoped resets
final String[] expectedResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2};
observer.execute(new VersionedPackage(
CALLING_PACKAGE1, 1), PackageWatchdog.FAILURE_REASON_APP_CRASH);
verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_DEFAULTS, expectedResetNamespaces);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
observer.execute(new VersionedPackage(
CALLING_PACKAGE1, 1), PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING);
verifySettingsResets(Settings.RESET_MODE_UNTRUSTED_CHANGES, expectedResetNamespaces);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
observer.execute(new VersionedPackage(
CALLING_PACKAGE1, 1), PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING);
verifySettingsResets(Settings.RESET_MODE_TRUSTED_DEFAULTS, /*resetNamespaces=*/null);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
observer.execute(new VersionedPackage(
CALLING_PACKAGE1, 1), PackageWatchdog.FAILURE_REASON_APP_CRASH);
verify(() -> RecoverySystem.rebootPromptAndWipeUserData(mMockContext, RescueParty.TAG));
assertTrue(RescueParty.isAttemptingFactoryReset());
}
@Test
public void testIsAttemptingFactoryReset() {
for (int i = 0; i < LEVEL_FACTORY_RESET; i++) {
noteBoot();
}
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, /*resetNamespaces=*/ null);
assertEquals(RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, RescueParty.LEVEL_NONE));
}
@Test
public void testNativeRescuePartyResets() {
doReturn(true).when(() -> SettingsToPropertiesMapper.isNativeFlagsResetPerformed());
doReturn(FAKE_RESET_NATIVE_NAMESPACES).when(
() -> SettingsToPropertiesMapper.getResetNativeCategories());
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> DeviceConfig.resetToDefaults(Settings.RESET_MODE_TRUSTED_DEFAULTS,
FAKE_NATIVE_NAMESPACE1));
verify(() -> DeviceConfig.resetToDefaults(Settings.RESET_MODE_TRUSTED_DEFAULTS,
FAKE_NATIVE_NAMESPACE2));
}
@Test
public void testExplicitlyEnablingAndDisablingRescue() {
SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(false));
SystemProperties.set(PROP_DISABLE_RESCUE, Boolean.toString(true));
assertEquals(RescuePartyObserver.getInstance(mMockContext).execute(sFailingPackage,
PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING), false);
SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(true));
assertTrue(RescuePartyObserver.getInstance(mMockContext).execute(sFailingPackage,
PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING));
}
@Test
public void testHealthCheckLevels() {
RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
// Ensure that no action is taken for cases where the failure reason is unknown
SystemProperties.set(RescueParty.PROP_RESCUE_LEVEL, Integer.toString(
LEVEL_FACTORY_RESET));
assertEquals(observer.onHealthCheckFailed(null, PackageWatchdog.FAILURE_REASON_UNKNOWN),
PackageHealthObserverImpact.USER_IMPACT_NONE);
/*
For the following cases, ensure that the returned user impact corresponds with the user
impact of the next available rescue level, not the current one.
*/
SystemProperties.set(RescueParty.PROP_RESCUE_LEVEL, Integer.toString(
RescueParty.LEVEL_NONE));
assertEquals(observer.onHealthCheckFailed(null,
PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING),
PackageHealthObserverImpact.USER_IMPACT_LOW);
SystemProperties.set(RescueParty.PROP_RESCUE_LEVEL, Integer.toString(
RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS));
assertEquals(observer.onHealthCheckFailed(null,
PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING),
PackageHealthObserverImpact.USER_IMPACT_LOW);
SystemProperties.set(RescueParty.PROP_RESCUE_LEVEL, Integer.toString(
RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES));
assertEquals(observer.onHealthCheckFailed(null,
PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING),
PackageHealthObserverImpact.USER_IMPACT_HIGH);
SystemProperties.set(RescueParty.PROP_RESCUE_LEVEL, Integer.toString(
RescueParty.LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS));
assertEquals(observer.onHealthCheckFailed(null,
PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING),
PackageHealthObserverImpact.USER_IMPACT_HIGH);
SystemProperties.set(RescueParty.PROP_RESCUE_LEVEL, Integer.toString(
LEVEL_FACTORY_RESET));
assertEquals(observer.onHealthCheckFailed(null,
PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING),
PackageHealthObserverImpact.USER_IMPACT_HIGH);
}
@Test
public void testRescueLevelIncrementsWhenExecuted() {
RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
SystemProperties.set(RescueParty.PROP_RESCUE_LEVEL, Integer.toString(
RescueParty.LEVEL_NONE));
observer.execute(sFailingPackage,
PackageWatchdog.FAILURE_REASON_APP_CRASH);
assertEquals(SystemProperties.getInt(RescueParty.PROP_RESCUE_LEVEL, -1),
RescueParty.LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS);
}
private void verifySettingsResets(int resetMode, String[] resetNamespaces) {
verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null,
resetMode, UserHandle.USER_SYSTEM));
verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(),
eq(resetMode), anyInt()));
// Verify DeviceConfig resets
if (resetNamespaces == null) {
verify(() -> DeviceConfig.resetToDefaults(resetMode, /*namespace=*/ null));
} else {
for (String namespace : resetNamespaces) {
verify(() -> DeviceConfig.resetToDefaults(resetMode, namespace));
}
}
}
private void noteBoot() {
RescuePartyObserver.getInstance(mMockContext).executeBootLoopMitigation();
}
private void notePersistentAppCrash() {
RescuePartyObserver.getInstance(mMockContext).execute(new VersionedPackage(
"com.package.name", 1), PackageWatchdog.FAILURE_REASON_APP_CRASH);
}
private Bundle getConfigAccessBundle(String callingPackage, String namespace) {
Bundle result = new Bundle();
result.putString(Settings.EXTRA_MONITOR_CALLBACK_TYPE, Settings.EXTRA_ACCESS_CALLBACK);
result.putString(Settings.EXTRA_CALLING_PACKAGE, callingPackage);
result.putString(Settings.EXTRA_NAMESPACE, namespace);
return result;
}
private Bundle getConfigNamespaceUpdateBundle(String updatedNamespace) {
Bundle result = new Bundle();
result.putString(Settings.EXTRA_MONITOR_CALLBACK_TYPE,
Settings.EXTRA_NAMESPACE_UPDATED_CALLBACK);
result.putString(Settings.EXTRA_NAMESPACE, updatedNamespace);
return result;
}
}