diff options
Diffstat (limited to 'core/java/android/os/RecoverySystem.java')
| -rw-r--r-- | core/java/android/os/RecoverySystem.java | 447 |
1 files changed, 403 insertions, 44 deletions
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 4113de2a7190..dd7be53d9865 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.SystemApi; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -24,8 +25,10 @@ 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; @@ -44,6 +47,8 @@ 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; @@ -66,14 +71,35 @@ public class RecoverySystem { private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; /** Used to communicate with recovery. See bootable/recovery/recovery.cpp. */ - private static File RECOVERY_DIR = new File("/cache/recovery"); - private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); - private static File UNCRYPT_FILE = new File(RECOVERY_DIR, "uncrypt_file"); - private static File LOG_FILE = new File(RECOVERY_DIR, "log"); - private static String LAST_PREFIX = "last_"; + 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 int LOG_FILE_MAX_LENGTH = 64 * 1024; + 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 @@ -286,6 +312,89 @@ public class RecoverySystem { } /** + * 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. @@ -300,30 +409,151 @@ public class RecoverySystem { * fails, or if the reboot itself fails. */ public static void installPackage(Context context, File packageFile) - throws IOException { - String filename = packageFile.getCanonicalPath(); + throws IOException { + installPackage(context, packageFile, false); + } - FileWriter uncryptFile = new FileWriter(UNCRYPT_FILE); - try { - uncryptFile.write(filename + "\n"); - } finally { - uncryptFile.close(); - } - // UNCRYPT_FILE needs to be readable by system server on bootup. - if (!UNCRYPT_FILE.setReadable(true, false)) { - Log.e(TAG, "Error setting readable for " + UNCRYPT_FILE.getCanonicalPath()); + /** + * 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?)"); } - Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); + } + + /** + * 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, write the block map file - // into COMMAND_FILE instead. + // 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; - final String localeArg = "--locale=" + Locale.getDefault().toString(); - bootCommand(context, filenameArg, localeArg); + 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"); + } } /** @@ -382,7 +612,7 @@ public class RecoverySystem { Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION"); intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER, + context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM, android.Manifest.permission.MASTER_CLEAR, new BroadcastReceiver() { @Override @@ -433,27 +663,101 @@ public class RecoverySystem { * @throws IOException if something goes wrong. */ private static void bootCommand(Context context, String... args) throws IOException { - RECOVERY_DIR.mkdirs(); // In case we need it - COMMAND_FILE.delete(); // In case it's not writable - LOG_FILE.delete(); + synchronized (sRequestLock) { + LOG_FILE.delete(); - FileWriter command = new FileWriter(COMMAND_FILE); - try { + StringBuilder command = new StringBuilder(); for (String arg : args) { if (!TextUtils.isEmpty(arg)) { - command.write(arg); - command.write("\n"); + command.append(arg); + command.append("\n"); } } - } finally { - command.close(); + + // Write the command into BCB (bootloader control block). + RecoverySystem rs = (RecoverySystem) context.getSystemService( + Context.RECOVERY_SERVICE); + rs.setupBcb(command.toString()); + + // Having set up the BCB, go ahead and reboot. + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + pm.reboot(PowerManager.REBOOT_RECOVERY); + + throw new IOException("Reboot failed (no permissions?)"); } + } - // Having written the command file, go ahead and reboot - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - pm.reboot(PowerManager.REBOOT_RECOVERY); + // 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; + } - throw new IOException("Reboot failed (no permissions?)"); + 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); + } } /** @@ -462,7 +766,7 @@ public class RecoverySystem { * * @hide */ - public static String handleAftermath() { + public static String handleAftermath(Context context) { // Record the tail of the LOG_FILE String log = null; try { @@ -473,10 +777,16 @@ public class RecoverySystem { Log.e(TAG, "Error reading recovery log", e); } - if (UNCRYPT_FILE.exists()) { + 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_FILE, 0, null); + filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null); } catch (IOException e) { Log.e(TAG, "Error reading uncrypt file", e); } @@ -484,7 +794,7 @@ public class RecoverySystem { // Remove the OTA package on /data that has been (possibly // partially) processed. (Bug: 24973532) if (filename != null && filename.startsWith("/data")) { - if (UNCRYPT_FILE.delete()) { + if (UNCRYPT_PACKAGE_FILE.delete()) { Log.i(TAG, "Deleted: " + filename); } else { Log.e(TAG, "Can't delete: " + filename); @@ -492,11 +802,18 @@ public class RecoverySystem { } } - // Delete everything in RECOVERY_DIR except those beginning - // with LAST_PREFIX + // 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])); } @@ -523,6 +840,39 @@ public class RecoverySystem { } /** + * 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. */ @@ -536,5 +886,14 @@ public class RecoverySystem { /** * @removed Was previously made visible by accident. */ - public RecoverySystem() { } + public RecoverySystem() { + mService = null; + } + + /** + * @hide + */ + public RecoverySystem(IRecoverySystem service) { + mService = service; + } } |
