Android 源码之Recovery升级的过程和问题分析

很多时候都需要通过第三方的apk去做FOTA升级,在Android源码中其中有相关的接口类可以参考或者调用,路径如下:

frameworks/base/core/java/android/os/RecoverySystem.java
frameworks/base/services/core/java/com/android/server/RecoverySystemService.java 
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.os;

import android.annotation.SystemApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;

import java.io.ByteArrayInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import com.android.internal.logging.MetricsLogger;

import sun.security.pkcs.PKCS7;
import sun.security.pkcs.SignerInfo;

/**
 * RecoverySystem contains methods for interacting with the Android
 * recovery system (the separate partition that can be used to install
 * system updates, wipe user data, etc.)
 */
public class RecoverySystem {
    private static final String TAG = "RecoverySystem";

    /**
     * Default location of zip file containing public keys (X509
     * certs) authorized to sign OTA updates.
     */
    private static final File DEFAULT_KEYSTORE =
        new File("/system/etc/security/otacerts.zip");

    /** Send progress to listeners no more often than this (in ms). */
    private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;

    /** Used to communicate with recovery.  See bootable/recovery/recovery.cpp. */
    private static final File RECOVERY_DIR = new File("/cache/recovery");
    private static final File LOG_FILE = new File(RECOVERY_DIR, "log");
    private static final File LAST_INSTALL_FILE = new File(RECOVERY_DIR, "last_install");
    private static final String LAST_PREFIX = "last_";

    /**
     * The recovery image uses this file to identify the location (i.e. blocks)
     * of an OTA package on the /data partition. The block map file is
     * generated by uncrypt.
     *
     * @hide
     */
    public static final File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map");

    /**
     * UNCRYPT_PACKAGE_FILE stores the filename to be uncrypt'd, which will be
     * read by uncrypt.
     *
     * @hide
     */
    public static final File UNCRYPT_PACKAGE_FILE = new File(RECOVERY_DIR, "uncrypt_file");

    // Length limits for reading files.
    private static final int LOG_FILE_MAX_LENGTH = 64 * 1024;

    // Prevent concurrent execution of requests.
    private static final Object sRequestLock = new Object();

    private final IRecoverySystem mService;

    /**
     * Interface definition for a callback to be invoked regularly as
     * verification proceeds.
     */
    public interface ProgressListener {
        /**
         * Called periodically as the verification progresses.
         *
         * @param progress  the approximate percentage of the
         *        verification that has been completed, ranging from 0
         *        to 100 (inclusive).
         */
        public void onProgress(int progress);
    }

    /** @return the set of certs that can be used to sign an OTA package. */
    private static HashSet<X509Certificate> getTrustedCerts(File keystore)
        throws IOException, GeneralSecurityException {
        HashSet<X509Certificate> trusted = new HashSet<X509Certificate>();
        if (keystore == null) {
            keystore = DEFAULT_KEYSTORE;
        }
        ZipFile zip = new ZipFile(keystore);
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            Enumeration<? extends ZipEntry> entries = zip.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                InputStream is = zip.getInputStream(entry);
                try {
                    trusted.add((X509Certificate) cf.generateCertificate(is));
                } finally {
                    is.close();
                }
            }
        } finally {
            zip.close();
        }
        return trusted;
    }

    /**
     * Verify the cryptographic signature of a system update package
     * before installing it.  Note that the package is also verified
     * separately by the installer once the device is rebooted into
     * the recovery system.  This function will return only if the
     * package was successfully verified; otherwise it will throw an
     * exception.
     *
     * Verification of a package can take significant time, so this
     * function should not be called from a UI thread.  Interrupting
     * the thread while this function is in progress will result in a
     * SecurityException being thrown (and the thread's interrupt flag
     * will be cleared).
     *
     * @param packageFile  the package to be verified
     * @param listener     an object to receive periodic progress
     * updates as verification proceeds.  May be null.
     * @param deviceCertsZipFile  the zip file of certificates whose
     * public keys we will accept.  Verification succeeds if the
     * package is signed by the private key corresponding to any
     * public key in this file.  May be null to use the system default
     * file (currently "/system/etc/security/otacerts.zip").
     *
     * @throws IOException if there were any errors reading the
     * package or certs files.
     * @throws GeneralSecurityException if verification failed
     */
    public static void verifyPackage(File packageFile,
                                     ProgressListener listener,
                                     File deviceCertsZipFile)
        throws IOException, GeneralSecurityException {
        final long fileLen = packageFile.length();

        final RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
        try {
            final long startTimeMillis = System.currentTimeMillis();
            if (listener != null) {
                listener.onProgress(0);
            }

            raf.seek(fileLen - 6);
            byte[] footer = new byte[6];
            raf.readFully(footer);

            if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
                throw new SignatureException("no signature in file (no footer)");
            }

            final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
            final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);

            byte[] eocd = new byte[commentSize + 22];
            raf.seek(fileLen - (commentSize + 22));
            raf.readFully(eocd);

            // Check that we have found the start of the
            // end-of-central-directory record.
            if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
                eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
                throw new SignatureException("no signature in file (bad footer)");
            }

            for (int i = 4; i < eocd.length-3; ++i) {
                if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
                    eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
                    throw new SignatureException("EOCD marker found after start of EOCD");
                }
            }

            // Parse the signature
            PKCS7 block =
                new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));

            // Take the first certificate from the signature (packages
            // should contain only one).
            X509Certificate[] certificates = block.getCertificates();
            if (certificates == null || certificates.length == 0) {
                throw new SignatureException("signature contains no certificates");
            }
            X509Certificate cert = certificates[0];
            PublicKey signatureKey = cert.getPublicKey();

            SignerInfo[] signerInfos = block.getSignerInfos();
            if (signerInfos == null || signerInfos.length == 0) {
                throw new SignatureException("signature contains no signedData");
            }
            SignerInfo signerInfo = signerInfos[0];

            // Check that the public key of the certificate contained
            // in the package equals one of our trusted public keys.
            boolean verified = false;
            HashSet<X509Certificate> trusted = getTrustedCerts(
                deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
            for (X509Certificate c : trusted) {
                if (c.getPublicKey().equals(signatureKey)) {
                    verified = true;
                    break;
                }
            }
            if (!verified) {
                throw new SignatureException("signature doesn't match any trusted key");
            }

            // The signature cert matches a trusted key.  Now verify that
            // the digest in the cert matches the actual file data.
            raf.seek(0);
            final ProgressListener listenerForInner = listener;
            SignerInfo verifyResult = block.verify(signerInfo, new InputStream() {
                // The signature covers all of the OTA package except the
                // archive comment and its 2-byte length.
                long toRead = fileLen - commentSize - 2;
                long soFar = 0;

                int lastPercent = 0;
                long lastPublishTime = startTimeMillis;

                @Override
                public int read() throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public int read(byte[] b, int off, int len) throws IOException {
                    if (soFar >= toRead) {
                        return -1;
                    }
                    if (Thread.currentThread().isInterrupted()) {
                        return -1;
                    }

                    int size = len;
                    if (soFar + size > toRead) {
                        size = (int)(toRead - soFar);
                    }
                    int read = raf.read(b, off, size);
                    soFar += read;

                    if (listenerForInner != null) {
                        long now = System.currentTimeMillis();
                        int p = (int)(soFar * 100 / toRead);
                        if (p > lastPercent &&
                            now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
                            lastPercent = p;
                            lastPublishTime = now;
                            listenerForInner.onProgress(lastPercent);
                        }
                    }

                    return read;
                }
            });

            final boolean interrupted = Thread.interrupted();
            if (listener != null) {
                listener.onProgress(100);
            }

            if (interrupted) {
                throw new SignatureException("verification was interrupted");
            }

            if (verifyResult == null) {
                throw new SignatureException("signature digest verification failed");
            }
        } finally {
            raf.close();
        }
    }

    /**
     * Process a given package with uncrypt. No-op if the package is not on the
     * /data partition.
     *
     * @param Context      the Context to use
     * @param packageFile  the package to be processed
     * @param listener     an object to receive periodic progress updates as
     *                     processing proceeds.  May be null.
     * @param handler      the Handler upon which the callbacks will be
     *                     executed.
     *
     * @throws IOException if there were any errors processing the package file.
     *
     * @hide
     */
    @SystemApi
    public static void processPackage(Context context,
                                      File packageFile,
                                      final ProgressListener listener,
                                      final Handler handler)
            throws IOException {
        String filename = packageFile.getCanonicalPath();
        if (!filename.startsWith("/data/")) {
            return;
        }

        RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
        IRecoverySystemProgressListener progressListener = null;
        if (listener != null) {
            final Handler progressHandler;
            if (handler != null) {
                progressHandler = handler;
            } else {
                progressHandler = new Handler(context.getMainLooper());
            }
            progressListener = new IRecoverySystemProgressListener.Stub() {
                int lastProgress = 0;
                long lastPublishTime = System.currentTimeMillis();

                @Override
                public void onProgress(final int progress) {
                    final long now = System.currentTimeMillis();
                    progressHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            if (progress > lastProgress &&
                                    now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
                                lastProgress = progress;
                                lastPublishTime = now;
                                listener.onProgress(progress);
                            }
                        }
                    });
                }
            };
        }

        if (!rs.uncrypt(filename, progressListener)) {
            throw new IOException("process package failed");
        }
    }

    /**
     * Process a given package with uncrypt. No-op if the package is not on the
     * /data partition.
     *
     * @param Context      the Context to use
     * @param packageFile  the package to be processed
     * @param listener     an object to receive periodic progress updates as
     *                     processing proceeds.  May be null.
     *
     * @throws IOException if there were any errors processing the package file.
     *
     * @hide
     */
    @SystemApi
    public static void processPackage(Context context,
                                      File packageFile,
                                      final ProgressListener listener)
            throws IOException {
        processPackage(context, packageFile, listener, null);
    }

    /**
     * Reboots the device in order to install the given update
     * package.
     * Requires the {@link android.Manifest.permission#REBOOT} permission.
     *
     * @param context      the Context to use
     * @param packageFile  the update package to install.  Must be on
     * a partition mountable by recovery.  (The set of partitions
     * known to recovery may vary from device to device.  Generally,
     * /cache and /data are safe.)
     *
     * @throws IOException  if writing the recovery command file
     * fails, or if the reboot itself fails.
     */
    public static void installPackage(Context context, File packageFile)
            throws IOException {
        installPackage(context, packageFile, false);
    }

    /**
     * If the package hasn't been processed (i.e. uncrypt'd), set up
     * UNCRYPT_PACKAGE_FILE and delete BLOCK_MAP_FILE to trigger uncrypt during the
     * reboot.
     *
     * @param context      the Context to use
     * @param packageFile  the update package to install.  Must be on a
     * partition mountable by recovery.
     * @param processed    if the package has been processed (uncrypt'd).
     *
     * @throws IOException if writing the recovery command file fails, or if
     * the reboot itself fails.
     *
     * @hide
     */
    @SystemApi
    public static void installPackage(Context context, File packageFile, boolean processed)
            throws IOException {
        synchronized (sRequestLock) {
            LOG_FILE.delete();
            // Must delete the file in case it was created by system server.
            UNCRYPT_PACKAGE_FILE.delete();

            String filename = packageFile.getCanonicalPath();
            Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");

            // If the package name ends with "_s.zip", it's a security update.
            boolean securityUpdate = filename.endsWith("_s.zip");

            // If the package is on the /data partition, the package needs to
            // be processed (i.e. uncrypt'd). The caller specifies if that has
            // been done in 'processed' parameter.
            if (filename.startsWith("/data/")) {
                if (processed) {
                    if (!BLOCK_MAP_FILE.exists()) {
                        Log.e(TAG, "Package claimed to have been processed but failed to find "
                                + "the block map file.");
                        throw new IOException("Failed to find block map file");
                    }
                } else {
                    FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE);
                    try {
                        uncryptFile.write(filename + "\n");
                    } finally {
                        uncryptFile.close();
                    }
                    // UNCRYPT_PACKAGE_FILE needs to be readable and writable
                    // by system server.
                    if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false)
                            || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) {
                        Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE);
                    }

                    BLOCK_MAP_FILE.delete();
                }

                // If the package is on the /data partition, use the block map
                // file as the package name instead.
                filename = "@/cache/recovery/block.map";
            }

            final String filenameArg = "--update_package=" + filename + "\n";
            final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n";
            final String securityArg = "--security\n";

            String command = filenameArg + localeArg;
            if (securityUpdate) {
                command += securityArg;
            }

            RecoverySystem rs = (RecoverySystem) context.getSystemService(
                    Context.RECOVERY_SERVICE);
            if (!rs.setupBcb(command)) {
                throw new IOException("Setup BCB failed");
            }

            // Having set up the BCB (bootloader control block), go ahead and reboot
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            pm.reboot(PowerManager.REBOOT_RECOVERY_UPDATE);

            throw new IOException("Reboot failed (no permissions?)");
        }
    }

    /**
     * Schedule to install the given package on next boot. The caller needs to
     * ensure that the package must have been processed (uncrypt'd) if needed.
     * It sets up the command in BCB (bootloader control block), which will
     * be read by the bootloader and the recovery image.
     *
     * @param Context      the Context to use.
     * @param packageFile  the package to be installed.
     *
     * @throws IOException if there were any errors setting up the BCB.
     *
     * @hide
     */
    @SystemApi
    public static void scheduleUpdateOnBoot(Context context, File packageFile)
            throws IOException {
        String filename = packageFile.getCanonicalPath();
        boolean securityUpdate = filename.endsWith("_s.zip");

        // If the package is on the /data partition, use the block map file as
        // the package name instead.
        if (filename.startsWith("/data/")) {
            filename = "@/cache/recovery/block.map";
        }

        final String filenameArg = "--update_package=" + filename + "\n";
        final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n";
        final String securityArg = "--security\n";

        String command = filenameArg + localeArg;
        if (securityUpdate) {
            command += securityArg;
        }

        RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
        if (!rs.setupBcb(command)) {
            throw new IOException("schedule update on boot failed");
        }
    }

    /**
     * Cancel any scheduled update by clearing up the BCB (bootloader control
     * block).
     *
     * @param Context      the Context to use.
     *
     * @throws IOException if there were any errors clearing up the BCB.
     *
     * @hide
     */
    @SystemApi
    public static void cancelScheduledUpdate(Context context)
            throws IOException {
        RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
        if (!rs.clearBcb()) {
            throw new IOException("cancel scheduled update failed");
        }
    }

    /**
     * Reboots the device and wipes the user data and cache
     * partitions.  This is sometimes called a "factory reset", which
     * is something of a misnomer because the system partition is not
     * restored to its factory state.  Requires the
     * {@link android.Manifest.permission#REBOOT} permission.
     *
     * @param context  the Context to use
     *
     * @throws IOException  if writing the recovery command file
     * fails, or if the reboot itself fails.
     * @throws SecurityException if the current user is not allowed to wipe data.
     */
    public static void rebootWipeUserData(Context context) throws IOException {
        rebootWipeUserData(context, false, context.getPackageName());
    }

    /** {@hide} */
    public static void rebootWipeUserData(Context context, String reason) throws IOException {
        rebootWipeUserData(context, false, reason);
    }

    /** {@hide} */
    public static void rebootWipeUserData(Context context, boolean shutdown)
            throws IOException {
        rebootWipeUserData(context, shutdown, context.getPackageName());
    }

    /**
     * Reboots the device and wipes the user data and cache
     * partitions.  This is sometimes called a "factory reset", which
     * is something of a misnomer because the system partition is not
     * restored to its factory state.  Requires the
     * {@link android.Manifest.permission#REBOOT} permission.
     *
     * @param context   the Context to use
     * @param shutdown  if true, the device will be powered down after
     *                  the wipe completes, rather than being rebooted
     *                  back to the regular system.
     *
     * @throws IOException  if writing the recovery command file
     * fails, or if the reboot itself fails.
     * @throws SecurityException if the current user is not allowed to wipe data.
     *
     * @hide
     */
    public static void rebootWipeUserData(Context context, boolean shutdown, String reason)
            throws IOException {
        UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
        if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
            throw new SecurityException("Wiping data is not allowed for this user.");
        }
        final ConditionVariable condition = new ConditionVariable();

        Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM,
                android.Manifest.permission.MASTER_CLEAR,
                new BroadcastReceiver() {
                    @Override
                    public void onReceive(Context context, Intent intent) {
                        condition.open();
                    }
                }, null, 0, null, null);

        // Block until the ordered broadcast has completed.
        condition.block();

        String shutdownArg = null;
        if (shutdown) {
            shutdownArg = "--shutdown_after";
        }

        String reasonArg = null;
        if (!TextUtils.isEmpty(reason)) {
            reasonArg = "--reason=" + sanitizeArg(reason);
        }

        final String localeArg = "--locale=" + Locale.getDefault().toString();
        bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
    }

    /**
     * Reboot into the recovery system to wipe the /cache partition.
     * @throws IOException if something goes wrong.
     */
    public static void rebootWipeCache(Context context) throws IOException {
        rebootWipeCache(context, context.getPackageName());
    }

    /** {@hide} */
    public static void rebootWipeCache(Context context, String reason) throws IOException {
        String reasonArg = null;
        if (!TextUtils.isEmpty(reason)) {
            reasonArg = "--reason=" + sanitizeArg(reason);
        }

        final String localeArg = "--locale=" + Locale.getDefault().toString();
        bootCommand(context, "--wipe_cache", reasonArg, localeArg);
    }

    /**
     * Reboot into the recovery system with the supplied argument.
     * @param args to pass to the recovery utility.
     * @throws IOException if something goes wrong.
     */
    private static void bootCommand(Context context, String... args) throws IOException {
            LOG_FILE.delete();

            StringBuilder command = new StringBuilder();
            for (String arg : args) {
                if (!TextUtils.isEmpty(arg)) {
                    command.append(arg);
                    command.append("\n");
                }
            }

        // Write the command into BCB (bootloader control block) and boot from
        // there. Will not return unless failed.
        RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
        rs.rebootRecoveryWithCommand(command.toString());

        throw new IOException("Reboot failed (no permissions?)");

    }

    // Read last_install; then report time (in seconds) and I/O (in MiB) for
    // this update to tron.
    // Only report on the reboots immediately after an OTA update.
    private static void parseLastInstallLog(Context context) {
        try (BufferedReader in = new BufferedReader(new FileReader(LAST_INSTALL_FILE))) {
            String line = null;
            int bytesWrittenInMiB = -1, bytesStashedInMiB = -1;
            int timeTotal = -1;
            int sourceVersion = -1;
            while ((line = in.readLine()) != null) {
                // Here is an example of lines in last_install:
                // ...
                // time_total: 101
                // bytes_written_vendor: 51074
                // bytes_stashed_vendor: 200
                int numIndex = line.indexOf(':');
                if (numIndex == -1 || numIndex + 1 >= line.length()) {
                    continue;
                }
                String numString = line.substring(numIndex + 1).trim();
                long parsedNum;
                try {
                    parsedNum = Long.parseLong(numString);
                } catch (NumberFormatException ignored) {
                    Log.e(TAG, "Failed to parse numbers in " + line);
                    continue;
                }

                final int MiB = 1024 * 1024;
                int scaled;
                try {
                    if (line.startsWith("bytes")) {
                        scaled = Math.toIntExact(parsedNum / MiB);
                    } else {
                        scaled = Math.toIntExact(parsedNum);
                    }
                } catch (ArithmeticException ignored) {
                    Log.e(TAG, "Number overflows in " + line);
                    continue;
                }

                if (line.startsWith("time")) {
                    timeTotal = scaled;
                } else if (line.startsWith("source_build")) {
                    sourceVersion = scaled;
                } else if (line.startsWith("bytes_written")) {
                    bytesWrittenInMiB = (bytesWrittenInMiB == -1) ? scaled :
                            bytesWrittenInMiB + scaled;
                } else if (line.startsWith("bytes_stashed")) {
                    bytesStashedInMiB = (bytesStashedInMiB == -1) ? scaled :
                            bytesStashedInMiB + scaled;
                }
            }

            // Don't report data to tron if corresponding entry isn't found in last_install.
            if (timeTotal != -1) {
                MetricsLogger.histogram(context, "ota_time_total", timeTotal);
            }
            if (sourceVersion != -1) {
                MetricsLogger.histogram(context, "ota_source_version", sourceVersion);
            }
            if (bytesWrittenInMiB != -1) {
                MetricsLogger.histogram(context, "ota_written_in_MiBs", bytesWrittenInMiB);
            }
            if (bytesStashedInMiB != -1) {
                MetricsLogger.histogram(context, "ota_stashed_in_MiBs", bytesStashedInMiB);
            }

        } catch (IOException e) {
            Log.e(TAG, "Failed to read lines in last_install", e);
        }
    }

    /**
     * Called after booting to process and remove recovery-related files.
     * @return the log file from recovery, or null if none was found.
     *
     * @hide
     */
    public static String handleAftermath(Context context) {
        // Record the tail of the LOG_FILE
        String log = null;
        try {
            log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
        } catch (FileNotFoundException e) {
            Log.i(TAG, "No recovery log file");
        } catch (IOException e) {
            Log.e(TAG, "Error reading recovery log", e);
        }

        if (log != null) {
            parseLastInstallLog(context);
        }

        // Only remove the OTA package if it's partially processed (uncrypt'd).
        boolean reservePackage = BLOCK_MAP_FILE.exists();
        if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) {
            String filename = null;
            try {
                filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null);
            } catch (IOException e) {
                Log.e(TAG, "Error reading uncrypt file", e);
            }

            // Remove the OTA package on /data that has been (possibly
            // partially) processed. (Bug: 24973532)
            if (filename != null && filename.startsWith("/data")) {
                if (UNCRYPT_PACKAGE_FILE.delete()) {
                    Log.i(TAG, "Deleted: " + filename);
                } else {
                    Log.e(TAG, "Can't delete: " + filename);
                }
            }
        }

        // We keep the update logs (beginning with LAST_PREFIX), and optionally
        // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE
        // will be created at the end of a successful uncrypt. If seeing this
        // file, we keep the block map file and the file that contains the
        // package name (UNCRYPT_PACKAGE_FILE). This is to reduce the work for
        // GmsCore to avoid re-downloading everything again.
        String[] names = RECOVERY_DIR.list();
        for (int i = 0; names != null && i < names.length; i++) {
            if (names[i].startsWith(LAST_PREFIX)) continue;
            if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue;
            if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue;

            recursiveDelete(new File(RECOVERY_DIR, names[i]));
        }

        return log;
    }

    /**
     * Internally, delete a given file or directory recursively.
     */
    private static void recursiveDelete(File name) {
        if (name.isDirectory()) {
            String[] files = name.list();
            for (int i = 0; files != null && i < files.length; i++) {
                File f = new File(name, files[i]);
                recursiveDelete(f);
            }
        }

        if (!name.delete()) {
            Log.e(TAG, "Can't delete: " + name);
        } else {
            Log.i(TAG, "Deleted: " + name);
        }
    }

    /**
     * Talks to RecoverySystemService via Binder to trigger uncrypt.
     */
    private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) {
        try {
            return mService.uncrypt(packageFile, listener);
        } catch (RemoteException unused) {
        }
        return false;
    }

    /**
     * Talks to RecoverySystemService via Binder to set up the BCB.
     */
    private boolean setupBcb(String command) {
        try {
            return mService.setupBcb(command);
        } catch (RemoteException unused) {
        }
        return false;
    }

    /**
     * Talks to RecoverySystemService via Binder to clear up the BCB.
     */
    private boolean clearBcb() {
        try {
            return mService.clearBcb();
        } catch (RemoteException unused) {
        }
        return false;
    }

    /**
     * Internally, recovery treats each line of the command file as a separate
     * argv, so we only need to protect against newlines and nulls.
     */
    private static String sanitizeArg(String arg) {
        arg = arg.replace('\0', '?');
        arg = arg.replace('\n', '?');
        return arg;
    }

     /**
     * Talks to RecoverySystemService via Binder to set up the BCB command and
     * reboot into recovery accordingly.
     */
    private void rebootRecoveryWithCommand(String command) {
        try {
            mService.rebootRecoveryWithCommand(command);
        } catch (RemoteException ignored) {
        }
    }

    /**
     * @removed Was previously made visible by accident.
     */
    public RecoverySystem() {
        mService = null;
    }

    /**
     * @hide
     */
    public RecoverySystem(IRecoverySystem service) {
        mService = service;
    }
}

在之前的项目中经常遇到客户的OTA apk内置进去后就报错升不了级的情况。这面我们可以简单总结一下。
第一是参照广升的FOTA修改seplicy下的权限文件让apk有recovery权限

2d74a390b3691748c8af8a51371ef4db88e957d1
 system/sepolicy/domain.te     | 16 +++++++-
 system/sepolicy/recovery.te   | 90 +++++++++++++++++++++++++++++++++++++++++--
 system/sepolicy/system_app.te |  4 ++
 3 files changed, 106 insertions(+), 4 deletions(-)

diff --git a/system/sepolicy/domain.te b/system/sepolicy/domain.te
index 939c0510ae..c8aebf03d5 100755
--- a/system/sepolicy/domain.te
+++ b/system/sepolicy/domain.te
@@ -379,8 +379,10 @@ neverallow {
 neverallow { domain -servicemanager } *:binder set_context_mgr;

 # Only authorized processes should be writing to files in /data/dalvik-cache
+#fota start
 neverallow {
   domain
+  -recovery
   -init # TODO: limit init to relabelfrom for files
   -zygote
   -installd
@@ -390,12 +392,14 @@ neverallow {

 neverallow {
   domain
+  -recovery
   -init
   -installd
   -postinstall_dexopt
   -dex2oat
   -zygote
 } dalvikcache_data_file:dir no_w_dir_perms;
+#fota end

 # Only system_server should be able to send commands via the zygote socket
 neverallow { domain -zygote -system_server } zygote:unix_stream_socket connectto;
@@ -468,13 +472,16 @@ neverallow ~domain domain:process { transition dyntransition };
 # Example type transition:
 #  mydomain.te:file_type_auto_trans(mydomain, system_data_file, new_file_type)
 #
+#fota start
 neverallow {
   domain
+  -recovery
   -system_server
   -system_app
   -init
   -installd # for relabelfrom and unlink, check for this in explicit neverallow
 } system_data_file:file no_w_file_perms;
+#fota end
 # do not grant anything greater than r_file_perms and relabelfrom unlink
 # to installd
 neverallow installd system_data_file:file ~{ r_file_perms relabelfrom unlink };
@@ -494,8 +501,10 @@ neverallow {

 # Minimize read access to shell- or app-writable symlinks.
 # This is to prevent malicious symlink attacks.
+#fota start
 neverallow {
   domain
+  -recovery
   -appdomain
   -installd
   -uncrypt  # TODO: see if we can remove
@@ -503,17 +512,21 @@ neverallow {

 neverallow {
   domain
+  -recovery
   -shell
   userdebug_or_eng(`-uncrypt')
   -installd
 } shell_data_file:lnk_file read;
+#fota end

 # In addition to the symlink reading restrictions above, restrict
 # write access to shell owned directories. The /data/local/tmp
 # directory is untrustworthy, and non-whitelisted domains should
 # not be trusting any content in those directories.
+#fota start
 neverallow {
   domain
+  -recovery
   -adbd
   -dumpstate
   -installd
@@ -524,6 +537,7 @@ neverallow {

 neverallow {
   domain
+  -recovery
   -adbd
   -appdomain
   -dumpstate
@@ -570,7 +584,7 @@ neverallow * domain:file { execute execute_no_trans entrypoint };
 # Instead, if access to part of debugfs is desired, it should have a
 # more specific label.
 # TODO: fix system_server and dumpstate
-neverallow { domain -init -system_server -cmd_services -dumpstate userdebug_or_eng(`-ylog') } debugfs:file no_rw_file_perms;
+neverallow { domain -init -system_server -dumpstate } debugfs:file no_rw_file_perms;

 neverallow {
   domain
diff --git a/system/sepolicy/recovery.te b/system/sepolicy/recovery.te
old mode 100644
new mode 100755
index d5767ed004..e30516c072
--- a/system/sepolicy/recovery.te
+++ b/system/sepolicy/recovery.te
@@ -2,7 +2,11 @@

 # Declare the domain unconditionally so we can always reference it
 # in neverallow rules.
-type recovery, domain, domain_deprecated;
+#fota start
+#type recovery, domain, domain_deprecated;
+type recovery, domain, domain_deprecated, mlstrustedsubject;
+typeattribute recovery mlstrustedsubject;
+#fota end

 # But the allow rules are only included in the recovery policy.
 # Otherwise recovery is only allowed the domain rules.
@@ -101,6 +105,84 @@ recovery_only(`
   # This line seems suspect, as it should not really need to
   # set scheduling parameters for a kernel domain task.
   allow recovery kernel:process setsched;
+
+  #fota start
+  allow recovery proc:file write;
+  allow recovery system_prop:property_service set;
+
+  allow recovery rootfs:dir { add_name create };
+  allow recovery rootfs:dir write;
+
+  allow recovery { adb_keys_file keychain_data_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { adb_keys_file keychain_data_file }:file { getattr unlink };
+
+  #allow recovery keystore_data_file:dir { open read getattr search };
+  #allow recovery keystore_data_file:file { getattr };
+
+  allow recovery { shell_data_file bluetooth_data_file net_data_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { shell_data_file bluetooth_data_file net_data_file }:file { getattr unlink };
+
+  allow recovery { apk_private_data_file vpn_data_file zoneinfo_data_file shared_relro_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { apk_private_data_file vpn_data_file zoneinfo_data_file shared_relro_file }:file { getattr unlink };
+
+  allow recovery { adb_data_file dhcp_data_file misc_user_data_file systemkeys_data_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { adb_data_file dhcp_data_file misc_user_data_file systemkeys_data_file }:file { getattr unlink };
+
+  allow recovery { wifi_data_file camera_data_file media_data_file wpa_socket }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { wifi_data_file camera_data_file media_data_file wpa_socket }:file { getattr unlink };
+
+  allow recovery { audio_data_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { audio_data_file }:file { getattr unlink };
+
+  allow recovery { anr_data_file asec_image_file backup_data_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { anr_data_file asec_image_file backup_data_file }:file { getattr unlink };
+
+  allow recovery { radio_data_file dalvikcache_data_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { radio_data_file dalvikcache_data_file }:file { getattr unlink };
+
+  allow recovery { drm_data_file nfc_data_file resourcecache_data_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { drm_data_file nfc_data_file resourcecache_data_file }:file { getattr unlink };
+
+  allow recovery property_data_file:dir { open read getattr search };
+  allow recovery property_data_file:file { getattr };
+
+  allow recovery { tombstone_data_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { tombstone_data_file }:file { getattr unlink };
+
+  allow recovery security_file:dir { open read getattr search };
+
+  allow recovery { cache_backup_file }:dir { open read write getattr search rmdir remove_name };
+  allow recovery { cache_backup_file }:file { getattr unlink };
+
+  allow recovery { wallpaper_file apk_private_tmp_file gps_data_file cache_file cache_backup_file efs_file }:file { getattr unlink};
+  allow recovery { asec_apk_file asec_public_file bluetooth_efs_file }:file { getattr unlink};
+
+  #allow recovery { rootfs }:file { create write getattr unlink };
+  allow recovery { shell_data_file radio_data_file bluetooth_data_file nfc_data_file }:lnk_file { getattr unlink read};
+
+  allow recovery { camera_data_file system_ndebug_socket system_wpa_socket wpa_socket }:sock_file { getattr unlink };
+
+  allow recovery media_rw_data_file:dir create_dir_perms;
+  allow recovery media_rw_data_file:file create_file_perms;
+  allow recovery media_rw_data_file:lnk_file { getattr unlink read};
+  
+  allow recovery app_data_file:dir create_dir_perms;
+  allow recovery app_data_file:file create_file_perms;
+  allow recovery app_data_file:lnk_file { getattr unlink read};
+  
+  allow recovery apk_data_file:dir create_dir_perms;
+  allow recovery apk_data_file:file create_file_perms;
+  allow recovery apk_data_file:lnk_file { getattr unlink read};
+
+  allow recovery system_app_data_file:dir create_dir_perms;
+  allow recovery system_app_data_file:file create_file_perms;
+  allow recovery system_app_data_file:lnk_file { getattr unlink read};
+  
+  allow recovery system_data_file:dir create_dir_perms;
+  allow recovery system_data_file:file create_file_perms;
+  allow recovery system_data_file:lnk_file { getattr unlink read};
+  allow recovery system_data_file:fifo_file { open read write getattr rw_file_perms unlink };
+  #fota end
 ')

 ###
@@ -117,5 +199,7 @@ recovery_only(`
 # domains, including recovery.
 #
 # TODO: tighten this up further.
-neverallow recovery data_file_type:file { no_w_file_perms no_x_file_perms };
-neverallow recovery data_file_type:dir no_w_dir_perms;
+#fota start
+#neverallow recovery data_file_type:file { no_w_file_perms no_x_file_perms };
+#neverallow recovery data_file_type:dir no_w_dir_perms;
+
diff --git a/system/sepolicy/system_app.te b/system/sepolicy/system_app.te
old mode 100644
new mode 100755
index 2d51c5a101..763793cf14
--- a/system/sepolicy/system_app.te
+++ b/system/sepolicy/system_app.te
@@ -73,4 +73,8 @@ allow system_app keystore:keystore_key {
 allow system_app sysfs_zram:dir search;
 allow system_app sysfs_zram:file r_file_perms;

+#fota start
+allow system_app { cache_file cache_recovery_file }:dir create_dir_perms;
+allow system_app { cache_file cache_recovery_file }:file create_file_perms;
+
 control_logd(system_app)

第二次是修改sepolicy下的.te文件让system/app下的文件有读写权限:

503fcc824fe04e32ac7d009b0417a60be5a68534
 device/sprd/scx20/common/sepolicy/platform_app.te | 4 ++++
 device/sprd/scx20/common/sepolicy/uncrypt.te      | 4 ++++
 2 files changed, 8 insertions(+)

diff --git a/device/sprd/scx20/common/sepolicy/platform_app.te b/device/sprd/scx20/common/sepolicy/platform_app.te
index b8a23ba315..8a5545f11f 100755
--- a/device/sprd/scx20/common/sepolicy/platform_app.te
+++ b/device/sprd/scx20/common/sepolicy/platform_app.te
@@ -8,3 +8,7 @@ allow platform_app slogmodem:unix_stream_socket { connectto sendto };
 allow platform_app system_app_data_file:file { open read write };
 allow platform_app mlog_file:file   rw_file_perms;
 allow platform_app mlog_file:dir  create_dir_perms;
+allow platform_app recovery_service:service_manager { find };
+allow platform_app app_data_file:dir { rmdir remove_name };
+allow platform_app app_data_file:file { unlink };
+
diff --git a/device/sprd/scx20/common/sepolicy/uncrypt.te b/device/sprd/scx20/common/sepolicy/uncrypt.te
index cfc5c1ea58..07dd00380b 100755
--- a/device/sprd/scx20/common/sepolicy/uncrypt.te
+++ b/device/sprd/scx20/common/sepolicy/uncrypt.te
@@ -1,3 +1,7 @@
 allow uncrypt media_rw_data_file:file { create open setattr getattr read write unlink };
 allow uncrypt media_rw_data_file:dir { create open setattr getattr read write unlink search };
 allow uncrypt mmcblk_device:blk_file { open setattr getattr read write };
+allow uncrypt system_app_data_file:dir rw_dir_perms;
+allow uncrypt system_app_data_file:file rw_file_perms;
+allow uncrypt app_data_file:dir rw_dir_perms;
+allow uncrypt app_data_file:file rw_file_perms;

第三次的情况是读不到userdata下的update.zp,查看log是userdata分区没有别挂载上,修改方法是:

1 file changed, 2 insertions(+)

diff --git a/bootable/recovery/recovery.cpp b/bootable/recovery/recovery.cpp
index ec8073d21d..da734115ed 100755
--- a/bootable/recovery/recovery.cpp
+++ b/bootable/recovery/recovery.cpp
@@ -1023,6 +1023,8 @@ static int unzip_partition_file(const char* path) {
    int numKeys;

     setup_install_mounts();
+      if (strncmp(path, "/data/", 6) == 0)
+               ensure_path_mounted(path);
     sysMapFile(path, &map);

    std::vector<Certificate> loadedKeys;

这里有个地方要注意,有的源码里面可能将userdata分区加密了,我们可以在out下面搜userdata 可以看到下面的权限情况

/dev/block/platform/sdio_emmc/by-name/userdata /data        ext4 noatime,nosuid,nodev,nomblk_io_submit,noauto_da_alloc wait,encryptable=footer,check

对应的我们在device下面搜/dev/block/platform/sdio_emmc/by-name/userdata /data
就能找到对应的文件,修改一下即可。我贴出的代码里面userdata的加密情况是encryptable=footer,即默认没加密

猜你喜欢

转载自blog.csdn.net/An_Times/article/details/79758385