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