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     // 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          */
    116         public void onProgress(int progress);
    117     }
    118 
    119     /** @return the set of certs that can be used to sign an OTA package. */
    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      */
    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
    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
    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      */
    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
    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
    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
    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      */
    572     public static void rebootWipeUserData(Context context) throws IOException {
    573         rebootWipeUserData(context, false /* shutdown */, context.getPackageName(),
    574                 false /* force */);
    575     }
    576 
    577     /** {@hide} */
    578     public static void rebootWipeUserData(Context context, String reason) throws IOException {
    579         rebootWipeUserData(context, false /* shutdown */, reason, false /* force */);
    580     }
    581 
    582     /** {@hide} */
    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      */
    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      */
    649     public static void rebootWipeCache(Context context) throws IOException {
    650         rebootWipeCache(context, context.getPackageName());
    651     }
    652 
    653     /** {@hide} */
    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
    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      */
    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.
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    918     public RecoverySystem() {
    919         mService = null;
    920     }
    921 
    922     /**
    923      * @hide
    924      */
    925     public RecoverySystem(IRecoverySystem service) {
    926         mService = service;
    927     }
    928 }
    929