RecoverySystem.java revision 2a03586998b857bb13f1119005be186e4a88ff02
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    /**
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        synchronized (sRequestLock) {
704            LOG_FILE.delete();
705
706            StringBuilder command = new StringBuilder();
707            for (String arg : args) {
708                if (!TextUtils.isEmpty(arg)) {
709                    command.append(arg);
710                    command.append("\n");
711                }
712            }
713
714            // Write the command into BCB (bootloader control block).
715            RecoverySystem rs = (RecoverySystem) context.getSystemService(
716                    Context.RECOVERY_SERVICE);
717            rs.setupBcb(command.toString());
718
719            // Having set up the BCB, go ahead and reboot.
720            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
721            pm.reboot(PowerManager.REBOOT_RECOVERY);
722
723            throw new IOException("Reboot failed (no permissions?)");
724        }
725    }
726
727    // Read last_install; then report time (in seconds) and I/O (in MiB) for
728    // this update to tron.
729    // Only report on the reboots immediately after an OTA update.
730    private static void parseLastInstallLog(Context context) {
731        try (BufferedReader in = new BufferedReader(new FileReader(LAST_INSTALL_FILE))) {
732            String line = null;
733            int bytesWrittenInMiB = -1, bytesStashedInMiB = -1;
734            int timeTotal = -1;
735            int uncryptTime = -1;
736            int sourceVersion = -1;
737            while ((line = in.readLine()) != null) {
738                // Here is an example of lines in last_install:
739                // ...
740                // time_total: 101
741                // bytes_written_vendor: 51074
742                // bytes_stashed_vendor: 200
743                int numIndex = line.indexOf(':');
744                if (numIndex == -1 || numIndex + 1 >= line.length()) {
745                    continue;
746                }
747                String numString = line.substring(numIndex + 1).trim();
748                long parsedNum;
749                try {
750                    parsedNum = Long.parseLong(numString);
751                } catch (NumberFormatException ignored) {
752                    Log.e(TAG, "Failed to parse numbers in " + line);
753                    continue;
754                }
755
756                final int MiB = 1024 * 1024;
757                int scaled;
758                try {
759                    if (line.startsWith("bytes")) {
760                        scaled = Math.toIntExact(parsedNum / MiB);
761                    } else {
762                        scaled = Math.toIntExact(parsedNum);
763                    }
764                } catch (ArithmeticException ignored) {
765                    Log.e(TAG, "Number overflows in " + line);
766                    continue;
767                }
768
769                if (line.startsWith("time")) {
770                    timeTotal = scaled;
771                } else if (line.startsWith("uncrypt_time")) {
772                    uncryptTime = scaled;
773                } else if (line.startsWith("source_build")) {
774                    sourceVersion = scaled;
775                } else if (line.startsWith("bytes_written")) {
776                    bytesWrittenInMiB = (bytesWrittenInMiB == -1) ? scaled :
777                            bytesWrittenInMiB + scaled;
778                } else if (line.startsWith("bytes_stashed")) {
779                    bytesStashedInMiB = (bytesStashedInMiB == -1) ? scaled :
780                            bytesStashedInMiB + scaled;
781                }
782            }
783
784            // Don't report data to tron if corresponding entry isn't found in last_install.
785            if (timeTotal != -1) {
786                MetricsLogger.histogram(context, "ota_time_total", timeTotal);
787            }
788            if (uncryptTime != -1) {
789                MetricsLogger.histogram(context, "ota_uncrypt_time", uncryptTime);
790            }
791            if (sourceVersion != -1) {
792                MetricsLogger.histogram(context, "ota_source_version", sourceVersion);
793            }
794            if (bytesWrittenInMiB != -1) {
795                MetricsLogger.histogram(context, "ota_written_in_MiBs", bytesWrittenInMiB);
796            }
797            if (bytesStashedInMiB != -1) {
798                MetricsLogger.histogram(context, "ota_stashed_in_MiBs", bytesStashedInMiB);
799            }
800
801        } catch (IOException e) {
802            Log.e(TAG, "Failed to read lines in last_install", e);
803        }
804    }
805
806    /**
807     * Called after booting to process and remove recovery-related files.
808     * @return the log file from recovery, or null if none was found.
809     *
810     * @hide
811     */
812    public static String handleAftermath(Context context) {
813        // Record the tail of the LOG_FILE
814        String log = null;
815        try {
816            log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
817        } catch (FileNotFoundException e) {
818            Log.i(TAG, "No recovery log file");
819        } catch (IOException e) {
820            Log.e(TAG, "Error reading recovery log", e);
821        }
822
823        if (log != null) {
824            parseLastInstallLog(context);
825        }
826
827        // Only remove the OTA package if it's partially processed (uncrypt'd).
828        boolean reservePackage = BLOCK_MAP_FILE.exists();
829        if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) {
830            String filename = null;
831            try {
832                filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null);
833            } catch (IOException e) {
834                Log.e(TAG, "Error reading uncrypt file", e);
835            }
836
837            // Remove the OTA package on /data that has been (possibly
838            // partially) processed. (Bug: 24973532)
839            if (filename != null && filename.startsWith("/data")) {
840                if (UNCRYPT_PACKAGE_FILE.delete()) {
841                    Log.i(TAG, "Deleted: " + filename);
842                } else {
843                    Log.e(TAG, "Can't delete: " + filename);
844                }
845            }
846        }
847
848        // We keep the update logs (beginning with LAST_PREFIX), and optionally
849        // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE
850        // will be created at the end of a successful uncrypt. If seeing this
851        // file, we keep the block map file and the file that contains the
852        // package name (UNCRYPT_PACKAGE_FILE). This is to reduce the work for
853        // GmsCore to avoid re-downloading everything again.
854        String[] names = RECOVERY_DIR.list();
855        for (int i = 0; names != null && i < names.length; i++) {
856            if (names[i].startsWith(LAST_PREFIX)) continue;
857            if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue;
858            if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue;
859
860            recursiveDelete(new File(RECOVERY_DIR, names[i]));
861        }
862
863        return log;
864    }
865
866    /**
867     * Internally, delete a given file or directory recursively.
868     */
869    private static void recursiveDelete(File name) {
870        if (name.isDirectory()) {
871            String[] files = name.list();
872            for (int i = 0; files != null && i < files.length; i++) {
873                File f = new File(name, files[i]);
874                recursiveDelete(f);
875            }
876        }
877
878        if (!name.delete()) {
879            Log.e(TAG, "Can't delete: " + name);
880        } else {
881            Log.i(TAG, "Deleted: " + name);
882        }
883    }
884
885    /**
886     * Talks to RecoverySystemService via Binder to trigger uncrypt.
887     */
888    private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) {
889        try {
890            return mService.uncrypt(packageFile, listener);
891        } catch (RemoteException unused) {
892        }
893        return false;
894    }
895
896    /**
897     * Talks to RecoverySystemService via Binder to set up the BCB.
898     */
899    private boolean setupBcb(String command) {
900        try {
901            return mService.setupBcb(command);
902        } catch (RemoteException unused) {
903        }
904        return false;
905    }
906
907    /**
908     * Talks to RecoverySystemService via Binder to clear up the BCB.
909     */
910    private boolean clearBcb() {
911        try {
912            return mService.clearBcb();
913        } catch (RemoteException unused) {
914        }
915        return false;
916    }
917
918    /**
919     * Internally, recovery treats each line of the command file as a separate
920     * argv, so we only need to protect against newlines and nulls.
921     */
922    private static String sanitizeArg(String arg) {
923        arg = arg.replace('\0', '?');
924        arg = arg.replace('\n', '?');
925        return arg;
926    }
927
928
929    /**
930     * @removed Was previously made visible by accident.
931     */
932    public RecoverySystem() {
933        mService = null;
934    }
935
936    /**
937     * @hide
938     */
939    public RecoverySystem(IRecoverySystem service) {
940        mService = service;
941    }
942}
943