summaryrefslogtreecommitdiff
path: root/services/core/java/com/android/server/RescueParty.java
diff options
context:
space:
mode:
Diffstat (limited to 'services/core/java/com/android/server/RescueParty.java')
-rw-r--r--services/core/java/com/android/server/RescueParty.java1068
1 files changed, 1068 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
new file mode 100644
index 000000000000..37c2d263d14f
--- /dev/null
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -0,0 +1,1068 @@
+/*
+ * Copyright (C) 2017 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 android.provider.DeviceConfig.Properties;
+
+import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
+import android.os.Build;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.PowerManager;
+import android.os.RecoverySystem;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.sysprop.CrashRecoveryProperties;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.server.PackageWatchdog.FailureReasons;
+import com.android.server.PackageWatchdog.PackageHealthObserver;
+import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
+import com.android.server.am.SettingsToPropertiesMapper;
+import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
+
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities to help rescue the system from crash loops. Callers are expected to
+ * report boot events and persistent app crashes, and if they happen frequently
+ * enough this class will slowly escalate through several rescue operations
+ * before finally rebooting and prompting the user if they want to wipe data as
+ * a last resort.
+ *
+ * @hide
+ */
+public class RescueParty {
+ @VisibleForTesting
+ static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue";
+ @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_WARM_REBOOT = 4;
+ @VisibleForTesting
+ static final int LEVEL_FACTORY_RESET = 5;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_NONE = 0;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_WARM_REBOOT = 3;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_FACTORY_RESET = 7;
+
+ @IntDef(prefix = { "RESCUE_LEVEL_" }, value = {
+ RESCUE_LEVEL_NONE,
+ RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET,
+ RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET,
+ RESCUE_LEVEL_WARM_REBOOT,
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
+ RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
+ RESCUE_LEVEL_FACTORY_RESET
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RescueLevels {}
+
+ @VisibleForTesting
+ static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit";
+ @VisibleForTesting
+ static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1;
+ @VisibleForTesting
+ static final String TAG = "RescueParty";
+ @VisibleForTesting
+ static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2);
+ @VisibleForTesting
+ static final int DEVICE_CONFIG_RESET_MODE = Settings.RESET_MODE_TRUSTED_DEFAULTS;
+ // The DeviceConfig namespace containing all RescueParty switches.
+ @VisibleForTesting
+ static final String NAMESPACE_CONFIGURATION = "configuration";
+ @VisibleForTesting
+ static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG =
+ "namespace_to_package_mapping";
+ @VisibleForTesting
+ static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 1440;
+
+ private static final String NAME = "rescue-party-observer";
+
+ private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue";
+ private static final String PROP_VIRTUAL_DEVICE = "ro.hardware.virtual_device";
+ private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG =
+ "persist.device_config.configuration.disable_rescue_party";
+ private static final String PROP_DISABLE_FACTORY_RESET_FLAG =
+ "persist.device_config.configuration.disable_rescue_party_factory_reset";
+ private static final String PROP_THROTTLE_DURATION_MIN_FLAG =
+ "persist.device_config.configuration.rescue_party_throttle_duration_min";
+
+ private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
+ | ApplicationInfo.FLAG_SYSTEM;
+
+ /** Register the Rescue Party observer as a Package Watchdog health observer */
+ public static void registerHealthObserver(Context context) {
+ PackageWatchdog.getInstance(context).registerHealthObserver(
+ RescuePartyObserver.getInstance(context));
+ }
+
+ private static boolean isDisabled() {
+ // Check if we're explicitly enabled for testing
+ if (SystemProperties.getBoolean(PROP_ENABLE_RESCUE, false)) {
+ return false;
+ }
+
+ // We're disabled if the DeviceConfig disable flag is set to true.
+ // This is in case that an emergency rollback of the feature is needed.
+ if (SystemProperties.getBoolean(PROP_DEVICE_CONFIG_DISABLE_FLAG, false)) {
+ Slog.v(TAG, "Disabled because of DeviceConfig flag");
+ return true;
+ }
+
+ // We're disabled on all engineering devices
+ if (Build.TYPE.equals("eng")) {
+ Slog.v(TAG, "Disabled because of eng build");
+ return true;
+ }
+
+ // We're disabled on userdebug devices connected over USB, since that's
+ // a decent signal that someone is actively trying to debug the device,
+ // or that it's in a lab environment.
+ if (Build.TYPE.equals("userdebug") && isUsbActive()) {
+ Slog.v(TAG, "Disabled because of active USB connection");
+ return true;
+ }
+
+ // One last-ditch check
+ if (SystemProperties.getBoolean(PROP_DISABLE_RESCUE, false)) {
+ Slog.v(TAG, "Disabled because of manual property");
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if we're currently attempting to reboot for a factory reset. This method must
+ * return true if RescueParty tries to reboot early during a boot loop, since the device
+ * will not be fully booted at this time.
+ */
+ public static boolean isRecoveryTriggeredReboot() {
+ return isFactoryResetPropertySet() || isRebootPropertySet();
+ }
+
+ static boolean isFactoryResetPropertySet() {
+ return CrashRecoveryProperties.attemptingFactoryReset().orElse(false);
+ }
+
+ static boolean isRebootPropertySet() {
+ return CrashRecoveryProperties.attemptingReboot().orElse(false);
+ }
+
+ protected static long getLastFactoryResetTimeMs() {
+ return CrashRecoveryProperties.lastFactoryResetTimeMs().orElse(0L);
+ }
+
+ protected static int getMaxRescueLevelAttempted() {
+ return CrashRecoveryProperties.maxRescueLevelAttempted().orElse(LEVEL_NONE);
+ }
+
+ protected static void setFactoryResetProperty(boolean value) {
+ CrashRecoveryProperties.attemptingFactoryReset(value);
+ }
+ protected static void setRebootProperty(boolean value) {
+ CrashRecoveryProperties.attemptingReboot(value);
+ }
+
+ protected static void setLastFactoryResetTimeMs(long value) {
+ CrashRecoveryProperties.lastFactoryResetTimeMs(value);
+ }
+
+ protected static void setMaxRescueLevelAttempted(int level) {
+ CrashRecoveryProperties.maxRescueLevelAttempted(level);
+ }
+
+ /**
+ * 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) {
+ handleNativeRescuePartyResets();
+ ContentResolver contentResolver = context.getContentResolver();
+ DeviceConfig.setMonitorCallback(
+ contentResolver,
+ Executors.newSingleThreadExecutor(),
+ new RescuePartyMonitorCallback(context));
+ }
+
+
+ /**
+ * Called when {@code RollbackManager} performs Mainline module rollbacks,
+ * to avoid rolled back modules consuming flag values only expected to work
+ * on modules of newer versions.
+ */
+ public static void resetDeviceConfigForPackages(List<String> packageNames) {
+ if (packageNames == null) {
+ return;
+ }
+ Set<String> namespacesToReset = new ArraySet<String>();
+ Iterator<String> it = packageNames.iterator();
+ RescuePartyObserver rescuePartyObserver = RescuePartyObserver.getInstanceIfCreated();
+ // Get runtime package to namespace mapping if created.
+ if (rescuePartyObserver != null) {
+ while (it.hasNext()) {
+ String packageName = it.next();
+ Set<String> runtimeAffectedNamespaces =
+ rescuePartyObserver.getAffectedNamespaceSet(packageName);
+ if (runtimeAffectedNamespaces != null) {
+ namespacesToReset.addAll(runtimeAffectedNamespaces);
+ }
+ }
+ }
+ // Get preset package to namespace mapping if created.
+ Set<String> presetAffectedNamespaces = getPresetNamespacesForPackages(
+ packageNames);
+ if (presetAffectedNamespaces != null) {
+ namespacesToReset.addAll(presetAffectedNamespaces);
+ }
+
+ // Clear flags under the namespaces mapped to these packages.
+ // Using setProperties since DeviceConfig.resetToDefaults bans the current flag set.
+ Iterator<String> namespaceIt = namespacesToReset.iterator();
+ while (namespaceIt.hasNext()) {
+ String namespaceToReset = namespaceIt.next();
+ Properties properties = new Properties.Builder(namespaceToReset).build();
+ try {
+ if (!DeviceConfig.setProperties(properties)) {
+ logCriticalInfo(Log.ERROR, "Failed to clear properties under "
+ + namespaceToReset
+ + ". Running `device_config get_sync_disabled_for_tests` will confirm"
+ + " if config-bulk-update is enabled.");
+ }
+ } catch (DeviceConfig.BadConfigException exception) {
+ logCriticalInfo(Log.WARN, "namespace " + namespaceToReset
+ + " is already banned, skip reset.");
+ }
+ }
+ }
+
+ private static Set<String> getPresetNamespacesForPackages(List<String> packageNames) {
+ Set<String> resultSet = new ArraySet<String>();
+ try {
+ String flagVal = DeviceConfig.getString(NAMESPACE_CONFIGURATION,
+ NAMESPACE_TO_PACKAGE_MAPPING_FLAG, "");
+ String[] mappingEntries = flagVal.split(",");
+ for (int i = 0; i < mappingEntries.length; i++) {
+ if (TextUtils.isEmpty(mappingEntries[i])) {
+ continue;
+ }
+ String[] splittedEntry = mappingEntries[i].split(":");
+ if (splittedEntry.length != 2) {
+ throw new RuntimeException("Invalid mapping entry: " + mappingEntries[i]);
+ }
+ String namespace = splittedEntry[0];
+ String packageName = splittedEntry[1];
+
+ if (packageNames.contains(packageName)) {
+ resultSet.add(namespace);
+ }
+ }
+ } catch (Exception e) {
+ resultSet.clear();
+ Slog.e(TAG, "Failed to read preset package to namespaces mapping.", e);
+ } finally {
+ return resultSet;
+ }
+ }
+
+ @VisibleForTesting
+ static long getElapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ private static class RescuePartyMonitorCallback implements DeviceConfig.MonitorCallback {
+ Context mContext;
+
+ RescuePartyMonitorCallback(Context context) {
+ this.mContext = context;
+ }
+
+ public void onNamespaceUpdate(@NonNull String updatedNamespace) {
+ startObservingPackages(mContext, updatedNamespace);
+ }
+
+ public void onDeviceConfigAccess(@NonNull String callingPackage,
+ @NonNull String namespace) {
+ RescuePartyObserver.getInstance(mContext).recordDeviceConfigAccess(
+ callingPackage,
+ namespace);
+ }
+ }
+
+ private static void startObservingPackages(Context context, @NonNull String updatedNamespace) {
+ RescuePartyObserver rescuePartyObserver = RescuePartyObserver.getInstance(context);
+ Set<String> callingPackages = rescuePartyObserver.getCallingPackagesSet(updatedNamespace);
+ if (callingPackages == null) {
+ return;
+ }
+ List<String> callingPackageList = new ArrayList<>();
+ callingPackageList.addAll(callingPackages);
+ Slog.i(TAG, "Starting to observe: " + callingPackageList + ", updated namespace: "
+ + updatedNamespace);
+ PackageWatchdog.getInstance(context).startObservingHealth(
+ rescuePartyObserver,
+ callingPackageList,
+ DEFAULT_OBSERVING_DURATION_MS);
+ }
+
+ private static void handleNativeRescuePartyResets() {
+ if (SettingsToPropertiesMapper.isNativeFlagsResetPerformed()) {
+ String[] resetNativeCategories = SettingsToPropertiesMapper.getResetNativeCategories();
+ for (int i = 0; i < resetNativeCategories.length; i++) {
+ // Don't let RescueParty reset the namespace for RescueParty switches.
+ if (NAMESPACE_CONFIGURATION.equals(resetNativeCategories[i])) {
+ continue;
+ }
+ DeviceConfig.resetToDefaults(DEVICE_CONFIG_RESET_MODE,
+ resetNativeCategories[i]);
+ }
+ }
+ }
+
+ private static int getMaxRescueLevel(boolean mayPerformReboot) {
+ if (Flags.recoverabilityDetection()) {
+ if (!mayPerformReboot
+ || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
+ return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT,
+ DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT);
+ }
+ return RESCUE_LEVEL_FACTORY_RESET;
+ } else {
+ if (!mayPerformReboot
+ || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
+ return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
+ }
+ return LEVEL_FACTORY_RESET;
+ }
+ }
+
+ /**
+ * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
+ *
+ * @param mitigationCount: the mitigation attempt number (1 = first attempt etc.)
+ * @param mayPerformReboot: whether or not a reboot and factory reset may be performed
+ * for the given failure.
+ * @return the rescue level for the n-th mitigation attempt.
+ */
+ private static int getRescueLevel(int mitigationCount, boolean mayPerformReboot) {
+ if (mitigationCount == 1) {
+ return LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS;
+ } else if (mitigationCount == 2) {
+ return LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES;
+ } else if (mitigationCount == 3) {
+ return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
+ } else if (mitigationCount == 4) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_WARM_REBOOT);
+ } else if (mitigationCount >= 5) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_FACTORY_RESET);
+ } else {
+ Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount);
+ return LEVEL_NONE;
+ }
+ }
+
+ /**
+ * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
+ * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and
+ * all device config reset). Behaves as if one mitigation attempt was already done.
+ *
+ * @param mitigationCount the mitigation attempt number (1 = first attempt etc.).
+ * @param mayPerformReboot whether or not a reboot and factory reset may be performed
+ * for the given failure.
+ * @param failedPackage in case of bootloop this is null.
+ * @return the rescue level for the n-th mitigation attempt.
+ */
+ private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot,
+ @Nullable VersionedPackage failedPackage) {
+ // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed
+ // package.
+ if (failedPackage == null && mitigationCount > 0) {
+ mitigationCount += 1;
+ }
+ if (mitigationCount == 1) {
+ return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET;
+ } else if (mitigationCount == 2) {
+ return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET;
+ } else if (mitigationCount == 3) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT);
+ } else if (mitigationCount == 4) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot),
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS);
+ } else if (mitigationCount == 5) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot),
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES);
+ } else if (mitigationCount == 6) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot),
+ RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS);
+ } else if (mitigationCount >= 7) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET);
+ } else {
+ return RESCUE_LEVEL_NONE;
+ }
+ }
+
+ private static void executeRescueLevel(Context context, @Nullable String failedPackage,
+ int level) {
+ Slog.w(TAG, "Attempting rescue level " + levelToString(level));
+ try {
+ executeRescueLevelInternal(context, level, failedPackage);
+ EventLogTags.writeRescueSuccess(level);
+ String successMsg = "Finished rescue level " + levelToString(level);
+ if (!TextUtils.isEmpty(failedPackage)) {
+ successMsg += " for package " + failedPackage;
+ }
+ logCriticalInfo(Log.DEBUG, successMsg);
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ }
+
+ private static void executeRescueLevelInternal(Context context, int level, @Nullable
+ String failedPackage) throws Exception {
+ if (Flags.recoverabilityDetection()) {
+ executeRescueLevelInternalNew(context, level, failedPackage);
+ } else {
+ executeRescueLevelInternalOld(context, level, failedPackage);
+ }
+ }
+
+ private static void executeRescueLevelInternalOld(Context context, int level, @Nullable
+ String failedPackage) throws Exception {
+
+ // Note: DeviceConfig reset is disabled currently and would be enabled using the flag,
+ // after we have figured out a way to reset flags without interfering with trunk
+ // development. TODO: b/287618292 For enabling flag resets.
+ if (!Flags.allowRescuePartyFlagResets() && level <= LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS) {
+ return;
+ }
+
+ CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED,
+ level, levelToString(level));
+ // Try our best to reset all settings possible, and once finished
+ // rethrow any exception that we encountered
+ Exception res = null;
+ switch (level) {
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ try {
+ resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_DEFAULTS,
+ level);
+ } catch (Exception e) {
+ res = e;
+ }
+ try {
+ resetDeviceConfig(context, /*isScoped=*/true, failedPackage);
+ } catch (Exception e) {
+ res = e;
+ }
+ break;
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ try {
+ resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_CHANGES,
+ level);
+ } catch (Exception e) {
+ res = e;
+ }
+ try {
+ resetDeviceConfig(context, /*isScoped=*/true, failedPackage);
+ } catch (Exception e) {
+ res = e;
+ }
+ break;
+ case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ try {
+ resetAllSettingsIfNecessary(context, Settings.RESET_MODE_TRUSTED_DEFAULTS,
+ level);
+ } catch (Exception e) {
+ res = e;
+ }
+ try {
+ resetDeviceConfig(context, /*isScoped=*/false, failedPackage);
+ } catch (Exception e) {
+ res = e;
+ }
+ break;
+ case LEVEL_WARM_REBOOT:
+ executeWarmReboot(context, level, failedPackage);
+ break;
+ case LEVEL_FACTORY_RESET:
+ // Before the completion of Reboot, if any crash happens then PackageWatchdog
+ // escalates to next level i.e. factory reset, as they happen in separate threads.
+ // Adding a check to prevent factory reset to execute before above reboot completes.
+ // Note: this reboot property is not persistent resets after reboot is completed.
+ if (isRebootPropertySet()) {
+ return;
+ }
+ executeFactoryReset(context, level, failedPackage);
+ break;
+ }
+
+ if (res != null) {
+ throw res;
+ }
+ }
+
+ private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level,
+ @Nullable String failedPackage) throws Exception {
+ CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED,
+ level, levelToString(level));
+ switch (level) {
+ case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+ // Enable deviceConfig reset behind flag
+ if (Flags.allowRescuePartyFlagResets()) {
+ resetDeviceConfig(context, /*isScoped=*/true, failedPackage);
+ }
+ break;
+ case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+ // Enable deviceConfig reset behind flag
+ if (Flags.allowRescuePartyFlagResets()) {
+ resetDeviceConfig(context, /*isScoped=*/false, failedPackage);
+ }
+ break;
+ case RESCUE_LEVEL_WARM_REBOOT:
+ executeWarmReboot(context, level, failedPackage);
+ break;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_DEFAULTS, level);
+ break;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_CHANGES, level);
+ break;
+ case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ resetAllSettingsIfNecessary(context, Settings.RESET_MODE_TRUSTED_DEFAULTS, level);
+ break;
+ case RESCUE_LEVEL_FACTORY_RESET:
+ // Before the completion of Reboot, if any crash happens then PackageWatchdog
+ // escalates to next level i.e. factory reset, as they happen in separate threads.
+ // Adding a check to prevent factory reset to execute before above reboot completes.
+ // Note: this reboot property is not persistent resets after reboot is completed.
+ if (isRebootPropertySet()) {
+ return;
+ }
+ executeFactoryReset(context, level, failedPackage);
+ break;
+ }
+ }
+
+ private static void executeWarmReboot(Context context, int level,
+ @Nullable String failedPackage) {
+ // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog
+ // when device shutting down.
+ setRebootProperty(true);
+ Runnable runnable = () -> {
+ try {
+ PowerManager pm = context.getSystemService(PowerManager.class);
+ if (pm != null) {
+ pm.reboot(TAG);
+ }
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ };
+ Thread thread = new Thread(runnable);
+ thread.start();
+ }
+
+ private static void executeFactoryReset(Context context, int level,
+ @Nullable String failedPackage) {
+ setFactoryResetProperty(true);
+ long now = System.currentTimeMillis();
+ setLastFactoryResetTimeMs(now);
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ RecoverySystem.rebootPromptAndWipeUserData(context, TAG);
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ }
+ };
+ Thread thread = new Thread(runnable);
+ thread.start();
+ }
+
+
+ private static String getCompleteMessage(Throwable t) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(t.getMessage());
+ while ((t = t.getCause()) != null) {
+ builder.append(": ").append(t.getMessage());
+ }
+ return builder.toString();
+ }
+
+ private static void logRescueException(int level, @Nullable String failedPackageName,
+ Throwable t) {
+ final String msg = getCompleteMessage(t);
+ EventLogTags.writeRescueFailure(level, msg);
+ String failureMsg = "Failed rescue level " + levelToString(level);
+ if (!TextUtils.isEmpty(failedPackageName)) {
+ failureMsg += " for package " + failedPackageName;
+ }
+ logCriticalInfo(Log.ERROR, failureMsg + ": " + msg);
+ }
+
+ private static int mapRescueLevelToUserImpact(int rescueLevel) {
+ if (Flags.recoverabilityDetection()) {
+ switch (rescueLevel) {
+ case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
+ case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_20;
+ case RESCUE_LEVEL_WARM_REBOOT:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75;
+ case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80;
+ case RESCUE_LEVEL_FACTORY_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
+ default:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+ } else {
+ switch (rescueLevel) {
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
+ case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ case LEVEL_WARM_REBOOT:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+ case LEVEL_FACTORY_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
+ default:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+ }
+ }
+
+ private static void resetAllSettingsIfNecessary(Context context, int mode,
+ int level) throws Exception {
+ // No need to reset Settings again if they are already reset in the current level once.
+ if (getMaxRescueLevelAttempted() >= level) {
+ return;
+ }
+ setMaxRescueLevelAttempted(level);
+ // Try our best to reset all settings possible, and once finished
+ // rethrow any exception that we encountered
+ Exception res = null;
+ final ContentResolver resolver = context.getContentResolver();
+ try {
+ Settings.Global.resetToDefaultsAsUser(resolver, null, mode,
+ UserHandle.SYSTEM.getIdentifier());
+ } catch (Exception e) {
+ res = new RuntimeException("Failed to reset global settings", e);
+ }
+ for (int userId : getAllUserIds()) {
+ try {
+ Settings.Secure.resetToDefaultsAsUser(resolver, null, mode, userId);
+ } catch (Exception e) {
+ res = new RuntimeException("Failed to reset secure settings for " + userId, e);
+ }
+ }
+ if (res != null) {
+ throw res;
+ }
+ }
+
+ private static void resetDeviceConfig(Context context, boolean isScoped,
+ @Nullable String failedPackage) throws Exception {
+ final ContentResolver resolver = context.getContentResolver();
+ try {
+ if (!isScoped || failedPackage == null) {
+ resetAllAffectedNamespaces(context);
+ } else {
+ performScopedReset(context, failedPackage);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to reset config settings", e);
+ }
+ }
+
+ private static void resetAllAffectedNamespaces(Context context) {
+ RescuePartyObserver rescuePartyObserver = RescuePartyObserver.getInstance(context);
+ Set<String> allAffectedNamespaces = rescuePartyObserver.getAllAffectedNamespaceSet();
+
+ Slog.w(TAG,
+ "Performing reset for all affected namespaces: "
+ + Arrays.toString(allAffectedNamespaces.toArray()));
+ Iterator<String> it = allAffectedNamespaces.iterator();
+ while (it.hasNext()) {
+ String namespace = it.next();
+ // Don't let RescueParty reset the namespace for RescueParty switches.
+ if (NAMESPACE_CONFIGURATION.equals(namespace)) {
+ continue;
+ }
+ DeviceConfig.resetToDefaults(DEVICE_CONFIG_RESET_MODE, namespace);
+ }
+ }
+
+ private static void performScopedReset(Context context, @NonNull String failedPackage) {
+ RescuePartyObserver rescuePartyObserver = RescuePartyObserver.getInstance(context);
+ Set<String> affectedNamespaces = rescuePartyObserver.getAffectedNamespaceSet(
+ failedPackage);
+ // If we can't find namespaces affected for current package,
+ // skip this round of reset.
+ if (affectedNamespaces != null) {
+ Slog.w(TAG,
+ "Performing scoped reset for package: " + failedPackage
+ + ", affected namespaces: "
+ + Arrays.toString(affectedNamespaces.toArray()));
+ Iterator<String> it = affectedNamespaces.iterator();
+ while (it.hasNext()) {
+ String namespace = it.next();
+ // Don't let RescueParty reset the namespace for RescueParty switches.
+ if (NAMESPACE_CONFIGURATION.equals(namespace)) {
+ continue;
+ }
+ DeviceConfig.resetToDefaults(DEVICE_CONFIG_RESET_MODE, namespace);
+ }
+ }
+ }
+
+ /**
+ * Handle mitigation action for package failures. This observer will be register to Package
+ * Watchdog and will receive calls about package failures. This observer is persistent so it
+ * may choose to mitigate failures for packages it has not explicitly asked to observe.
+ */
+ public static class RescuePartyObserver implements PackageHealthObserver {
+
+ private final Context mContext;
+ private final Map<String, Set<String>> mCallingPackageNamespaceSetMap = new HashMap<>();
+ private final Map<String, Set<String>> mNamespaceCallingPackageSetMap = new HashMap<>();
+
+ @GuardedBy("RescuePartyObserver.class")
+ static RescuePartyObserver sRescuePartyObserver;
+
+ private RescuePartyObserver(Context context) {
+ mContext = context;
+ }
+
+ /** Creates or gets singleton instance of RescueParty. */
+ public static RescuePartyObserver getInstance(Context context) {
+ synchronized (RescuePartyObserver.class) {
+ if (sRescuePartyObserver == null) {
+ sRescuePartyObserver = new RescuePartyObserver(context);
+ }
+ return sRescuePartyObserver;
+ }
+ }
+
+ /** Gets singleton instance. It returns null if the instance is not created yet.*/
+ @Nullable
+ public static RescuePartyObserver getInstanceIfCreated() {
+ synchronized (RescuePartyObserver.class) {
+ return sRescuePartyObserver;
+ }
+ }
+
+ @VisibleForTesting
+ static void reset() {
+ synchronized (RescuePartyObserver.class) {
+ sRescuePartyObserver = null;
+ }
+ }
+
+ @Override
+ public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage,
+ @FailureReasons int failureReason, int mitigationCount) {
+ if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
+ || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) {
+ if (Flags.recoverabilityDetection()) {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+ mayPerformReboot(failedPackage), failedPackage));
+ } else {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+ mayPerformReboot(failedPackage)));
+ }
+ } else {
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+ }
+
+ @Override
+ public boolean execute(@Nullable VersionedPackage failedPackage,
+ @FailureReasons int failureReason, int mitigationCount) {
+ if (isDisabled()) {
+ return false;
+ }
+ if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
+ || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) {
+ final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount,
+ mayPerformReboot(failedPackage), failedPackage)
+ : getRescueLevel(mitigationCount,
+ mayPerformReboot(failedPackage));
+ executeRescueLevel(mContext,
+ failedPackage == null ? null : failedPackage.getPackageName(), level);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isPersistent() {
+ return true;
+ }
+
+ @Override
+ public boolean mayObservePackage(String packageName) {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ // A package is a module if this is non-null
+ if (pm.getModuleInfo(packageName, 0) != null) {
+ return true;
+ }
+ } catch (PackageManager.NameNotFoundException | IllegalStateException ignore) {
+ }
+
+ return isPersistentSystemApp(packageName);
+ }
+
+ @Override
+ public int onBootLoop(int mitigationCount) {
+ if (isDisabled()) {
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+ if (Flags.recoverabilityDetection()) {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+ true, /*failedPackage=*/ null));
+ } else {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true));
+ }
+ }
+
+ @Override
+ public boolean executeBootLoopMitigation(int mitigationCount) {
+ if (isDisabled()) {
+ return false;
+ }
+ boolean mayPerformReboot = !shouldThrottleReboot();
+ final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount,
+ mayPerformReboot, /*failedPackage=*/ null)
+ : getRescueLevel(mitigationCount, mayPerformReboot);
+ executeRescueLevel(mContext, /*failedPackage=*/ null, level);
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ /**
+ * Returns {@code true} if the failing package is non-null and performing a reboot or
+ * prompting a factory reset is an acceptable mitigation strategy for the package's
+ * failure, {@code false} otherwise.
+ */
+ private boolean mayPerformReboot(@Nullable VersionedPackage failingPackage) {
+ if (failingPackage == null) {
+ return false;
+ }
+ if (shouldThrottleReboot()) {
+ return false;
+ }
+
+ return isPersistentSystemApp(failingPackage.getPackageName());
+ }
+
+ /**
+ * Returns {@code true} if Rescue Party is allowed to attempt a reboot or factory reset.
+ * Will return {@code false} if a factory reset was already offered recently.
+ */
+ private boolean shouldThrottleReboot() {
+ Long lastResetTime = getLastFactoryResetTimeMs();
+ long now = System.currentTimeMillis();
+ long throttleDurationMin = SystemProperties.getLong(PROP_THROTTLE_DURATION_MIN_FLAG,
+ DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN);
+ return now < lastResetTime + TimeUnit.MINUTES.toMillis(throttleDurationMin);
+ }
+
+ private boolean isPersistentSystemApp(@NonNull String packageName) {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+ return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private synchronized void recordDeviceConfigAccess(@NonNull String callingPackage,
+ @NonNull String namespace) {
+ // Record it in calling packages to namespace map
+ Set<String> namespaceSet = mCallingPackageNamespaceSetMap.get(callingPackage);
+ if (namespaceSet == null) {
+ namespaceSet = new ArraySet<>();
+ mCallingPackageNamespaceSetMap.put(callingPackage, namespaceSet);
+ }
+ namespaceSet.add(namespace);
+ // Record it in namespace to calling packages map
+ Set<String> callingPackageSet = mNamespaceCallingPackageSetMap.get(namespace);
+ if (callingPackageSet == null) {
+ callingPackageSet = new ArraySet<>();
+ }
+ callingPackageSet.add(callingPackage);
+ mNamespaceCallingPackageSetMap.put(namespace, callingPackageSet);
+ }
+
+ private synchronized Set<String> getAffectedNamespaceSet(String failedPackage) {
+ return mCallingPackageNamespaceSetMap.get(failedPackage);
+ }
+
+ private synchronized Set<String> getAllAffectedNamespaceSet() {
+ return new HashSet<String>(mNamespaceCallingPackageSetMap.keySet());
+ }
+
+ private synchronized Set<String> getCallingPackagesSet(String namespace) {
+ return mNamespaceCallingPackageSetMap.get(namespace);
+ }
+ }
+
+ private static int[] getAllUserIds() {
+ int systemUserId = UserHandle.SYSTEM.getIdentifier();
+ int[] userIds = { systemUserId };
+ try {
+ for (File file : FileUtils.listFilesOrEmpty(Environment.getDataSystemDeDirectory())) {
+ try {
+ final int userId = Integer.parseInt(file.getName());
+ if (userId != systemUserId) {
+ userIds = ArrayUtils.appendInt(userIds, userId);
+ }
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ } catch (Throwable t) {
+ Slog.w(TAG, "Trouble discovering users", t);
+ }
+ return userIds;
+ }
+
+ /**
+ * Hacky test to check if the device has an active USB connection, which is
+ * a good proxy for someone doing local development work.
+ */
+ private static boolean isUsbActive() {
+ if (SystemProperties.getBoolean(PROP_VIRTUAL_DEVICE, false)) {
+ Slog.v(TAG, "Assuming virtual device is connected over USB");
+ return true;
+ }
+ try {
+ final String state = FileUtils
+ .readTextFile(new File("/sys/class/android_usb/android0/state"), 128, "");
+ return "CONFIGURED".equals(state.trim());
+ } catch (Throwable t) {
+ Slog.w(TAG, "Failed to determine if device was on USB", t);
+ return false;
+ }
+ }
+
+ private static String levelToString(int level) {
+ if (Flags.recoverabilityDetection()) {
+ switch (level) {
+ case RESCUE_LEVEL_NONE:
+ return "NONE";
+ case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+ return "SCOPED_DEVICE_CONFIG_RESET";
+ case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+ return "ALL_DEVICE_CONFIG_RESET";
+ case RESCUE_LEVEL_WARM_REBOOT:
+ return "WARM_REBOOT";
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return "RESET_SETTINGS_UNTRUSTED_CHANGES";
+ case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_TRUSTED_DEFAULTS";
+ case RESCUE_LEVEL_FACTORY_RESET:
+ return "FACTORY_RESET";
+ default:
+ return Integer.toString(level);
+ }
+ } else {
+ switch (level) {
+ case LEVEL_NONE:
+ return "NONE";
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return "RESET_SETTINGS_UNTRUSTED_CHANGES";
+ case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_TRUSTED_DEFAULTS";
+ case LEVEL_WARM_REBOOT:
+ return "WARM_REBOOT";
+ case LEVEL_FACTORY_RESET:
+ return "FACTORY_RESET";
+ default:
+ return Integer.toString(level);
+ }
+ }
+ }
+}