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
17package android.os;
18
19import android.annotation.SystemApi;
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.Intent;
23import android.os.UserManager;
24import android.text.TextUtils;
25import android.util.Log;
26
27import java.io.ByteArrayInputStream;
28import java.io.BufferedReader;
29import java.io.File;
30import java.io.FileNotFoundException;
31import java.io.FileReader;
32import java.io.FileWriter;
33import java.io.IOException;
34import java.io.InputStream;
35import java.io.RandomAccessFile;
36import java.security.GeneralSecurityException;
37import java.security.PublicKey;
38import java.security.Signature;
39import java.security.SignatureException;
40import java.security.cert.CertificateFactory;
41import java.security.cert.X509Certificate;
42import java.util.Enumeration;
43import java.util.HashSet;
44import java.util.Iterator;
45import java.util.List;
46import java.util.Locale;
47import java.util.zip.ZipEntry;
48import java.util.zip.ZipFile;
49
50import com.android.internal.logging.MetricsLogger;
51
52import sun.security.pkcs.PKCS7;
53import 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 */
60public 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, context.getPackageName());
574    }
575
576    /** {@hide} */
577    public static void rebootWipeUserData(Context context, String reason) throws IOException {
578        rebootWipeUserData(context, false, reason);
579    }
580
581    /** {@hide} */
582    public static void rebootWipeUserData(Context context, boolean shutdown)
583            throws IOException {
584        rebootWipeUserData(context, shutdown, context.getPackageName());
585    }
586
587    /**
588     * Reboots the device and wipes the user data and cache
589     * partitions.  This is sometimes called a "factory reset", which
590     * is something of a misnomer because the system partition is not
591     * restored to its factory state.  Requires the
592     * {@link android.Manifest.permission#REBOOT} permission.
593     *
594     * @param context   the Context to use
595     * @param shutdown  if true, the device will be powered down after
596     *                  the wipe completes, rather than being rebooted
597     *                  back to the regular system.
598     *
599     * @throws IOException  if writing the recovery command file
600     * fails, or if the reboot itself fails.
601     * @throws SecurityException if the current user is not allowed to wipe data.
602     *
603     * @hide
604     */
605    public static void rebootWipeUserData(Context context, boolean shutdown, String reason)
606            throws IOException {
607        UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
608        if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
609            throw new SecurityException("Wiping data is not allowed for this user.");
610        }
611        final ConditionVariable condition = new ConditionVariable();
612
613        Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
614        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
615        context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM,
616                android.Manifest.permission.MASTER_CLEAR,
617                new BroadcastReceiver() {
618                    @Override
619                    public void onReceive(Context context, Intent intent) {
620                        condition.open();
621                    }
622                }, null, 0, null, null);
623
624        // Block until the ordered broadcast has completed.
625        condition.block();
626
627        String shutdownArg = null;
628        if (shutdown) {
629            shutdownArg = "--shutdown_after";
630        }
631
632        String reasonArg = null;
633        if (!TextUtils.isEmpty(reason)) {
634            reasonArg = "--reason=" + sanitizeArg(reason);
635        }
636
637        final String localeArg = "--locale=" + Locale.getDefault().toString();
638        bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
639    }
640
641    /**
642     * Reboot into the recovery system to wipe the /cache partition.
643     * @throws IOException if something goes wrong.
644     */
645    public static void rebootWipeCache(Context context) throws IOException {
646        rebootWipeCache(context, context.getPackageName());
647    }
648
649    /** {@hide} */
650    public static void rebootWipeCache(Context context, String reason) throws IOException {
651        String reasonArg = null;
652        if (!TextUtils.isEmpty(reason)) {
653            reasonArg = "--reason=" + sanitizeArg(reason);
654        }
655
656        final String localeArg = "--locale=" + Locale.getDefault().toString();
657        bootCommand(context, "--wipe_cache", reasonArg, localeArg);
658    }
659
660    /**
661     * Reboot into the recovery system with the supplied argument.
662     * @param args to pass to the recovery utility.
663     * @throws IOException if something goes wrong.
664     */
665    private static void bootCommand(Context context, String... args) throws IOException {
666        synchronized (sRequestLock) {
667            LOG_FILE.delete();
668
669            StringBuilder command = new StringBuilder();
670            for (String arg : args) {
671                if (!TextUtils.isEmpty(arg)) {
672                    command.append(arg);
673                    command.append("\n");
674                }
675            }
676
677            // Write the command into BCB (bootloader control block).
678            RecoverySystem rs = (RecoverySystem) context.getSystemService(
679                    Context.RECOVERY_SERVICE);
680            rs.setupBcb(command.toString());
681
682            // Having set up the BCB, go ahead and reboot.
683            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
684            pm.reboot(PowerManager.REBOOT_RECOVERY);
685
686            throw new IOException("Reboot failed (no permissions?)");
687        }
688    }
689
690    // Read last_install; then report time (in seconds) and I/O (in MiB) for
691    // this update to tron.
692    // Only report on the reboots immediately after an OTA update.
693    private static void parseLastInstallLog(Context context) {
694        try (BufferedReader in = new BufferedReader(new FileReader(LAST_INSTALL_FILE))) {
695            String line = null;
696            int bytesWrittenInMiB = -1, bytesStashedInMiB = -1;
697            int timeTotal = -1;
698            int sourceVersion = -1;
699            while ((line = in.readLine()) != null) {
700                // Here is an example of lines in last_install:
701                // ...
702                // time_total: 101
703                // bytes_written_vendor: 51074
704                // bytes_stashed_vendor: 200
705                int numIndex = line.indexOf(':');
706                if (numIndex == -1 || numIndex + 1 >= line.length()) {
707                    continue;
708                }
709                String numString = line.substring(numIndex + 1).trim();
710                long parsedNum;
711                try {
712                    parsedNum = Long.parseLong(numString);
713                } catch (NumberFormatException ignored) {
714                    Log.e(TAG, "Failed to parse numbers in " + line);
715                    continue;
716                }
717
718                final int MiB = 1024 * 1024;
719                int scaled;
720                try {
721                    if (line.startsWith("bytes")) {
722                        scaled = Math.toIntExact(parsedNum / MiB);
723                    } else {
724                        scaled = Math.toIntExact(parsedNum);
725                    }
726                } catch (ArithmeticException ignored) {
727                    Log.e(TAG, "Number overflows in " + line);
728                    continue;
729                }
730
731                if (line.startsWith("time")) {
732                    timeTotal = scaled;
733                } else if (line.startsWith("source_build")) {
734                    sourceVersion = scaled;
735                } else if (line.startsWith("bytes_written")) {
736                    bytesWrittenInMiB = (bytesWrittenInMiB == -1) ? scaled :
737                            bytesWrittenInMiB + scaled;
738                } else if (line.startsWith("bytes_stashed")) {
739                    bytesStashedInMiB = (bytesStashedInMiB == -1) ? scaled :
740                            bytesStashedInMiB + scaled;
741                }
742            }
743
744            // Don't report data to tron if corresponding entry isn't found in last_install.
745            if (timeTotal != -1) {
746                MetricsLogger.histogram(context, "ota_time_total", timeTotal);
747            }
748            if (sourceVersion != -1) {
749                MetricsLogger.histogram(context, "ota_source_version", sourceVersion);
750            }
751            if (bytesWrittenInMiB != -1) {
752                MetricsLogger.histogram(context, "ota_written_in_MiBs", bytesWrittenInMiB);
753            }
754            if (bytesStashedInMiB != -1) {
755                MetricsLogger.histogram(context, "ota_stashed_in_MiBs", bytesStashedInMiB);
756            }
757
758        } catch (IOException e) {
759            Log.e(TAG, "Failed to read lines in last_install", e);
760        }
761    }
762
763    /**
764     * Called after booting to process and remove recovery-related files.
765     * @return the log file from recovery, or null if none was found.
766     *
767     * @hide
768     */
769    public static String handleAftermath(Context context) {
770        // Record the tail of the LOG_FILE
771        String log = null;
772        try {
773            log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
774        } catch (FileNotFoundException e) {
775            Log.i(TAG, "No recovery log file");
776        } catch (IOException e) {
777            Log.e(TAG, "Error reading recovery log", e);
778        }
779
780        if (log != null) {
781            parseLastInstallLog(context);
782        }
783
784        // Only remove the OTA package if it's partially processed (uncrypt'd).
785        boolean reservePackage = BLOCK_MAP_FILE.exists();
786        if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) {
787            String filename = null;
788            try {
789                filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null);
790            } catch (IOException e) {
791                Log.e(TAG, "Error reading uncrypt file", e);
792            }
793
794            // Remove the OTA package on /data that has been (possibly
795            // partially) processed. (Bug: 24973532)
796            if (filename != null && filename.startsWith("/data")) {
797                if (UNCRYPT_PACKAGE_FILE.delete()) {
798                    Log.i(TAG, "Deleted: " + filename);
799                } else {
800                    Log.e(TAG, "Can't delete: " + filename);
801                }
802            }
803        }
804
805        // We keep the update logs (beginning with LAST_PREFIX), and optionally
806        // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE
807        // will be created at the end of a successful uncrypt. If seeing this
808        // file, we keep the block map file and the file that contains the
809        // package name (UNCRYPT_PACKAGE_FILE). This is to reduce the work for
810        // GmsCore to avoid re-downloading everything again.
811        String[] names = RECOVERY_DIR.list();
812        for (int i = 0; names != null && i < names.length; i++) {
813            if (names[i].startsWith(LAST_PREFIX)) continue;
814            if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue;
815            if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue;
816
817            recursiveDelete(new File(RECOVERY_DIR, names[i]));
818        }
819
820        return log;
821    }
822
823    /**
824     * Internally, delete a given file or directory recursively.
825     */
826    private static void recursiveDelete(File name) {
827        if (name.isDirectory()) {
828            String[] files = name.list();
829            for (int i = 0; files != null && i < files.length; i++) {
830                File f = new File(name, files[i]);
831                recursiveDelete(f);
832            }
833        }
834
835        if (!name.delete()) {
836            Log.e(TAG, "Can't delete: " + name);
837        } else {
838            Log.i(TAG, "Deleted: " + name);
839        }
840    }
841
842    /**
843     * Talks to RecoverySystemService via Binder to trigger uncrypt.
844     */
845    private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) {
846        try {
847            return mService.uncrypt(packageFile, listener);
848        } catch (RemoteException unused) {
849        }
850        return false;
851    }
852
853    /**
854     * Talks to RecoverySystemService via Binder to set up the BCB.
855     */
856    private boolean setupBcb(String command) {
857        try {
858            return mService.setupBcb(command);
859        } catch (RemoteException unused) {
860        }
861        return false;
862    }
863
864    /**
865     * Talks to RecoverySystemService via Binder to clear up the BCB.
866     */
867    private boolean clearBcb() {
868        try {
869            return mService.clearBcb();
870        } catch (RemoteException unused) {
871        }
872        return false;
873    }
874
875    /**
876     * Internally, recovery treats each line of the command file as a separate
877     * argv, so we only need to protect against newlines and nulls.
878     */
879    private static String sanitizeArg(String arg) {
880        arg = arg.replace('\0', '?');
881        arg = arg.replace('\n', '?');
882        return arg;
883    }
884
885
886    /**
887     * @removed Was previously made visible by accident.
888     */
889    public RecoverySystem() {
890        mService = null;
891    }
892
893    /**
894     * @hide
895     */
896    public RecoverySystem(IRecoverySystem service) {
897        mService = service;
898    }
899}
900