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 android.annotation.SystemApi; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.os.UserManager; 24 import android.text.TextUtils; 25 import android.util.Log; 26 27 import java.io.ByteArrayInputStream; 28 import java.io.BufferedReader; 29 import java.io.File; 30 import java.io.FileNotFoundException; 31 import java.io.FileReader; 32 import java.io.FileWriter; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.RandomAccessFile; 36 import java.security.GeneralSecurityException; 37 import java.security.PublicKey; 38 import java.security.Signature; 39 import java.security.SignatureException; 40 import java.security.cert.CertificateFactory; 41 import java.security.cert.X509Certificate; 42 import java.util.Enumeration; 43 import java.util.HashSet; 44 import java.util.Iterator; 45 import java.util.List; 46 import java.util.Locale; 47 import java.util.zip.ZipEntry; 48 import java.util.zip.ZipFile; 49 50 import com.android.internal.logging.MetricsLogger; 51 52 import sun.security.pkcs.PKCS7; 53 import sun.security.pkcs.SignerInfo; 54 55 /** 56 * RecoverySystem contains methods for interacting with the Android 57 * recovery system (the separate partition that can be used to install 58 * system updates, wipe user data, etc.) 59 */ 60 public class RecoverySystem { 61 private static final String TAG = "RecoverySystem"; 62 63 /** 64 * Default location of zip file containing public keys (X509 65 * certs) authorized to sign OTA updates. 66 */ 67 private static final File DEFAULT_KEYSTORE = 68 new File("/system/etc/security/otacerts.zip"); 69 70 /** Send progress to listeners no more often than this (in ms). */ 71 private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; 72 73 /** Used to communicate with recovery. See bootable/recovery/recovery.cpp. */ 74 private static final File RECOVERY_DIR = new File("/cache/recovery"); 75 private static final File LOG_FILE = new File(RECOVERY_DIR, "log"); 76 private static final File LAST_INSTALL_FILE = new File(RECOVERY_DIR, "last_install"); 77 private static final String LAST_PREFIX = "last_"; 78 79 /** 80 * The recovery image uses this file to identify the location (i.e. blocks) 81 * of an OTA package on the /data partition. The block map file is 82 * generated by uncrypt. 83 * 84 * @hide 85 */ 86 public static final File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map"); 87 88 /** 89 * UNCRYPT_PACKAGE_FILE stores the filename to be uncrypt'd, which will be 90 * read by uncrypt. 91 * 92 * @hide 93 */ 94 public static final File UNCRYPT_PACKAGE_FILE = new File(RECOVERY_DIR, "uncrypt_file"); 95 96 // Length limits for reading files. 97 private static final int LOG_FILE_MAX_LENGTH = 64 * 1024; 98 99 // Prevent concurrent execution of requests. 100 private static final Object sRequestLock = new Object(); 101 102 private final IRecoverySystem mService; 103 104 /** 105 * Interface definition for a callback to be invoked regularly as 106 * verification proceeds. 107 */ 108 public interface ProgressListener { 109 /** 110 * Called periodically as the verification progresses. 111 * 112 * @param progress the approximate percentage of the 113 * verification that has been completed, ranging from 0 114 * to 100 (inclusive). 115 */ onProgress(int progress)116 public void onProgress(int progress); 117 } 118 119 /** @return the set of certs that can be used to sign an OTA package. */ getTrustedCerts(File keystore)120 private static HashSet<X509Certificate> getTrustedCerts(File keystore) 121 throws IOException, GeneralSecurityException { 122 HashSet<X509Certificate> trusted = new HashSet<X509Certificate>(); 123 if (keystore == null) { 124 keystore = DEFAULT_KEYSTORE; 125 } 126 ZipFile zip = new ZipFile(keystore); 127 try { 128 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 129 Enumeration<? extends ZipEntry> entries = zip.entries(); 130 while (entries.hasMoreElements()) { 131 ZipEntry entry = entries.nextElement(); 132 InputStream is = zip.getInputStream(entry); 133 try { 134 trusted.add((X509Certificate) cf.generateCertificate(is)); 135 } finally { 136 is.close(); 137 } 138 } 139 } finally { 140 zip.close(); 141 } 142 return trusted; 143 } 144 145 /** 146 * Verify the cryptographic signature of a system update package 147 * before installing it. Note that the package is also verified 148 * separately by the installer once the device is rebooted into 149 * the recovery system. This function will return only if the 150 * package was successfully verified; otherwise it will throw an 151 * exception. 152 * 153 * Verification of a package can take significant time, so this 154 * function should not be called from a UI thread. Interrupting 155 * the thread while this function is in progress will result in a 156 * SecurityException being thrown (and the thread's interrupt flag 157 * will be cleared). 158 * 159 * @param packageFile the package to be verified 160 * @param listener an object to receive periodic progress 161 * updates as verification proceeds. May be null. 162 * @param deviceCertsZipFile the zip file of certificates whose 163 * public keys we will accept. Verification succeeds if the 164 * package is signed by the private key corresponding to any 165 * public key in this file. May be null to use the system default 166 * file (currently "/system/etc/security/otacerts.zip"). 167 * 168 * @throws IOException if there were any errors reading the 169 * package or certs files. 170 * @throws GeneralSecurityException if verification failed 171 */ verifyPackage(File packageFile, ProgressListener listener, File deviceCertsZipFile)172 public static void verifyPackage(File packageFile, 173 ProgressListener listener, 174 File deviceCertsZipFile) 175 throws IOException, GeneralSecurityException { 176 final long fileLen = packageFile.length(); 177 178 final RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); 179 try { 180 final long startTimeMillis = System.currentTimeMillis(); 181 if (listener != null) { 182 listener.onProgress(0); 183 } 184 185 raf.seek(fileLen - 6); 186 byte[] footer = new byte[6]; 187 raf.readFully(footer); 188 189 if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) { 190 throw new SignatureException("no signature in file (no footer)"); 191 } 192 193 final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); 194 final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); 195 196 byte[] eocd = new byte[commentSize + 22]; 197 raf.seek(fileLen - (commentSize + 22)); 198 raf.readFully(eocd); 199 200 // Check that we have found the start of the 201 // end-of-central-directory record. 202 if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b || 203 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) { 204 throw new SignatureException("no signature in file (bad footer)"); 205 } 206 207 for (int i = 4; i < eocd.length-3; ++i) { 208 if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b && 209 eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) { 210 throw new SignatureException("EOCD marker found after start of EOCD"); 211 } 212 } 213 214 // Parse the signature 215 PKCS7 block = 216 new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); 217 218 // Take the first certificate from the signature (packages 219 // should contain only one). 220 X509Certificate[] certificates = block.getCertificates(); 221 if (certificates == null || certificates.length == 0) { 222 throw new SignatureException("signature contains no certificates"); 223 } 224 X509Certificate cert = certificates[0]; 225 PublicKey signatureKey = cert.getPublicKey(); 226 227 SignerInfo[] signerInfos = block.getSignerInfos(); 228 if (signerInfos == null || signerInfos.length == 0) { 229 throw new SignatureException("signature contains no signedData"); 230 } 231 SignerInfo signerInfo = signerInfos[0]; 232 233 // Check that the public key of the certificate contained 234 // in the package equals one of our trusted public keys. 235 boolean verified = false; 236 HashSet<X509Certificate> trusted = getTrustedCerts( 237 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); 238 for (X509Certificate c : trusted) { 239 if (c.getPublicKey().equals(signatureKey)) { 240 verified = true; 241 break; 242 } 243 } 244 if (!verified) { 245 throw new SignatureException("signature doesn't match any trusted key"); 246 } 247 248 // The signature cert matches a trusted key. Now verify that 249 // the digest in the cert matches the actual file data. 250 raf.seek(0); 251 final ProgressListener listenerForInner = listener; 252 SignerInfo verifyResult = block.verify(signerInfo, new InputStream() { 253 // The signature covers all of the OTA package except the 254 // archive comment and its 2-byte length. 255 long toRead = fileLen - commentSize - 2; 256 long soFar = 0; 257 258 int lastPercent = 0; 259 long lastPublishTime = startTimeMillis; 260 261 @Override 262 public int read() throws IOException { 263 throw new UnsupportedOperationException(); 264 } 265 266 @Override 267 public int read(byte[] b, int off, int len) throws IOException { 268 if (soFar >= toRead) { 269 return -1; 270 } 271 if (Thread.currentThread().isInterrupted()) { 272 return -1; 273 } 274 275 int size = len; 276 if (soFar + size > toRead) { 277 size = (int)(toRead - soFar); 278 } 279 int read = raf.read(b, off, size); 280 soFar += read; 281 282 if (listenerForInner != null) { 283 long now = System.currentTimeMillis(); 284 int p = (int)(soFar * 100 / toRead); 285 if (p > lastPercent && 286 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { 287 lastPercent = p; 288 lastPublishTime = now; 289 listenerForInner.onProgress(lastPercent); 290 } 291 } 292 293 return read; 294 } 295 }); 296 297 final boolean interrupted = Thread.interrupted(); 298 if (listener != null) { 299 listener.onProgress(100); 300 } 301 302 if (interrupted) { 303 throw new SignatureException("verification was interrupted"); 304 } 305 306 if (verifyResult == null) { 307 throw new SignatureException("signature digest verification failed"); 308 } 309 } finally { 310 raf.close(); 311 } 312 } 313 314 /** 315 * Process a given package with uncrypt. No-op if the package is not on the 316 * /data partition. 317 * 318 * @param Context the Context to use 319 * @param packageFile the package to be processed 320 * @param listener an object to receive periodic progress updates as 321 * processing proceeds. May be null. 322 * @param handler the Handler upon which the callbacks will be 323 * executed. 324 * 325 * @throws IOException if there were any errors processing the package file. 326 * 327 * @hide 328 */ 329 @SystemApi processPackage(Context context, File packageFile, final ProgressListener listener, final Handler handler)330 public static void processPackage(Context context, 331 File packageFile, 332 final ProgressListener listener, 333 final Handler handler) 334 throws IOException { 335 String filename = packageFile.getCanonicalPath(); 336 if (!filename.startsWith("/data/")) { 337 return; 338 } 339 340 RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); 341 IRecoverySystemProgressListener progressListener = null; 342 if (listener != null) { 343 final Handler progressHandler; 344 if (handler != null) { 345 progressHandler = handler; 346 } else { 347 progressHandler = new Handler(context.getMainLooper()); 348 } 349 progressListener = new IRecoverySystemProgressListener.Stub() { 350 int lastProgress = 0; 351 long lastPublishTime = System.currentTimeMillis(); 352 353 @Override 354 public void onProgress(final int progress) { 355 final long now = System.currentTimeMillis(); 356 progressHandler.post(new Runnable() { 357 @Override 358 public void run() { 359 if (progress > lastProgress && 360 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { 361 lastProgress = progress; 362 lastPublishTime = now; 363 listener.onProgress(progress); 364 } 365 } 366 }); 367 } 368 }; 369 } 370 371 if (!rs.uncrypt(filename, progressListener)) { 372 throw new IOException("process package failed"); 373 } 374 } 375 376 /** 377 * Process a given package with uncrypt. No-op if the package is not on the 378 * /data partition. 379 * 380 * @param Context the Context to use 381 * @param packageFile the package to be processed 382 * @param listener an object to receive periodic progress updates as 383 * processing proceeds. May be null. 384 * 385 * @throws IOException if there were any errors processing the package file. 386 * 387 * @hide 388 */ 389 @SystemApi processPackage(Context context, File packageFile, final ProgressListener listener)390 public static void processPackage(Context context, 391 File packageFile, 392 final ProgressListener listener) 393 throws IOException { 394 processPackage(context, packageFile, listener, null); 395 } 396 397 /** 398 * Reboots the device in order to install the given update 399 * package. 400 * Requires the {@link android.Manifest.permission#REBOOT} permission. 401 * 402 * @param context the Context to use 403 * @param packageFile the update package to install. Must be on 404 * a partition mountable by recovery. (The set of partitions 405 * known to recovery may vary from device to device. Generally, 406 * /cache and /data are safe.) 407 * 408 * @throws IOException if writing the recovery command file 409 * fails, or if the reboot itself fails. 410 */ installPackage(Context context, File packageFile)411 public static void installPackage(Context context, File packageFile) 412 throws IOException { 413 installPackage(context, packageFile, false); 414 } 415 416 /** 417 * If the package hasn't been processed (i.e. uncrypt'd), set up 418 * UNCRYPT_PACKAGE_FILE and delete BLOCK_MAP_FILE to trigger uncrypt during the 419 * reboot. 420 * 421 * @param context the Context to use 422 * @param packageFile the update package to install. Must be on a 423 * partition mountable by recovery. 424 * @param processed if the package has been processed (uncrypt'd). 425 * 426 * @throws IOException if writing the recovery command file fails, or if 427 * the reboot itself fails. 428 * 429 * @hide 430 */ 431 @SystemApi installPackage(Context context, File packageFile, boolean processed)432 public static void installPackage(Context context, File packageFile, boolean processed) 433 throws IOException { 434 synchronized (sRequestLock) { 435 LOG_FILE.delete(); 436 // Must delete the file in case it was created by system server. 437 UNCRYPT_PACKAGE_FILE.delete(); 438 439 String filename = packageFile.getCanonicalPath(); 440 Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); 441 442 // If the package name ends with "_s.zip", it's a security update. 443 boolean securityUpdate = filename.endsWith("_s.zip"); 444 445 // If the package is on the /data partition, the package needs to 446 // be processed (i.e. uncrypt'd). The caller specifies if that has 447 // been done in 'processed' parameter. 448 if (filename.startsWith("/data/")) { 449 if (processed) { 450 if (!BLOCK_MAP_FILE.exists()) { 451 Log.e(TAG, "Package claimed to have been processed but failed to find " 452 + "the block map file."); 453 throw new IOException("Failed to find block map file"); 454 } 455 } else { 456 FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE); 457 try { 458 uncryptFile.write(filename + "\n"); 459 } finally { 460 uncryptFile.close(); 461 } 462 // UNCRYPT_PACKAGE_FILE needs to be readable and writable 463 // by system server. 464 if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false) 465 || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) { 466 Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE); 467 } 468 469 BLOCK_MAP_FILE.delete(); 470 } 471 472 // If the package is on the /data partition, use the block map 473 // file as the package name instead. 474 filename = "@/cache/recovery/block.map"; 475 } 476 477 final String filenameArg = "--update_package=" + filename + "\n"; 478 final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n"; 479 final String securityArg = "--security\n"; 480 481 String command = filenameArg + localeArg; 482 if (securityUpdate) { 483 command += securityArg; 484 } 485 486 RecoverySystem rs = (RecoverySystem) context.getSystemService( 487 Context.RECOVERY_SERVICE); 488 if (!rs.setupBcb(command)) { 489 throw new IOException("Setup BCB failed"); 490 } 491 492 // Having set up the BCB (bootloader control block), go ahead and reboot 493 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 494 pm.reboot(PowerManager.REBOOT_RECOVERY_UPDATE); 495 496 throw new IOException("Reboot failed (no permissions?)"); 497 } 498 } 499 500 /** 501 * Schedule to install the given package on next boot. The caller needs to 502 * ensure that the package must have been processed (uncrypt'd) if needed. 503 * It sets up the command in BCB (bootloader control block), which will 504 * be read by the bootloader and the recovery image. 505 * 506 * @param Context the Context to use. 507 * @param packageFile the package to be installed. 508 * 509 * @throws IOException if there were any errors setting up the BCB. 510 * 511 * @hide 512 */ 513 @SystemApi scheduleUpdateOnBoot(Context context, File packageFile)514 public static void scheduleUpdateOnBoot(Context context, File packageFile) 515 throws IOException { 516 String filename = packageFile.getCanonicalPath(); 517 boolean securityUpdate = filename.endsWith("_s.zip"); 518 519 // If the package is on the /data partition, use the block map file as 520 // the package name instead. 521 if (filename.startsWith("/data/")) { 522 filename = "@/cache/recovery/block.map"; 523 } 524 525 final String filenameArg = "--update_package=" + filename + "\n"; 526 final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n"; 527 final String securityArg = "--security\n"; 528 529 String command = filenameArg + localeArg; 530 if (securityUpdate) { 531 command += securityArg; 532 } 533 534 RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); 535 if (!rs.setupBcb(command)) { 536 throw new IOException("schedule update on boot failed"); 537 } 538 } 539 540 /** 541 * Cancel any scheduled update by clearing up the BCB (bootloader control 542 * block). 543 * 544 * @param Context the Context to use. 545 * 546 * @throws IOException if there were any errors clearing up the BCB. 547 * 548 * @hide 549 */ 550 @SystemApi cancelScheduledUpdate(Context context)551 public static void cancelScheduledUpdate(Context context) 552 throws IOException { 553 RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); 554 if (!rs.clearBcb()) { 555 throw new IOException("cancel scheduled update failed"); 556 } 557 } 558 559 /** 560 * Reboots the device and wipes the user data and cache 561 * partitions. This is sometimes called a "factory reset", which 562 * is something of a misnomer because the system partition is not 563 * restored to its factory state. Requires the 564 * {@link android.Manifest.permission#REBOOT} permission. 565 * 566 * @param context the Context to use 567 * 568 * @throws IOException if writing the recovery command file 569 * fails, or if the reboot itself fails. 570 * @throws SecurityException if the current user is not allowed to wipe data. 571 */ rebootWipeUserData(Context context)572 public static void rebootWipeUserData(Context context) throws IOException { 573 rebootWipeUserData(context, false /* shutdown */, context.getPackageName(), 574 false /* force */); 575 } 576 577 /** {@hide} */ rebootWipeUserData(Context context, String reason)578 public static void rebootWipeUserData(Context context, String reason) throws IOException { 579 rebootWipeUserData(context, false /* shutdown */, reason, false /* force */); 580 } 581 582 /** {@hide} */ rebootWipeUserData(Context context, boolean shutdown)583 public static void rebootWipeUserData(Context context, boolean shutdown) 584 throws IOException { 585 rebootWipeUserData(context, shutdown, context.getPackageName(), false /* force */); 586 } 587 588 /** 589 * Reboots the device and wipes the user data and cache 590 * partitions. This is sometimes called a "factory reset", which 591 * is something of a misnomer because the system partition is not 592 * restored to its factory state. Requires the 593 * {@link android.Manifest.permission#REBOOT} permission. 594 * 595 * @param context the Context to use 596 * @param shutdown if true, the device will be powered down after 597 * the wipe completes, rather than being rebooted 598 * back to the regular system. 599 * @param reason the reason for the wipe that is visible in the logs 600 * @param force whether the {@link UserManager.DISALLOW_FACTORY_RESET} user restriction 601 * should be ignored 602 * 603 * @throws IOException if writing the recovery command file 604 * fails, or if the reboot itself fails. 605 * @throws SecurityException if the current user is not allowed to wipe data. 606 * 607 * @hide 608 */ rebootWipeUserData(Context context, boolean shutdown, String reason, boolean force)609 public static void rebootWipeUserData(Context context, boolean shutdown, String reason, 610 boolean force) throws IOException { 611 UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); 612 if (!force && um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) { 613 throw new SecurityException("Wiping data is not allowed for this user."); 614 } 615 final ConditionVariable condition = new ConditionVariable(); 616 617 Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION"); 618 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 619 context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM, 620 android.Manifest.permission.MASTER_CLEAR, 621 new BroadcastReceiver() { 622 @Override 623 public void onReceive(Context context, Intent intent) { 624 condition.open(); 625 } 626 }, null, 0, null, null); 627 628 // Block until the ordered broadcast has completed. 629 condition.block(); 630 631 String shutdownArg = null; 632 if (shutdown) { 633 shutdownArg = "--shutdown_after"; 634 } 635 636 String reasonArg = null; 637 if (!TextUtils.isEmpty(reason)) { 638 reasonArg = "--reason=" + sanitizeArg(reason); 639 } 640 641 final String localeArg = "--locale=" + Locale.getDefault().toString(); 642 bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg); 643 } 644 645 /** 646 * Reboot into the recovery system to wipe the /cache partition. 647 * @throws IOException if something goes wrong. 648 */ rebootWipeCache(Context context)649 public static void rebootWipeCache(Context context) throws IOException { 650 rebootWipeCache(context, context.getPackageName()); 651 } 652 653 /** {@hide} */ rebootWipeCache(Context context, String reason)654 public static void rebootWipeCache(Context context, String reason) throws IOException { 655 String reasonArg = null; 656 if (!TextUtils.isEmpty(reason)) { 657 reasonArg = "--reason=" + sanitizeArg(reason); 658 } 659 660 final String localeArg = "--locale=" + Locale.getDefault().toString(); 661 bootCommand(context, "--wipe_cache", reasonArg, localeArg); 662 } 663 664 /** 665 * Reboot into recovery and wipe the A/B device. 666 * 667 * @param Context the Context to use. 668 * @param packageFile the wipe package to be applied. 669 * @param reason the reason to wipe. 670 * 671 * @throws IOException if something goes wrong. 672 * 673 * @hide 674 */ 675 @SystemApi rebootWipeAb(Context context, File packageFile, String reason)676 public static void rebootWipeAb(Context context, File packageFile, String reason) 677 throws IOException { 678 String reasonArg = null; 679 if (!TextUtils.isEmpty(reason)) { 680 reasonArg = "--reason=" + sanitizeArg(reason); 681 } 682 683 final String filename = packageFile.getCanonicalPath(); 684 final String filenameArg = "--wipe_package=" + filename; 685 final String localeArg = "--locale=" + Locale.getDefault().toString(); 686 bootCommand(context, "--wipe_ab", filenameArg, reasonArg, localeArg); 687 } 688 689 /** 690 * Reboot into the recovery system with the supplied argument. 691 * @param args to pass to the recovery utility. 692 * @throws IOException if something goes wrong. 693 */ bootCommand(Context context, String... args)694 private static void bootCommand(Context context, String... args) throws IOException { 695 synchronized (sRequestLock) { 696 LOG_FILE.delete(); 697 698 StringBuilder command = new StringBuilder(); 699 for (String arg : args) { 700 if (!TextUtils.isEmpty(arg)) { 701 command.append(arg); 702 command.append("\n"); 703 } 704 } 705 706 // Write the command into BCB (bootloader control block). 707 RecoverySystem rs = (RecoverySystem) context.getSystemService( 708 Context.RECOVERY_SERVICE); 709 rs.setupBcb(command.toString()); 710 711 // Having set up the BCB, go ahead and reboot. 712 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 713 pm.reboot(PowerManager.REBOOT_RECOVERY); 714 715 throw new IOException("Reboot failed (no permissions?)"); 716 } 717 } 718 719 // Read last_install; then report time (in seconds) and I/O (in MiB) for 720 // this update to tron. 721 // Only report on the reboots immediately after an OTA update. parseLastInstallLog(Context context)722 private static void parseLastInstallLog(Context context) { 723 try (BufferedReader in = new BufferedReader(new FileReader(LAST_INSTALL_FILE))) { 724 String line = null; 725 int bytesWrittenInMiB = -1, bytesStashedInMiB = -1; 726 int timeTotal = -1; 727 int sourceVersion = -1; 728 while ((line = in.readLine()) != null) { 729 // Here is an example of lines in last_install: 730 // ... 731 // time_total: 101 732 // bytes_written_vendor: 51074 733 // bytes_stashed_vendor: 200 734 int numIndex = line.indexOf(':'); 735 if (numIndex == -1 || numIndex + 1 >= line.length()) { 736 continue; 737 } 738 String numString = line.substring(numIndex + 1).trim(); 739 long parsedNum; 740 try { 741 parsedNum = Long.parseLong(numString); 742 } catch (NumberFormatException ignored) { 743 Log.e(TAG, "Failed to parse numbers in " + line); 744 continue; 745 } 746 747 final int MiB = 1024 * 1024; 748 int scaled; 749 try { 750 if (line.startsWith("bytes")) { 751 scaled = Math.toIntExact(parsedNum / MiB); 752 } else { 753 scaled = Math.toIntExact(parsedNum); 754 } 755 } catch (ArithmeticException ignored) { 756 Log.e(TAG, "Number overflows in " + line); 757 continue; 758 } 759 760 if (line.startsWith("time")) { 761 timeTotal = scaled; 762 } else if (line.startsWith("source_build")) { 763 sourceVersion = scaled; 764 } else if (line.startsWith("bytes_written")) { 765 bytesWrittenInMiB = (bytesWrittenInMiB == -1) ? scaled : 766 bytesWrittenInMiB + scaled; 767 } else if (line.startsWith("bytes_stashed")) { 768 bytesStashedInMiB = (bytesStashedInMiB == -1) ? scaled : 769 bytesStashedInMiB + scaled; 770 } 771 } 772 773 // Don't report data to tron if corresponding entry isn't found in last_install. 774 if (timeTotal != -1) { 775 MetricsLogger.histogram(context, "ota_time_total", timeTotal); 776 } 777 if (sourceVersion != -1) { 778 MetricsLogger.histogram(context, "ota_source_version", sourceVersion); 779 } 780 if (bytesWrittenInMiB != -1) { 781 MetricsLogger.histogram(context, "ota_written_in_MiBs", bytesWrittenInMiB); 782 } 783 if (bytesStashedInMiB != -1) { 784 MetricsLogger.histogram(context, "ota_stashed_in_MiBs", bytesStashedInMiB); 785 } 786 787 } catch (IOException e) { 788 Log.e(TAG, "Failed to read lines in last_install", e); 789 } 790 } 791 792 /** 793 * Called after booting to process and remove recovery-related files. 794 * @return the log file from recovery, or null if none was found. 795 * 796 * @hide 797 */ handleAftermath(Context context)798 public static String handleAftermath(Context context) { 799 // Record the tail of the LOG_FILE 800 String log = null; 801 try { 802 log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); 803 } catch (FileNotFoundException e) { 804 Log.i(TAG, "No recovery log file"); 805 } catch (IOException e) { 806 Log.e(TAG, "Error reading recovery log", e); 807 } 808 809 if (log != null) { 810 parseLastInstallLog(context); 811 } 812 813 // Only remove the OTA package if it's partially processed (uncrypt'd). 814 boolean reservePackage = BLOCK_MAP_FILE.exists(); 815 if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) { 816 String filename = null; 817 try { 818 filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null); 819 } catch (IOException e) { 820 Log.e(TAG, "Error reading uncrypt file", e); 821 } 822 823 // Remove the OTA package on /data that has been (possibly 824 // partially) processed. (Bug: 24973532) 825 if (filename != null && filename.startsWith("/data")) { 826 if (UNCRYPT_PACKAGE_FILE.delete()) { 827 Log.i(TAG, "Deleted: " + filename); 828 } else { 829 Log.e(TAG, "Can't delete: " + filename); 830 } 831 } 832 } 833 834 // We keep the update logs (beginning with LAST_PREFIX), and optionally 835 // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE 836 // will be created at the end of a successful uncrypt. If seeing this 837 // file, we keep the block map file and the file that contains the 838 // package name (UNCRYPT_PACKAGE_FILE). This is to reduce the work for 839 // GmsCore to avoid re-downloading everything again. 840 String[] names = RECOVERY_DIR.list(); 841 for (int i = 0; names != null && i < names.length; i++) { 842 if (names[i].startsWith(LAST_PREFIX)) continue; 843 if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue; 844 if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue; 845 846 recursiveDelete(new File(RECOVERY_DIR, names[i])); 847 } 848 849 return log; 850 } 851 852 /** 853 * Internally, delete a given file or directory recursively. 854 */ recursiveDelete(File name)855 private static void recursiveDelete(File name) { 856 if (name.isDirectory()) { 857 String[] files = name.list(); 858 for (int i = 0; files != null && i < files.length; i++) { 859 File f = new File(name, files[i]); 860 recursiveDelete(f); 861 } 862 } 863 864 if (!name.delete()) { 865 Log.e(TAG, "Can't delete: " + name); 866 } else { 867 Log.i(TAG, "Deleted: " + name); 868 } 869 } 870 871 /** 872 * Talks to RecoverySystemService via Binder to trigger uncrypt. 873 */ uncrypt(String packageFile, IRecoverySystemProgressListener listener)874 private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) { 875 try { 876 return mService.uncrypt(packageFile, listener); 877 } catch (RemoteException unused) { 878 } 879 return false; 880 } 881 882 /** 883 * Talks to RecoverySystemService via Binder to set up the BCB. 884 */ setupBcb(String command)885 private boolean setupBcb(String command) { 886 try { 887 return mService.setupBcb(command); 888 } catch (RemoteException unused) { 889 } 890 return false; 891 } 892 893 /** 894 * Talks to RecoverySystemService via Binder to clear up the BCB. 895 */ clearBcb()896 private boolean clearBcb() { 897 try { 898 return mService.clearBcb(); 899 } catch (RemoteException unused) { 900 } 901 return false; 902 } 903 904 /** 905 * Internally, recovery treats each line of the command file as a separate 906 * argv, so we only need to protect against newlines and nulls. 907 */ sanitizeArg(String arg)908 private static String sanitizeArg(String arg) { 909 arg = arg.replace('\0', '?'); 910 arg = arg.replace('\n', '?'); 911 return arg; 912 } 913 914 915 /** 916 * @removed Was previously made visible by accident. 917 */ RecoverySystem()918 public RecoverySystem() { 919 mService = null; 920 } 921 922 /** 923 * @hide 924 */ RecoverySystem(IRecoverySystem service)925 public RecoverySystem(IRecoverySystem service) { 926 mService = service; 927 } 928 } 929