1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server; 18 19 import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SKIPPED; 20 import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS; 21 import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; 22 23 import android.annotation.IntDef; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.content.Context; 27 import android.content.pm.ApplicationInfo; 28 import android.content.pm.PackageManager; 29 import android.content.pm.VersionedPackage; 30 import android.crashrecovery.flags.Flags; 31 import android.os.Build; 32 import android.os.PowerManager; 33 import android.os.RecoverySystem; 34 import android.os.SystemClock; 35 import android.os.SystemProperties; 36 import android.sysprop.CrashRecoveryProperties; 37 import android.text.TextUtils; 38 import android.util.EventLog; 39 import android.util.FileUtils; 40 import android.util.Log; 41 import android.util.Slog; 42 43 import com.android.internal.annotations.GuardedBy; 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.server.PackageWatchdog.FailureReasons; 46 import com.android.server.PackageWatchdog.PackageHealthObserver; 47 import com.android.server.PackageWatchdog.PackageHealthObserverImpact; 48 import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; 49 50 import java.io.File; 51 import java.lang.annotation.Retention; 52 import java.lang.annotation.RetentionPolicy; 53 import java.util.concurrent.TimeUnit; 54 55 /** 56 * Utilities to help rescue the system from crash loops. Callers are expected to 57 * report boot events and persistent app crashes, and if they happen frequently 58 * enough this class will slowly escalate through several rescue operations 59 * before finally rebooting and prompting the user if they want to wipe data as 60 * a last resort. 61 * 62 * @hide 63 */ 64 public class RescueParty { 65 @VisibleForTesting 66 static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue"; 67 @VisibleForTesting 68 static final int LEVEL_FACTORY_RESET = 5; 69 @VisibleForTesting 70 static final int RESCUE_LEVEL_NONE = 0; 71 @VisibleForTesting 72 static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1; 73 @VisibleForTesting 74 static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2; 75 @VisibleForTesting 76 static final int RESCUE_LEVEL_WARM_REBOOT = 3; 77 @VisibleForTesting 78 static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4; 79 @VisibleForTesting 80 static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5; 81 @VisibleForTesting 82 static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6; 83 @VisibleForTesting 84 static final int RESCUE_LEVEL_FACTORY_RESET = 7; 85 86 @IntDef(prefix = { "RESCUE_LEVEL_" }, value = { 87 RESCUE_LEVEL_NONE, 88 RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET, 89 RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET, 90 RESCUE_LEVEL_WARM_REBOOT, 91 RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS, 92 RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES, 93 RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS, 94 RESCUE_LEVEL_FACTORY_RESET 95 }) 96 @Retention(RetentionPolicy.SOURCE) 97 @interface RescueLevels {} 98 99 @VisibleForTesting 100 static final String TAG = "RescueParty"; 101 @VisibleForTesting 102 static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 1440; 103 104 private static final String NAME = "rescue-party-observer"; 105 106 private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue"; 107 private static final String PROP_VIRTUAL_DEVICE = "ro.hardware.virtual_device"; 108 private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG = 109 "persist.device_config.configuration.disable_rescue_party"; 110 private static final String PROP_DISABLE_FACTORY_RESET_FLAG = 111 "persist.device_config.configuration.disable_rescue_party_factory_reset"; 112 private static final String PROP_THROTTLE_DURATION_MIN_FLAG = 113 "persist.device_config.configuration.rescue_party_throttle_duration_min"; 114 115 private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT 116 | ApplicationInfo.FLAG_SYSTEM; 117 118 /** 119 * EventLog tags used when logging into the event log. Note the values must be sync with 120 * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct 121 * name translation. 122 */ 123 private static final int LOG_TAG_RESCUE_SUCCESS = 2902; 124 private static final int LOG_TAG_RESCUE_FAILURE = 2903; 125 126 /** Register the Rescue Party observer as a Package Watchdog health observer */ registerHealthObserver(Context context)127 public static void registerHealthObserver(Context context) { 128 PackageWatchdog.getInstance(context).registerHealthObserver( 129 context.getMainExecutor(), RescuePartyObserver.getInstance(context)); 130 } 131 isDisabled()132 private static boolean isDisabled() { 133 // Check if we're explicitly enabled for testing 134 if (SystemProperties.getBoolean(PROP_ENABLE_RESCUE, false)) { 135 return false; 136 } 137 138 // We're disabled if the DeviceConfig disable flag is set to true. 139 // This is in case that an emergency rollback of the feature is needed. 140 if (SystemProperties.getBoolean(PROP_DEVICE_CONFIG_DISABLE_FLAG, false)) { 141 Slog.v(TAG, "Disabled because of DeviceConfig flag"); 142 return true; 143 } 144 145 // We're disabled on all engineering devices 146 if (Build.TYPE.equals("eng")) { 147 Slog.v(TAG, "Disabled because of eng build"); 148 return true; 149 } 150 151 // We're disabled on userdebug devices connected over USB, since that's 152 // a decent signal that someone is actively trying to debug the device, 153 // or that it's in a lab environment. 154 if (Build.TYPE.equals("userdebug") && isUsbActive()) { 155 Slog.v(TAG, "Disabled because of active USB connection"); 156 return true; 157 } 158 159 // One last-ditch check 160 if (SystemProperties.getBoolean(PROP_DISABLE_RESCUE, false)) { 161 Slog.v(TAG, "Disabled because of manual property"); 162 return true; 163 } 164 165 return false; 166 } 167 168 /** 169 * Check if we're currently attempting to reboot for a factory reset. This method must 170 * return true if RescueParty tries to reboot early during a boot loop, since the device 171 * will not be fully booted at this time. 172 */ isRecoveryTriggeredReboot()173 public static boolean isRecoveryTriggeredReboot() { 174 return isFactoryResetPropertySet() || isRebootPropertySet(); 175 } 176 isFactoryResetPropertySet()177 static boolean isFactoryResetPropertySet() { 178 return CrashRecoveryProperties.attemptingFactoryReset().orElse(false); 179 } 180 isRebootPropertySet()181 static boolean isRebootPropertySet() { 182 return CrashRecoveryProperties.attemptingReboot().orElse(false); 183 } 184 getLastFactoryResetTimeMs()185 protected static long getLastFactoryResetTimeMs() { 186 return CrashRecoveryProperties.lastFactoryResetTimeMs().orElse(0L); 187 } 188 getMaxRescueLevelAttempted()189 protected static int getMaxRescueLevelAttempted() { 190 return CrashRecoveryProperties.maxRescueLevelAttempted().orElse(RESCUE_LEVEL_NONE); 191 } 192 setFactoryResetProperty(boolean value)193 protected static void setFactoryResetProperty(boolean value) { 194 CrashRecoveryProperties.attemptingFactoryReset(value); 195 } setRebootProperty(boolean value)196 protected static void setRebootProperty(boolean value) { 197 CrashRecoveryProperties.attemptingReboot(value); 198 } 199 setLastFactoryResetTimeMs(long value)200 protected static void setLastFactoryResetTimeMs(long value) { 201 CrashRecoveryProperties.lastFactoryResetTimeMs(value); 202 } 203 setMaxRescueLevelAttempted(int level)204 protected static void setMaxRescueLevelAttempted(int level) { 205 CrashRecoveryProperties.maxRescueLevelAttempted(level); 206 } 207 208 @VisibleForTesting getElapsedRealtime()209 static long getElapsedRealtime() { 210 return SystemClock.elapsedRealtime(); 211 } 212 getMaxRescueLevel()213 private static int getMaxRescueLevel() { 214 if (!SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { 215 return Level.factoryReset(); 216 } 217 return Level.reboot(); 218 } 219 220 /** 221 * Get the rescue level to perform if this is the n-th attempt at mitigating failure. 222 * 223 * @param mitigationCount the mitigation attempt number (1 = first attempt etc.). 224 * @return the rescue level for the n-th mitigation attempt. 225 */ getRescueLevel(int mitigationCount)226 private static @RescueLevels int getRescueLevel(int mitigationCount) { 227 if (mitigationCount == 1) { 228 return Level.reboot(); 229 } else if (mitigationCount >= 2) { 230 return Math.min(getMaxRescueLevel(), Level.factoryReset()); 231 } else { 232 return Level.none(); 233 } 234 } 235 executeRescueLevel(Context context, @Nullable String failedPackage, int level)236 private static void executeRescueLevel(Context context, @Nullable String failedPackage, 237 int level) { 238 Slog.w(TAG, "Attempting rescue level " + levelToString(level)); 239 try { 240 executeRescueLevelInternal(context, level, failedPackage); 241 EventLog.writeEvent(LOG_TAG_RESCUE_SUCCESS, level); 242 String successMsg = "Finished rescue level " + levelToString(level); 243 if (!TextUtils.isEmpty(failedPackage)) { 244 successMsg += " for package " + failedPackage; 245 } 246 logCrashRecoveryEvent(Log.DEBUG, successMsg); 247 } catch (Throwable t) { 248 logRescueException(level, failedPackage, t); 249 } 250 } 251 executeRescueLevelInternal(Context context, @RescueLevels int level, @Nullable String failedPackage)252 private static void executeRescueLevelInternal(Context context, @RescueLevels int level, 253 @Nullable String failedPackage) { 254 CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, 255 level, levelToString(level)); 256 switch (level) { 257 case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: 258 break; 259 case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: 260 break; 261 case RESCUE_LEVEL_WARM_REBOOT: 262 executeWarmReboot(context, level, failedPackage); 263 break; 264 case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: 265 // do nothing 266 break; 267 case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: 268 // do nothing 269 break; 270 case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: 271 // do nothing 272 break; 273 case RESCUE_LEVEL_FACTORY_RESET: 274 // Before the completion of Reboot, if any crash happens then PackageWatchdog 275 // escalates to next level i.e. factory reset, as they happen in separate threads. 276 // Adding a check to prevent factory reset to execute before above reboot completes. 277 // Note: this reboot property is not persistent resets after reboot is completed. 278 if (isRebootPropertySet()) { 279 return; 280 } 281 executeFactoryReset(context, level, failedPackage); 282 break; 283 } 284 } 285 executeWarmReboot(Context context, int level, @Nullable String failedPackage)286 private static void executeWarmReboot(Context context, int level, 287 @Nullable String failedPackage) { 288 if (shouldThrottleReboot()) { 289 return; 290 } 291 292 // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog 293 // when device shutting down. 294 setRebootProperty(true); 295 296 if (Flags.synchronousRebootInRescueParty()) { 297 try { 298 PowerManager pm = context.getSystemService(PowerManager.class); 299 if (pm != null) { 300 pm.reboot(TAG); 301 } 302 } catch (Throwable t) { 303 logRescueException(level, failedPackage, t); 304 } 305 } else { 306 Runnable runnable = () -> { 307 try { 308 PowerManager pm = context.getSystemService(PowerManager.class); 309 if (pm != null) { 310 pm.reboot(TAG); 311 } 312 } catch (Throwable t) { 313 logRescueException(level, failedPackage, t); 314 } 315 }; 316 Thread thread = new Thread(runnable); 317 thread.start(); 318 } 319 } 320 executeFactoryReset(Context context, int level, @Nullable String failedPackage)321 private static void executeFactoryReset(Context context, int level, 322 @Nullable String failedPackage) { 323 if (shouldThrottleReboot()) { 324 return; 325 } 326 setFactoryResetProperty(true); 327 long now = System.currentTimeMillis(); 328 setLastFactoryResetTimeMs(now); 329 330 if (Flags.synchronousRebootInRescueParty()) { 331 try { 332 RecoverySystem.rebootPromptAndWipeUserData(context, TAG + "," + failedPackage); 333 } catch (Throwable t) { 334 logRescueException(level, failedPackage, t); 335 } 336 } else { 337 Runnable runnable = new Runnable() { 338 @Override 339 public void run() { 340 try { 341 RecoverySystem.rebootPromptAndWipeUserData(context, 342 TAG + "," + failedPackage); 343 } catch (Throwable t) { 344 logRescueException(level, failedPackage, t); 345 } 346 } 347 }; 348 Thread thread = new Thread(runnable); 349 thread.start(); 350 } 351 } 352 353 getCompleteMessage(Throwable t)354 private static String getCompleteMessage(Throwable t) { 355 final StringBuilder builder = new StringBuilder(); 356 builder.append(t.getMessage()); 357 while ((t = t.getCause()) != null) { 358 builder.append(": ").append(t.getMessage()); 359 } 360 return builder.toString(); 361 } 362 logRescueException(int level, @Nullable String failedPackageName, Throwable t)363 private static void logRescueException(int level, @Nullable String failedPackageName, 364 Throwable t) { 365 final String msg = getCompleteMessage(t); 366 EventLog.writeEvent(LOG_TAG_RESCUE_FAILURE, level, msg); 367 String failureMsg = "Failed rescue level " + levelToString(level); 368 if (!TextUtils.isEmpty(failedPackageName)) { 369 failureMsg += " for package " + failedPackageName; 370 } 371 logCrashRecoveryEvent(Log.ERROR, failureMsg + ": " + msg); 372 } 373 mapRescueLevelToUserImpact(int rescueLevel)374 private static int mapRescueLevelToUserImpact(int rescueLevel) { 375 switch (rescueLevel) { 376 case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: 377 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; 378 case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: 379 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_40; 380 case RESCUE_LEVEL_WARM_REBOOT: 381 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; 382 case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: 383 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; 384 case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: 385 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75; 386 case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: 387 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80; 388 case RESCUE_LEVEL_FACTORY_RESET: 389 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; 390 default: 391 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; 392 } 393 } 394 395 /** 396 * Handle mitigation action for package failures. This observer will be register to Package 397 * Watchdog and will receive calls about package failures. This observer is persistent so it 398 * may choose to mitigate failures for packages it has not explicitly asked to observe. 399 */ 400 public static class RescuePartyObserver implements PackageHealthObserver { 401 402 private final Context mContext; 403 404 @GuardedBy("RescuePartyObserver.class") 405 static RescuePartyObserver sRescuePartyObserver; 406 RescuePartyObserver(Context context)407 private RescuePartyObserver(Context context) { 408 mContext = context; 409 } 410 411 /** Creates or gets singleton instance of RescueParty. */ getInstance(Context context)412 public static RescuePartyObserver getInstance(Context context) { 413 synchronized (RescuePartyObserver.class) { 414 if (sRescuePartyObserver == null) { 415 sRescuePartyObserver = new RescuePartyObserver(context); 416 } 417 return sRescuePartyObserver; 418 } 419 } 420 421 @VisibleForTesting reset()422 static void reset() { 423 synchronized (RescuePartyObserver.class) { 424 sRescuePartyObserver = null; 425 } 426 } 427 428 @Override onHealthCheckFailed(@ullable VersionedPackage failedPackage, @FailureReasons int failureReason, int mitigationCount)429 public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage, 430 @FailureReasons int failureReason, int mitigationCount) { 431 int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; 432 if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH 433 || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) { 434 impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount)); 435 } 436 437 Slog.i(TAG, "Checking available remediations for health check failure." 438 + " failedPackage: " 439 + (failedPackage == null ? null : failedPackage.getPackageName()) 440 + " failureReason: " + failureReason 441 + " available impact: " + impact); 442 return impact; 443 } 444 445 @Override onExecuteHealthCheckMitigation(@ullable VersionedPackage failedPackage, @FailureReasons int failureReason, int mitigationCount)446 public int onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage, 447 @FailureReasons int failureReason, int mitigationCount) { 448 if (isDisabled()) { 449 return MITIGATION_RESULT_SKIPPED; 450 } 451 Slog.i(TAG, "Executing remediation." 452 + " failedPackage: " 453 + (failedPackage == null ? null : failedPackage.getPackageName()) 454 + " failureReason: " + failureReason 455 + " mitigationCount: " + mitigationCount); 456 if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH 457 || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) { 458 final int level; 459 level = getRescueLevel(mitigationCount); 460 executeRescueLevel(mContext, 461 failedPackage == null ? null : failedPackage.getPackageName(), level); 462 return MITIGATION_RESULT_SUCCESS; 463 } else { 464 return MITIGATION_RESULT_SKIPPED; 465 } 466 } 467 468 @Override isPersistent()469 public boolean isPersistent() { 470 return true; 471 } 472 473 @Override mayObservePackage(@onNull String packageName)474 public boolean mayObservePackage(@NonNull String packageName) { 475 PackageManager pm = mContext.getPackageManager(); 476 try { 477 // A package is a module if this is non-null 478 if (pm.getModuleInfo(packageName, 0) != null) { 479 return true; 480 } 481 } catch (PackageManager.NameNotFoundException | IllegalStateException ignore) { 482 } 483 484 return isPersistentSystemApp(packageName); 485 } 486 487 @Override onBootLoop(int mitigationCount)488 public int onBootLoop(int mitigationCount) { 489 if (isDisabled()) { 490 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; 491 } 492 return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount)); 493 } 494 495 @Override onExecuteBootLoopMitigation(int mitigationCount)496 public int onExecuteBootLoopMitigation(int mitigationCount) { 497 if (isDisabled()) { 498 return MITIGATION_RESULT_SKIPPED; 499 } 500 final int level; 501 level = getRescueLevel(mitigationCount); 502 executeRescueLevel(mContext, /*failedPackage=*/ null, level); 503 return MITIGATION_RESULT_SUCCESS; 504 } 505 506 @Override getUniqueIdentifier()507 public String getUniqueIdentifier() { 508 return NAME; 509 } 510 isPersistentSystemApp(@onNull String packageName)511 private boolean isPersistentSystemApp(@NonNull String packageName) { 512 PackageManager pm = mContext.getPackageManager(); 513 try { 514 ApplicationInfo info = pm.getApplicationInfo(packageName, 0); 515 return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK; 516 } catch (PackageManager.NameNotFoundException e) { 517 return false; 518 } 519 } 520 521 } 522 523 /** 524 * Returns {@code true} if Rescue Party is allowed to attempt a reboot or factory reset. 525 * Will return {@code false} if a factory reset was already offered recently. 526 */ shouldThrottleReboot()527 private static boolean shouldThrottleReboot() { 528 Long lastResetTime = getLastFactoryResetTimeMs(); 529 long now = System.currentTimeMillis(); 530 long throttleDurationMin = SystemProperties.getLong(PROP_THROTTLE_DURATION_MIN_FLAG, 531 DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN); 532 return now < lastResetTime + TimeUnit.MINUTES.toMillis(throttleDurationMin); 533 } 534 535 /** 536 * Hacky test to check if the device has an active USB connection, which is 537 * a good proxy for someone doing local development work. 538 */ isUsbActive()539 private static boolean isUsbActive() { 540 if (SystemProperties.getBoolean(PROP_VIRTUAL_DEVICE, false)) { 541 Slog.v(TAG, "Assuming virtual device is connected over USB"); 542 return true; 543 } 544 try { 545 final String state = FileUtils 546 .readTextFile(new File("/sys/class/android_usb/android0/state"), 128, ""); 547 return "CONFIGURED".equals(state.trim()); 548 } catch (Throwable t) { 549 Slog.w(TAG, "Failed to determine if device was on USB", t); 550 return false; 551 } 552 } 553 554 private static class Level { none()555 static int none() { 556 return RESCUE_LEVEL_NONE; 557 } 558 reboot()559 static int reboot() { 560 return RESCUE_LEVEL_WARM_REBOOT; 561 } 562 factoryReset()563 static int factoryReset() { 564 return RESCUE_LEVEL_FACTORY_RESET; 565 } 566 } 567 levelToString(int level)568 private static String levelToString(int level) { 569 switch (level) { 570 case RESCUE_LEVEL_NONE: 571 return "NONE"; 572 case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: 573 return "SCOPED_DEVICE_CONFIG_RESET"; 574 case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: 575 return "ALL_DEVICE_CONFIG_RESET"; 576 case RESCUE_LEVEL_WARM_REBOOT: 577 return "WARM_REBOOT"; 578 case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: 579 return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; 580 case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: 581 return "RESET_SETTINGS_UNTRUSTED_CHANGES"; 582 case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: 583 return "RESET_SETTINGS_TRUSTED_DEFAULTS"; 584 case RESCUE_LEVEL_FACTORY_RESET: 585 return "FACTORY_RESET"; 586 default: 587 return Integer.toString(level); 588 } 589 } 590 } 591