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