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