1 /* 2 * Copyright (C) 2010 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 android.os; 18 19 import static java.nio.charset.StandardCharsets.UTF_8; 20 21 import android.annotation.RequiresPermission; 22 import android.annotation.SuppressLint; 23 import android.annotation.SystemApi; 24 import android.annotation.SystemService; 25 import android.app.PendingIntent; 26 import android.compat.annotation.UnsupportedAppUsage; 27 import android.content.BroadcastReceiver; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.pm.PackageManager; 33 import android.provider.Settings; 34 import android.telephony.SubscriptionInfo; 35 import android.telephony.SubscriptionManager; 36 import android.telephony.euicc.EuiccManager; 37 import android.text.TextUtils; 38 import android.text.format.DateFormat; 39 import android.util.Log; 40 import android.view.Display; 41 import android.view.WindowManager; 42 43 import libcore.io.Streams; 44 45 import java.io.ByteArrayInputStream; 46 import java.io.File; 47 import java.io.FileInputStream; 48 import java.io.FileNotFoundException; 49 import java.io.FileWriter; 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.io.RandomAccessFile; 53 import java.security.GeneralSecurityException; 54 import java.security.PublicKey; 55 import java.security.SignatureException; 56 import java.security.cert.CertificateFactory; 57 import java.security.cert.X509Certificate; 58 import java.util.ArrayList; 59 import java.util.Enumeration; 60 import java.util.HashSet; 61 import java.util.List; 62 import java.util.Locale; 63 import java.util.concurrent.CountDownLatch; 64 import java.util.concurrent.TimeUnit; 65 import java.util.concurrent.atomic.AtomicBoolean; 66 import java.util.concurrent.atomic.AtomicInteger; 67 import java.util.zip.ZipEntry; 68 import java.util.zip.ZipFile; 69 import java.util.zip.ZipInputStream; 70 71 import sun.security.pkcs.PKCS7; 72 import sun.security.pkcs.SignerInfo; 73 74 /** 75 * RecoverySystem contains methods for interacting with the Android 76 * recovery system (the separate partition that can be used to install 77 * system updates, wipe user data, etc.) 78 */ 79 @SystemService(Context.RECOVERY_SERVICE) 80 public class RecoverySystem { 81 private static final String TAG = "RecoverySystem"; 82 83 /** 84 * Default location of zip file containing public keys (X509 85 * certs) authorized to sign OTA updates. 86 */ 87 private static final File DEFAULT_KEYSTORE = 88 new File("/system/etc/security/otacerts.zip"); 89 90 /** Send progress to listeners no more often than this (in ms). */ 91 private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; 92 93 private static final long DEFAULT_EUICC_FACTORY_RESET_TIMEOUT_MILLIS = 30000L; // 30 s 94 private static final long MIN_EUICC_FACTORY_RESET_TIMEOUT_MILLIS = 5000L; // 5 s 95 private static final long MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS = 60000L; // 60 s 96 97 private static final long DEFAULT_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS = 98 45000L; // 45 s 99 private static final long MIN_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS = 15000L; // 15 s 100 private static final long MAX_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS = 90000L; // 90 s 101 102 /** Used to communicate with recovery. See bootable/recovery/recovery.cpp. */ 103 private static final File RECOVERY_DIR = new File("/cache/recovery"); 104 private static final File LOG_FILE = new File(RECOVERY_DIR, "log"); 105 private static final String LAST_INSTALL_PATH = "last_install"; 106 private static final String LAST_PREFIX = "last_"; 107 private static final String ACTION_EUICC_FACTORY_RESET = 108 "com.android.internal.action.EUICC_FACTORY_RESET"; 109 private static final String ACTION_EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS = 110 "com.android.internal.action.EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS"; 111 112 /** 113 * Used in {@link #wipeEuiccData} & {@link #removeEuiccInvisibleSubs} as package name of 114 * callback intent. 115 */ 116 private static final String PACKAGE_NAME_EUICC_DATA_MANAGEMENT_CALLBACK = "android"; 117 118 /** 119 * The recovery image uses this file to identify the location (i.e. blocks) 120 * of an OTA package on the /data partition. The block map file is 121 * generated by uncrypt. 122 * 123 * @hide 124 */ 125 public static final File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map"); 126 127 /** 128 * UNCRYPT_PACKAGE_FILE stores the filename to be uncrypt'd, which will be 129 * read by uncrypt. 130 * 131 * @hide 132 */ 133 public static final File UNCRYPT_PACKAGE_FILE = new File(RECOVERY_DIR, "uncrypt_file"); 134 135 /** 136 * UNCRYPT_STATUS_FILE stores the time cost (and error code in the case of a failure) 137 * of uncrypt. 138 * 139 * @hide 140 */ 141 public static final File UNCRYPT_STATUS_FILE = new File(RECOVERY_DIR, "uncrypt_status"); 142 143 // Length limits for reading files. 144 private static final int LOG_FILE_MAX_LENGTH = 64 * 1024; 145 146 // Prevent concurrent execution of requests. 147 private static final Object sRequestLock = new Object(); 148 149 private final IRecoverySystem mService; 150 151 /** 152 * Interface definition for a callback to be invoked regularly as 153 * verification proceeds. 154 */ 155 public interface ProgressListener { 156 /** 157 * Called periodically as the verification progresses. 158 * 159 * @param progress the approximate percentage of the 160 * verification that has been completed, ranging from 0 161 * to 100 (inclusive). 162 */ onProgress(int progress)163 public void onProgress(int progress); 164 } 165 166 /** @return the set of certs that can be used to sign an OTA package. */ getTrustedCerts(File keystore)167 private static HashSet<X509Certificate> getTrustedCerts(File keystore) 168 throws IOException, GeneralSecurityException { 169 HashSet<X509Certificate> trusted = new HashSet<X509Certificate>(); 170 if (keystore == null) { 171 keystore = DEFAULT_KEYSTORE; 172 } 173 ZipFile zip = new ZipFile(keystore); 174 try { 175 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 176 Enumeration<? extends ZipEntry> entries = zip.entries(); 177 while (entries.hasMoreElements()) { 178 ZipEntry entry = entries.nextElement(); 179 InputStream is = zip.getInputStream(entry); 180 try { 181 trusted.add((X509Certificate) cf.generateCertificate(is)); 182 } finally { 183 is.close(); 184 } 185 } 186 } finally { 187 zip.close(); 188 } 189 return trusted; 190 } 191 192 /** 193 * Verify the cryptographic signature of a system update package 194 * before installing it. Note that the package is also verified 195 * separately by the installer once the device is rebooted into 196 * the recovery system. This function will return only if the 197 * package was successfully verified; otherwise it will throw an 198 * exception. 199 * 200 * Verification of a package can take significant time, so this 201 * function should not be called from a UI thread. Interrupting 202 * the thread while this function is in progress will result in a 203 * SecurityException being thrown (and the thread's interrupt flag 204 * will be cleared). 205 * 206 * @param packageFile the package to be verified 207 * @param listener an object to receive periodic progress 208 * updates as verification proceeds. May be null. 209 * @param deviceCertsZipFile the zip file of certificates whose 210 * public keys we will accept. Verification succeeds if the 211 * package is signed by the private key corresponding to any 212 * public key in this file. May be null to use the system default 213 * file (currently "/system/etc/security/otacerts.zip"). 214 * 215 * @throws IOException if there were any errors reading the 216 * package or certs files. 217 * @throws GeneralSecurityException if verification failed 218 */ verifyPackage(File packageFile, ProgressListener listener, File deviceCertsZipFile)219 public static void verifyPackage(File packageFile, 220 ProgressListener listener, 221 File deviceCertsZipFile) 222 throws IOException, GeneralSecurityException { 223 final long fileLen = packageFile.length(); 224 225 final RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); 226 try { 227 final long startTimeMillis = System.currentTimeMillis(); 228 if (listener != null) { 229 listener.onProgress(0); 230 } 231 232 raf.seek(fileLen - 6); 233 byte[] footer = new byte[6]; 234 raf.readFully(footer); 235 236 if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) { 237 throw new SignatureException("no signature in file (no footer)"); 238 } 239 240 final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); 241 final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); 242 243 byte[] eocd = new byte[commentSize + 22]; 244 raf.seek(fileLen - (commentSize + 22)); 245 raf.readFully(eocd); 246 247 // Check that we have found the start of the 248 // end-of-central-directory record. 249 if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b || 250 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) { 251 throw new SignatureException("no signature in file (bad footer)"); 252 } 253 254 for (int i = 4; i < eocd.length-3; ++i) { 255 if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b && 256 eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) { 257 throw new SignatureException("EOCD marker found after start of EOCD"); 258 } 259 } 260 261 // Parse the signature 262 PKCS7 block = 263 new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); 264 265 // Take the first certificate from the signature (packages 266 // should contain only one). 267 X509Certificate[] certificates = block.getCertificates(); 268 if (certificates == null || certificates.length == 0) { 269 throw new SignatureException("signature contains no certificates"); 270 } 271 X509Certificate cert = certificates[0]; 272 PublicKey signatureKey = cert.getPublicKey(); 273 274 SignerInfo[] signerInfos = block.getSignerInfos(); 275 if (signerInfos == null || signerInfos.length == 0) { 276 throw new SignatureException("signature contains no signedData"); 277 } 278 SignerInfo signerInfo = signerInfos[0]; 279 280 // Check that the public key of the certificate contained 281 // in the package equals one of our trusted public keys. 282 boolean verified = false; 283 HashSet<X509Certificate> trusted = getTrustedCerts( 284 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); 285 for (X509Certificate c : trusted) { 286 if (c.getPublicKey().equals(signatureKey)) { 287 verified = true; 288 break; 289 } 290 } 291 if (!verified) { 292 throw new SignatureException("signature doesn't match any trusted key"); 293 } 294 295 // The signature cert matches a trusted key. Now verify that 296 // the digest in the cert matches the actual file data. 297 raf.seek(0); 298 final ProgressListener listenerForInner = listener; 299 SignerInfo verifyResult = block.verify(signerInfo, new InputStream() { 300 // The signature covers all of the OTA package except the 301 // archive comment and its 2-byte length. 302 long toRead = fileLen - commentSize - 2; 303 long soFar = 0; 304 305 int lastPercent = 0; 306 long lastPublishTime = startTimeMillis; 307 308 @Override 309 public int read() throws IOException { 310 throw new UnsupportedOperationException(); 311 } 312 313 @Override 314 public int read(byte[] b, int off, int len) throws IOException { 315 if (soFar >= toRead) { 316 return -1; 317 } 318 if (Thread.currentThread().isInterrupted()) { 319 return -1; 320 } 321 322 int size = len; 323 if (soFar + size > toRead) { 324 size = (int)(toRead - soFar); 325 } 326 int read = raf.read(b, off, size); 327 soFar += read; 328 329 if (listenerForInner != null) { 330 long now = System.currentTimeMillis(); 331 int p = (int)(soFar * 100 / toRead); 332 if (p > lastPercent && 333 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { 334 lastPercent = p; 335 lastPublishTime = now; 336 listenerForInner.onProgress(lastPercent); 337 } 338 } 339 340 return read; 341 } 342 }); 343 344 final boolean interrupted = Thread.interrupted(); 345 if (listener != null) { 346 listener.onProgress(100); 347 } 348 349 if (interrupted) { 350 throw new SignatureException("verification was interrupted"); 351 } 352 353 if (verifyResult == null) { 354 throw new SignatureException("signature digest verification failed"); 355 } 356 } finally { 357 raf.close(); 358 } 359 360 // Additionally verify the package compatibility. 361 if (!readAndVerifyPackageCompatibilityEntry(packageFile)) { 362 throw new SignatureException("package compatibility verification failed"); 363 } 364 } 365 366 /** 367 * Verifies the compatibility entry from an {@link InputStream}. 368 * 369 * @return the verification result. 370 */ 371 @UnsupportedAppUsage verifyPackageCompatibility(InputStream inputStream)372 private static boolean verifyPackageCompatibility(InputStream inputStream) throws IOException { 373 ArrayList<String> list = new ArrayList<>(); 374 ZipInputStream zis = new ZipInputStream(inputStream); 375 ZipEntry entry; 376 while ((entry = zis.getNextEntry()) != null) { 377 long entrySize = entry.getSize(); 378 if (entrySize > Integer.MAX_VALUE || entrySize < 0) { 379 throw new IOException( 380 "invalid entry size (" + entrySize + ") in the compatibility file"); 381 } 382 byte[] bytes = new byte[(int) entrySize]; 383 Streams.readFully(zis, bytes); 384 list.add(new String(bytes, UTF_8)); 385 } 386 if (list.isEmpty()) { 387 throw new IOException("no entries found in the compatibility file"); 388 } 389 return (VintfObject.verify(list.toArray(new String[list.size()])) == 0); 390 } 391 392 /** 393 * Reads and verifies the compatibility entry in an OTA zip package. The compatibility entry is 394 * a zip file (inside the OTA package zip). 395 * 396 * @return {@code true} if the entry doesn't exist or verification passes. 397 */ readAndVerifyPackageCompatibilityEntry(File packageFile)398 private static boolean readAndVerifyPackageCompatibilityEntry(File packageFile) 399 throws IOException { 400 try (ZipFile zip = new ZipFile(packageFile)) { 401 ZipEntry entry = zip.getEntry("compatibility.zip"); 402 if (entry == null) { 403 return true; 404 } 405 InputStream inputStream = zip.getInputStream(entry); 406 return verifyPackageCompatibility(inputStream); 407 } 408 } 409 410 /** 411 * Verifies the package compatibility info against the current system. 412 * 413 * @param compatibilityFile the {@link File} that contains the package compatibility info. 414 * @throws IOException if there were any errors reading the compatibility file. 415 * @return the compatibility verification result. 416 * 417 * {@hide} 418 */ 419 @SystemApi 420 @SuppressLint("Doclava125") verifyPackageCompatibility(File compatibilityFile)421 public static boolean verifyPackageCompatibility(File compatibilityFile) throws IOException { 422 try (InputStream inputStream = new FileInputStream(compatibilityFile)) { 423 return verifyPackageCompatibility(inputStream); 424 } 425 } 426 427 /** 428 * Process a given package with uncrypt. No-op if the package is not on the 429 * /data partition. 430 * 431 * @param Context the Context to use 432 * @param packageFile the package to be processed 433 * @param listener an object to receive periodic progress updates as 434 * processing proceeds. May be null. 435 * @param handler the Handler upon which the callbacks will be 436 * executed. 437 * 438 * @throws IOException if there were any errors processing the package file. 439 * 440 * @hide 441 */ 442 @SystemApi 443 @RequiresPermission(android.Manifest.permission.RECOVERY) processPackage(Context context, File packageFile, final ProgressListener listener, final Handler handler)444 public static void processPackage(Context context, 445 File packageFile, 446 final ProgressListener listener, 447 final Handler handler) 448 throws IOException { 449 String filename = packageFile.getCanonicalPath(); 450 if (!filename.startsWith("/data/")) { 451 return; 452 } 453 454 RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); 455 IRecoverySystemProgressListener progressListener = null; 456 if (listener != null) { 457 final Handler progressHandler; 458 if (handler != null) { 459 progressHandler = handler; 460 } else { 461 progressHandler = new Handler(context.getMainLooper()); 462 } 463 progressListener = new IRecoverySystemProgressListener.Stub() { 464 int lastProgress = 0; 465 long lastPublishTime = System.currentTimeMillis(); 466 467 @Override 468 public void onProgress(final int progress) { 469 final long now = System.currentTimeMillis(); 470 progressHandler.post(new Runnable() { 471 @Override 472 public void run() { 473 if (progress > lastProgress && 474 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { 475 lastProgress = progress; 476 lastPublishTime = now; 477 listener.onProgress(progress); 478 } 479 } 480 }); 481 } 482 }; 483 } 484 485 if (!rs.uncrypt(filename, progressListener)) { 486 throw new IOException("process package failed"); 487 } 488 } 489 490 /** 491 * Process a given package with uncrypt. No-op if the package is not on the 492 * /data partition. 493 * 494 * @param Context the Context to use 495 * @param packageFile the package to be processed 496 * @param listener an object to receive periodic progress updates as 497 * processing proceeds. May be null. 498 * 499 * @throws IOException if there were any errors processing the package file. 500 * 501 * @hide 502 */ 503 @SystemApi 504 @RequiresPermission(android.Manifest.permission.RECOVERY) processPackage(Context context, File packageFile, final ProgressListener listener)505 public static void processPackage(Context context, 506 File packageFile, 507 final ProgressListener listener) 508 throws IOException { 509 processPackage(context, packageFile, listener, null); 510 } 511 512 /** 513 * Reboots the device in order to install the given update 514 * package. 515 * Requires the {@link android.Manifest.permission#REBOOT} permission. 516 * 517 * @param context the Context to use 518 * @param packageFile the update package to install. Must be on 519 * a partition mountable by recovery. (The set of partitions 520 * known to recovery may vary from device to device. Generally, 521 * /cache and /data are safe.) 522 * 523 * @throws IOException if writing the recovery command file 524 * fails, or if the reboot itself fails. 525 */ 526 @RequiresPermission(android.Manifest.permission.RECOVERY) installPackage(Context context, File packageFile)527 public static void installPackage(Context context, File packageFile) 528 throws IOException { 529 installPackage(context, packageFile, false); 530 } 531 532 /** 533 * If the package hasn't been processed (i.e. uncrypt'd), set up 534 * UNCRYPT_PACKAGE_FILE and delete BLOCK_MAP_FILE to trigger uncrypt during the 535 * reboot. 536 * 537 * @param context the Context to use 538 * @param packageFile the update package to install. Must be on a 539 * partition mountable by recovery. 540 * @param processed if the package has been processed (uncrypt'd). 541 * 542 * @throws IOException if writing the recovery command file fails, or if 543 * the reboot itself fails. 544 * 545 * @hide 546 */ 547 @SystemApi 548 @RequiresPermission(android.Manifest.permission.RECOVERY) installPackage(Context context, File packageFile, boolean processed)549 public static void installPackage(Context context, File packageFile, boolean processed) 550 throws IOException { 551 synchronized (sRequestLock) { 552 LOG_FILE.delete(); 553 // Must delete the file in case it was created by system server. 554 UNCRYPT_PACKAGE_FILE.delete(); 555 556 String filename = packageFile.getCanonicalPath(); 557 Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); 558 559 // If the package name ends with "_s.zip", it's a security update. 560 boolean securityUpdate = filename.endsWith("_s.zip"); 561 562 // If the package is on the /data partition, the package needs to 563 // be processed (i.e. uncrypt'd). The caller specifies if that has 564 // been done in 'processed' parameter. 565 if (filename.startsWith("/data/")) { 566 if (processed) { 567 if (!BLOCK_MAP_FILE.exists()) { 568 Log.e(TAG, "Package claimed to have been processed but failed to find " 569 + "the block map file."); 570 throw new IOException("Failed to find block map file"); 571 } 572 } else { 573 FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE); 574 try { 575 uncryptFile.write(filename + "\n"); 576 } finally { 577 uncryptFile.close(); 578 } 579 // UNCRYPT_PACKAGE_FILE needs to be readable and writable 580 // by system server. 581 if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false) 582 || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) { 583 Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE); 584 } 585 586 BLOCK_MAP_FILE.delete(); 587 } 588 589 // If the package is on the /data partition, use the block map 590 // file as the package name instead. 591 filename = "@/cache/recovery/block.map"; 592 } 593 594 final String filenameArg = "--update_package=" + filename + "\n"; 595 final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() + "\n"; 596 final String securityArg = "--security\n"; 597 598 String command = filenameArg + localeArg; 599 if (securityUpdate) { 600 command += securityArg; 601 } 602 603 RecoverySystem rs = (RecoverySystem) context.getSystemService( 604 Context.RECOVERY_SERVICE); 605 if (!rs.setupBcb(command)) { 606 throw new IOException("Setup BCB failed"); 607 } 608 609 // Having set up the BCB (bootloader control block), go ahead and reboot 610 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 611 String reason = PowerManager.REBOOT_RECOVERY_UPDATE; 612 613 // On TV, reboot quiescently if the screen is off 614 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { 615 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 616 if (wm.getDefaultDisplay().getState() != Display.STATE_ON) { 617 reason += ",quiescent"; 618 } 619 } 620 pm.reboot(reason); 621 622 throw new IOException("Reboot failed (no permissions?)"); 623 } 624 } 625 626 /** 627 * Schedule to install the given package on next boot. The caller needs to 628 * ensure that the package must have been processed (uncrypt'd) if needed. 629 * It sets up the command in BCB (bootloader control block), which will 630 * be read by the bootloader and the recovery image. 631 * 632 * @param Context the Context to use. 633 * @param packageFile the package to be installed. 634 * 635 * @throws IOException if there were any errors setting up the BCB. 636 * 637 * @hide 638 */ 639 @SystemApi 640 @RequiresPermission(android.Manifest.permission.RECOVERY) scheduleUpdateOnBoot(Context context, File packageFile)641 public static void scheduleUpdateOnBoot(Context context, File packageFile) 642 throws IOException { 643 String filename = packageFile.getCanonicalPath(); 644 boolean securityUpdate = filename.endsWith("_s.zip"); 645 646 // If the package is on the /data partition, use the block map file as 647 // the package name instead. 648 if (filename.startsWith("/data/")) { 649 filename = "@/cache/recovery/block.map"; 650 } 651 652 final String filenameArg = "--update_package=" + filename + "\n"; 653 final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() + "\n"; 654 final String securityArg = "--security\n"; 655 656 String command = filenameArg + localeArg; 657 if (securityUpdate) { 658 command += securityArg; 659 } 660 661 RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); 662 if (!rs.setupBcb(command)) { 663 throw new IOException("schedule update on boot failed"); 664 } 665 } 666 667 /** 668 * Cancel any scheduled update by clearing up the BCB (bootloader control 669 * block). 670 * 671 * @param Context the Context to use. 672 * 673 * @throws IOException if there were any errors clearing up the BCB. 674 * 675 * @hide 676 */ 677 @SystemApi 678 @RequiresPermission(android.Manifest.permission.RECOVERY) cancelScheduledUpdate(Context context)679 public static void cancelScheduledUpdate(Context context) 680 throws IOException { 681 RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); 682 if (!rs.clearBcb()) { 683 throw new IOException("cancel scheduled update failed"); 684 } 685 } 686 687 /** 688 * Reboots the device and wipes the user data and cache 689 * partitions. This is sometimes called a "factory reset", which 690 * is something of a misnomer because the system partition is not 691 * restored to its factory state. Requires the 692 * {@link android.Manifest.permission#REBOOT} permission. 693 * 694 * @param context the Context to use 695 * 696 * @throws IOException if writing the recovery command file 697 * fails, or if the reboot itself fails. 698 * @throws SecurityException if the current user is not allowed to wipe data. 699 */ rebootWipeUserData(Context context)700 public static void rebootWipeUserData(Context context) throws IOException { 701 rebootWipeUserData(context, false /* shutdown */, context.getPackageName(), 702 false /* force */, false /* wipeEuicc */); 703 } 704 705 /** {@hide} */ rebootWipeUserData(Context context, String reason)706 public static void rebootWipeUserData(Context context, String reason) throws IOException { 707 rebootWipeUserData(context, false /* shutdown */, reason, false /* force */, 708 false /* wipeEuicc */); 709 } 710 711 /** {@hide} */ rebootWipeUserData(Context context, boolean shutdown)712 public static void rebootWipeUserData(Context context, boolean shutdown) 713 throws IOException { 714 rebootWipeUserData(context, shutdown, context.getPackageName(), false /* force */, 715 false /* wipeEuicc */); 716 } 717 718 /** {@hide} */ rebootWipeUserData(Context context, boolean shutdown, String reason, boolean force)719 public static void rebootWipeUserData(Context context, boolean shutdown, String reason, 720 boolean force) throws IOException { 721 rebootWipeUserData(context, shutdown, reason, force, false /* wipeEuicc */); 722 } 723 724 /** 725 * Reboots the device and wipes the user data and cache 726 * partitions. This is sometimes called a "factory reset", which 727 * is something of a misnomer because the system partition is not 728 * restored to its factory state. Requires the 729 * {@link android.Manifest.permission#REBOOT} permission. 730 * 731 * @param context the Context to use 732 * @param shutdown if true, the device will be powered down after 733 * the wipe completes, rather than being rebooted 734 * back to the regular system. 735 * @param reason the reason for the wipe that is visible in the logs 736 * @param force whether the {@link UserManager.DISALLOW_FACTORY_RESET} user restriction 737 * should be ignored 738 * @param wipeEuicc whether wipe the euicc data 739 * 740 * @throws IOException if writing the recovery command file 741 * fails, or if the reboot itself fails. 742 * @throws SecurityException if the current user is not allowed to wipe data. 743 * 744 * @hide 745 */ rebootWipeUserData(Context context, boolean shutdown, String reason, boolean force, boolean wipeEuicc)746 public static void rebootWipeUserData(Context context, boolean shutdown, String reason, 747 boolean force, boolean wipeEuicc) throws IOException { 748 UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); 749 if (!force && um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) { 750 throw new SecurityException("Wiping data is not allowed for this user."); 751 } 752 final ConditionVariable condition = new ConditionVariable(); 753 754 Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION"); 755 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND 756 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 757 context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM, 758 android.Manifest.permission.MASTER_CLEAR, 759 new BroadcastReceiver() { 760 @Override 761 public void onReceive(Context context, Intent intent) { 762 condition.open(); 763 } 764 }, null, 0, null, null); 765 766 // Block until the ordered broadcast has completed. 767 condition.block(); 768 769 EuiccManager euiccManager = context.getSystemService(EuiccManager.class); 770 if (wipeEuicc) { 771 wipeEuiccData(context, PACKAGE_NAME_EUICC_DATA_MANAGEMENT_CALLBACK); 772 } else { 773 removeEuiccInvisibleSubs(context, euiccManager); 774 } 775 776 String shutdownArg = null; 777 if (shutdown) { 778 shutdownArg = "--shutdown_after"; 779 } 780 781 String reasonArg = null; 782 if (!TextUtils.isEmpty(reason)) { 783 String timeStamp = DateFormat.format("yyyy-MM-ddTHH:mm:ssZ", System.currentTimeMillis()).toString(); 784 reasonArg = "--reason=" + sanitizeArg(reason + "," + timeStamp); 785 } 786 787 final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ; 788 bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg); 789 } 790 791 /** 792 * Returns whether wipe Euicc data successfully or not. 793 * 794 * @param packageName the package name of the caller app. 795 * 796 * @hide 797 */ wipeEuiccData(Context context, final String packageName)798 public static boolean wipeEuiccData(Context context, final String packageName) { 799 ContentResolver cr = context.getContentResolver(); 800 if (Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) == 0) { 801 // If the eUICC isn't provisioned, there's no reason to either wipe or retain profiles, 802 // as there's nothing to wipe nor retain. 803 Log.d(TAG, "Skipping eUICC wipe/retain as it is not provisioned"); 804 return true; 805 } 806 807 EuiccManager euiccManager = (EuiccManager) context.getSystemService( 808 Context.EUICC_SERVICE); 809 if (euiccManager != null && euiccManager.isEnabled()) { 810 CountDownLatch euiccFactoryResetLatch = new CountDownLatch(1); 811 final AtomicBoolean wipingSucceeded = new AtomicBoolean(false); 812 813 BroadcastReceiver euiccWipeFinishReceiver = new BroadcastReceiver() { 814 @Override 815 public void onReceive(Context context, Intent intent) { 816 if (ACTION_EUICC_FACTORY_RESET.equals(intent.getAction())) { 817 if (getResultCode() != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) { 818 int detailedCode = intent.getIntExtra( 819 EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, 0); 820 Log.e(TAG, "Error wiping euicc data, Detailed code = " 821 + detailedCode); 822 } else { 823 Log.d(TAG, "Successfully wiped euicc data."); 824 wipingSucceeded.set(true /* newValue */); 825 } 826 euiccFactoryResetLatch.countDown(); 827 } 828 } 829 }; 830 831 Intent intent = new Intent(ACTION_EUICC_FACTORY_RESET); 832 intent.setPackage(packageName); 833 PendingIntent callbackIntent = PendingIntent.getBroadcastAsUser( 834 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.SYSTEM); 835 IntentFilter filterConsent = new IntentFilter(); 836 filterConsent.addAction(ACTION_EUICC_FACTORY_RESET); 837 HandlerThread euiccHandlerThread = new HandlerThread("euiccWipeFinishReceiverThread"); 838 euiccHandlerThread.start(); 839 Handler euiccHandler = new Handler(euiccHandlerThread.getLooper()); 840 context.getApplicationContext() 841 .registerReceiver(euiccWipeFinishReceiver, filterConsent, null, euiccHandler); 842 euiccManager.eraseSubscriptions(callbackIntent); 843 try { 844 long waitingTimeMillis = Settings.Global.getLong( 845 context.getContentResolver(), 846 Settings.Global.EUICC_FACTORY_RESET_TIMEOUT_MILLIS, 847 DEFAULT_EUICC_FACTORY_RESET_TIMEOUT_MILLIS); 848 if (waitingTimeMillis < MIN_EUICC_FACTORY_RESET_TIMEOUT_MILLIS) { 849 waitingTimeMillis = MIN_EUICC_FACTORY_RESET_TIMEOUT_MILLIS; 850 } else if (waitingTimeMillis > MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS) { 851 waitingTimeMillis = MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS; 852 } 853 if (!euiccFactoryResetLatch.await(waitingTimeMillis, TimeUnit.MILLISECONDS)) { 854 Log.e(TAG, "Timeout wiping eUICC data."); 855 return false; 856 } 857 } catch (InterruptedException e) { 858 Thread.currentThread().interrupt(); 859 Log.e(TAG, "Wiping eUICC data interrupted", e); 860 return false; 861 } finally { 862 context.getApplicationContext().unregisterReceiver(euiccWipeFinishReceiver); 863 } 864 return wipingSucceeded.get(); 865 } 866 return false; 867 } 868 removeEuiccInvisibleSubs( Context context, EuiccManager euiccManager)869 private static void removeEuiccInvisibleSubs( 870 Context context, EuiccManager euiccManager) { 871 ContentResolver cr = context.getContentResolver(); 872 if (Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) == 0) { 873 // If the eUICC isn't provisioned, there's no need to remove euicc invisible profiles, 874 // as there's nothing to be removed. 875 Log.i(TAG, "Skip removing eUICC invisible profiles as it is not provisioned."); 876 return; 877 } else if (euiccManager == null || !euiccManager.isEnabled()) { 878 Log.i(TAG, "Skip removing eUICC invisible profiles as eUICC manager is not available."); 879 return; 880 } 881 SubscriptionManager subscriptionManager = 882 context.getSystemService(SubscriptionManager.class); 883 List<SubscriptionInfo> availableSubs = 884 subscriptionManager.getAvailableSubscriptionInfoList(); 885 if (availableSubs == null || availableSubs.isEmpty()) { 886 Log.i(TAG, "Skip removing eUICC invisible profiles as no available profiles found."); 887 return; 888 } 889 List<SubscriptionInfo> invisibleSubs = new ArrayList<>(); 890 for (SubscriptionInfo sub : availableSubs) { 891 if (sub.isEmbedded() && sub.getGroupUuid() != null && sub.isOpportunistic()) { 892 invisibleSubs.add(sub); 893 } 894 } 895 removeEuiccInvisibleSubs(context, invisibleSubs, euiccManager); 896 } 897 removeEuiccInvisibleSubs( Context context, List<SubscriptionInfo> subscriptionInfos, EuiccManager euiccManager)898 private static boolean removeEuiccInvisibleSubs( 899 Context context, List<SubscriptionInfo> subscriptionInfos, EuiccManager euiccManager) { 900 if (subscriptionInfos == null || subscriptionInfos.isEmpty()) { 901 Log.i(TAG, "There are no eUICC invisible profiles needed to be removed."); 902 return true; 903 } 904 CountDownLatch removeSubsLatch = new CountDownLatch(subscriptionInfos.size()); 905 final AtomicInteger removedSubsCount = new AtomicInteger(0); 906 907 BroadcastReceiver removeEuiccSubsReceiver = new BroadcastReceiver() { 908 @Override 909 public void onReceive(Context context, Intent intent) { 910 if (ACTION_EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS.equals(intent.getAction())) { 911 if (getResultCode() != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) { 912 int detailedCode = intent.getIntExtra( 913 EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, 0); 914 Log.e(TAG, "Error removing euicc opportunistic profile, Detailed code = " 915 + detailedCode); 916 } else { 917 Log.e(TAG, "Successfully remove euicc opportunistic profile."); 918 removedSubsCount.incrementAndGet(); 919 } 920 removeSubsLatch.countDown(); 921 } 922 } 923 }; 924 925 Intent intent = new Intent(ACTION_EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS); 926 intent.setPackage(PACKAGE_NAME_EUICC_DATA_MANAGEMENT_CALLBACK); 927 PendingIntent callbackIntent = PendingIntent.getBroadcastAsUser( 928 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.SYSTEM); 929 IntentFilter intentFilter = new IntentFilter(); 930 intentFilter.addAction(ACTION_EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS); 931 HandlerThread euiccHandlerThread = 932 new HandlerThread("euiccRemovingSubsReceiverThread"); 933 euiccHandlerThread.start(); 934 Handler euiccHandler = new Handler(euiccHandlerThread.getLooper()); 935 context.getApplicationContext() 936 .registerReceiver( 937 removeEuiccSubsReceiver, intentFilter, null, euiccHandler); 938 for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { 939 Log.i( 940 TAG, 941 "Remove invisible subscription " + subscriptionInfo.getSubscriptionId() 942 + " from card " + subscriptionInfo.getCardId()); 943 euiccManager.createForCardId(subscriptionInfo.getCardId()) 944 .deleteSubscription(subscriptionInfo.getSubscriptionId(), callbackIntent); 945 } 946 try { 947 long waitingTimeMillis = Settings.Global.getLong( 948 context.getContentResolver(), 949 Settings.Global.EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS, 950 DEFAULT_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS); 951 if (waitingTimeMillis < MIN_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS) { 952 waitingTimeMillis = MIN_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS; 953 } else if (waitingTimeMillis > MAX_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS) { 954 waitingTimeMillis = MAX_EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS; 955 } 956 if (!removeSubsLatch.await(waitingTimeMillis, TimeUnit.MILLISECONDS)) { 957 Log.e(TAG, "Timeout removing invisible euicc profiles."); 958 return false; 959 } 960 } catch (InterruptedException e) { 961 Thread.currentThread().interrupt(); 962 Log.e(TAG, "Removing invisible euicc profiles interrupted", e); 963 return false; 964 } finally { 965 context.getApplicationContext().unregisterReceiver(removeEuiccSubsReceiver); 966 if (euiccHandlerThread != null) { 967 euiccHandlerThread.quit(); 968 } 969 } 970 return removedSubsCount.get() == subscriptionInfos.size(); 971 } 972 973 /** {@hide} */ rebootPromptAndWipeUserData(Context context, String reason)974 public static void rebootPromptAndWipeUserData(Context context, String reason) 975 throws IOException { 976 boolean checkpointing = false; 977 boolean needReboot = false; 978 IVold vold = null; 979 try { 980 vold = IVold.Stub.asInterface(ServiceManager.checkService("vold")); 981 if (vold != null) { 982 checkpointing = vold.needsCheckpoint(); 983 } else { 984 Log.w(TAG, "Failed to get vold"); 985 } 986 } catch (Exception e) { 987 Log.w(TAG, "Failed to check for checkpointing"); 988 } 989 990 // If we are running in checkpointing mode, we should not prompt a wipe. 991 // Checkpointing may save us. If it doesn't, we will wind up here again. 992 if (checkpointing) { 993 try { 994 vold.abortChanges("rescueparty", false); 995 Log.i(TAG, "Rescue Party requested wipe. Aborting update"); 996 } catch (Exception e) { 997 Log.i(TAG, "Rescue Party requested wipe. Rebooting instead."); 998 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 999 pm.reboot("rescueparty"); 1000 } 1001 return; 1002 } 1003 1004 String reasonArg = null; 1005 if (!TextUtils.isEmpty(reason)) { 1006 reasonArg = "--reason=" + sanitizeArg(reason); 1007 } 1008 1009 final String localeArg = "--locale=" + Locale.getDefault().toString(); 1010 bootCommand(context, null, "--prompt_and_wipe_data", reasonArg, localeArg); 1011 } 1012 1013 /** 1014 * Reboot into the recovery system to wipe the /cache partition. 1015 * @throws IOException if something goes wrong. 1016 */ rebootWipeCache(Context context)1017 public static void rebootWipeCache(Context context) throws IOException { 1018 rebootWipeCache(context, context.getPackageName()); 1019 } 1020 1021 /** {@hide} */ rebootWipeCache(Context context, String reason)1022 public static void rebootWipeCache(Context context, String reason) throws IOException { 1023 String reasonArg = null; 1024 if (!TextUtils.isEmpty(reason)) { 1025 reasonArg = "--reason=" + sanitizeArg(reason); 1026 } 1027 1028 final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ; 1029 bootCommand(context, "--wipe_cache", reasonArg, localeArg); 1030 } 1031 1032 /** 1033 * Reboot into recovery and wipe the A/B device. 1034 * 1035 * @param Context the Context to use. 1036 * @param packageFile the wipe package to be applied. 1037 * @param reason the reason to wipe. 1038 * 1039 * @throws IOException if something goes wrong. 1040 * 1041 * @hide 1042 */ 1043 @SystemApi 1044 @RequiresPermission(allOf = { 1045 android.Manifest.permission.RECOVERY, 1046 android.Manifest.permission.REBOOT 1047 }) rebootWipeAb(Context context, File packageFile, String reason)1048 public static void rebootWipeAb(Context context, File packageFile, String reason) 1049 throws IOException { 1050 String reasonArg = null; 1051 if (!TextUtils.isEmpty(reason)) { 1052 reasonArg = "--reason=" + sanitizeArg(reason); 1053 } 1054 1055 final String filename = packageFile.getCanonicalPath(); 1056 final String filenameArg = "--wipe_package=" + filename; 1057 final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ; 1058 bootCommand(context, "--wipe_ab", filenameArg, reasonArg, localeArg); 1059 } 1060 1061 /** 1062 * Reboot into the recovery system with the supplied argument. 1063 * @param args to pass to the recovery utility. 1064 * @throws IOException if something goes wrong. 1065 */ bootCommand(Context context, String... args)1066 private static void bootCommand(Context context, String... args) throws IOException { 1067 LOG_FILE.delete(); 1068 1069 StringBuilder command = new StringBuilder(); 1070 for (String arg : args) { 1071 if (!TextUtils.isEmpty(arg)) { 1072 command.append(arg); 1073 command.append("\n"); 1074 } 1075 } 1076 1077 // Write the command into BCB (bootloader control block) and boot from 1078 // there. Will not return unless failed. 1079 RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); 1080 rs.rebootRecoveryWithCommand(command.toString()); 1081 1082 throw new IOException("Reboot failed (no permissions?)"); 1083 } 1084 1085 /** 1086 * Called after booting to process and remove recovery-related files. 1087 * @return the log file from recovery, or null if none was found. 1088 * 1089 * @hide 1090 */ handleAftermath(Context context)1091 public static String handleAftermath(Context context) { 1092 // Record the tail of the LOG_FILE 1093 String log = null; 1094 try { 1095 log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); 1096 } catch (FileNotFoundException e) { 1097 Log.i(TAG, "No recovery log file"); 1098 } catch (IOException e) { 1099 Log.e(TAG, "Error reading recovery log", e); 1100 } 1101 1102 1103 // Only remove the OTA package if it's partially processed (uncrypt'd). 1104 boolean reservePackage = BLOCK_MAP_FILE.exists(); 1105 if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) { 1106 String filename = null; 1107 try { 1108 filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null); 1109 } catch (IOException e) { 1110 Log.e(TAG, "Error reading uncrypt file", e); 1111 } 1112 1113 // Remove the OTA package on /data that has been (possibly 1114 // partially) processed. (Bug: 24973532) 1115 if (filename != null && filename.startsWith("/data")) { 1116 if (UNCRYPT_PACKAGE_FILE.delete()) { 1117 Log.i(TAG, "Deleted: " + filename); 1118 } else { 1119 Log.e(TAG, "Can't delete: " + filename); 1120 } 1121 } 1122 } 1123 1124 // We keep the update logs (beginning with LAST_PREFIX), and optionally 1125 // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE 1126 // will be created at the end of a successful uncrypt. If seeing this 1127 // file, we keep the block map file and the file that contains the 1128 // package name (UNCRYPT_PACKAGE_FILE). This is to reduce the work for 1129 // GmsCore to avoid re-downloading everything again. 1130 String[] names = RECOVERY_DIR.list(); 1131 for (int i = 0; names != null && i < names.length; i++) { 1132 // Do not remove the last_install file since the recovery-persist takes care of it. 1133 if (names[i].startsWith(LAST_PREFIX) || names[i].equals(LAST_INSTALL_PATH)) continue; 1134 if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue; 1135 if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue; 1136 1137 recursiveDelete(new File(RECOVERY_DIR, names[i])); 1138 } 1139 1140 return log; 1141 } 1142 1143 /** 1144 * Internally, delete a given file or directory recursively. 1145 */ recursiveDelete(File name)1146 private static void recursiveDelete(File name) { 1147 if (name.isDirectory()) { 1148 String[] files = name.list(); 1149 for (int i = 0; files != null && i < files.length; i++) { 1150 File f = new File(name, files[i]); 1151 recursiveDelete(f); 1152 } 1153 } 1154 1155 if (!name.delete()) { 1156 Log.e(TAG, "Can't delete: " + name); 1157 } else { 1158 Log.i(TAG, "Deleted: " + name); 1159 } 1160 } 1161 1162 /** 1163 * Talks to RecoverySystemService via Binder to trigger uncrypt. 1164 */ uncrypt(String packageFile, IRecoverySystemProgressListener listener)1165 private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) { 1166 try { 1167 return mService.uncrypt(packageFile, listener); 1168 } catch (RemoteException unused) { 1169 } 1170 return false; 1171 } 1172 1173 /** 1174 * Talks to RecoverySystemService via Binder to set up the BCB. 1175 */ setupBcb(String command)1176 private boolean setupBcb(String command) { 1177 try { 1178 return mService.setupBcb(command); 1179 } catch (RemoteException unused) { 1180 } 1181 return false; 1182 } 1183 1184 /** 1185 * Talks to RecoverySystemService via Binder to clear up the BCB. 1186 */ clearBcb()1187 private boolean clearBcb() { 1188 try { 1189 return mService.clearBcb(); 1190 } catch (RemoteException unused) { 1191 } 1192 return false; 1193 } 1194 1195 /** 1196 * Talks to RecoverySystemService via Binder to set up the BCB command and 1197 * reboot into recovery accordingly. 1198 */ rebootRecoveryWithCommand(String command)1199 private void rebootRecoveryWithCommand(String command) { 1200 try { 1201 mService.rebootRecoveryWithCommand(command); 1202 } catch (RemoteException ignored) { 1203 } 1204 } 1205 1206 /** 1207 * Internally, recovery treats each line of the command file as a separate 1208 * argv, so we only need to protect against newlines and nulls. 1209 */ sanitizeArg(String arg)1210 private static String sanitizeArg(String arg) { 1211 arg = arg.replace('\0', '?'); 1212 arg = arg.replace('\n', '?'); 1213 return arg; 1214 } 1215 1216 1217 /** 1218 * @removed Was previously made visible by accident. 1219 */ RecoverySystem()1220 public RecoverySystem() { 1221 mService = null; 1222 } 1223 1224 /** 1225 * @hide 1226 */ RecoverySystem(IRecoverySystem service)1227 public RecoverySystem(IRecoverySystem service) { 1228 mService = service; 1229 } 1230 } 1231