summaryrefslogtreecommitdiff
path: root/core/java/android/os/RecoverySystem.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/os/RecoverySystem.java')
-rw-r--r--core/java/android/os/RecoverySystem.java447
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;
+ }
}