summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.mk2
-rw-r--r--api/system-current.txt5
-rw-r--r--core/java/android/app/SystemServiceRegistry.java14
-rw-r--r--core/java/android/content/Context.java10
-rw-r--r--core/java/android/os/IRecoverySystem.aidl28
-rw-r--r--core/java/android/os/IRecoverySystemProgressListener.aidl24
-rw-r--r--core/java/android/os/PowerManager.java18
-rw-r--r--core/java/android/os/RecoverySystem.java333
-rw-r--r--services/core/java/com/android/server/RecoverySystemService.java214
-rw-r--r--services/core/java/com/android/server/power/PowerManagerService.java12
-rw-r--r--services/core/java/com/android/server/power/ShutdownThread.java161
-rw-r--r--services/java/com/android/server/SystemServer.java8
12 files changed, 686 insertions, 143 deletions
diff --git a/Android.mk b/Android.mk
index 918377178aac..97dfc1d0ca25 100644
--- a/Android.mk
+++ b/Android.mk
@@ -223,6 +223,8 @@ LOCAL_SRC_FILES += \
core/java/android/os/IPermissionController.aidl \
core/java/android/os/IProcessInfoService.aidl \
core/java/android/os/IPowerManager.aidl \
+ core/java/android/os/IRecoverySystem.aidl \
+ core/java/android/os/IRecoverySystemProgressListener.aidl \
core/java/android/os/IRemoteCallback.aidl \
core/java/android/os/ISchedulingPolicyService.aidl \
core/java/android/os/IUpdateLock.aidl \
diff --git a/api/system-current.txt b/api/system-current.txt
index b36d7e89cb8d..4e7157d0ff4e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -31418,9 +31418,14 @@ package android.os {
}
public class RecoverySystem {
+ method public static void cancelScheduledUpdate(android.content.Context) throws java.io.IOException;
method public static void installPackage(android.content.Context, java.io.File) throws java.io.IOException;
+ method public static void installPackage(android.content.Context, java.io.File, boolean) throws java.io.IOException;
+ method public static void processPackage(android.content.Context, java.io.File, android.os.RecoverySystem.ProgressListener, android.os.Handler) throws java.io.IOException;
+ method public static void processPackage(android.content.Context, java.io.File, android.os.RecoverySystem.ProgressListener) throws java.io.IOException;
method public static void rebootWipeCache(android.content.Context) throws java.io.IOException;
method public static void rebootWipeUserData(android.content.Context) throws java.io.IOException;
+ method public static void scheduleUpdateOnBoot(android.content.Context, java.io.File) throws java.io.IOException;
method public static void verifyPackage(java.io.File, android.os.RecoverySystem.ProgressListener, java.io.File) throws java.security.GeneralSecurityException, java.io.IOException;
}
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 52fba3b7b840..307c3eb18362 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -91,9 +91,11 @@ import android.os.HardwarePropertiesManager;
import android.os.IBinder;
import android.os.IHardwarePropertiesManager;
import android.os.IPowerManager;
+import android.os.IRecoverySystem;
import android.os.IUserManager;
import android.os.PowerManager;
import android.os.Process;
+import android.os.RecoverySystem;
import android.os.ServiceManager;
import android.os.SystemVibrator;
import android.os.UserHandle;
@@ -380,6 +382,18 @@ final class SystemServiceRegistry {
service, ctx.mMainThread.getHandler());
}});
+ registerService(Context.RECOVERY_SERVICE, RecoverySystem.class,
+ new CachedServiceFetcher<RecoverySystem>() {
+ @Override
+ public RecoverySystem createService(ContextImpl ctx) {
+ IBinder b = ServiceManager.getService(Context.RECOVERY_SERVICE);
+ IRecoverySystem service = IRecoverySystem.Stub.asInterface(b);
+ if (service == null) {
+ Log.wtf(TAG, "Failed to get recovery service.");
+ }
+ return new RecoverySystem(service);
+ }});
+
registerService(Context.SEARCH_SERVICE, SearchManager.class,
new CachedServiceFetcher<SearchManager>() {
@Override
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 0cdbef0b770b..b935b256b880 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2863,6 +2863,16 @@ public abstract class Context {
/**
* Use with {@link #getSystemService} to retrieve a
+ * {@link android.os.RecoverySystem} for accessing the recovery system
+ * service.
+ *
+ * @see #getSystemService
+ * @hide
+ */
+ public static final String RECOVERY_SERVICE = "recovery";
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a
* {@link android.view.WindowManager} for accessing the system's window
* manager.
*
diff --git a/core/java/android/os/IRecoverySystem.aidl b/core/java/android/os/IRecoverySystem.aidl
new file mode 100644
index 000000000000..12830a4996aa
--- /dev/null
+++ b/core/java/android/os/IRecoverySystem.aidl
@@ -0,0 +1,28 @@
+/* //device/java/android/android/os/IRecoverySystem.aidl
+**
+** Copyright 2016, 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.os.IRecoverySystemProgressListener;
+
+/** @hide */
+
+interface IRecoverySystem {
+ boolean uncrypt(in String packageFile, IRecoverySystemProgressListener listener);
+ boolean setupBcb(in String command);
+ boolean clearBcb();
+}
diff --git a/core/java/android/os/IRecoverySystemProgressListener.aidl b/core/java/android/os/IRecoverySystemProgressListener.aidl
new file mode 100644
index 000000000000..d6f712ea3240
--- /dev/null
+++ b/core/java/android/os/IRecoverySystemProgressListener.aidl
@@ -0,0 +1,24 @@
+/* //device/java/android/android/os/IRecoverySystemProgressListener.aidl
+**
+** Copyright 2016, 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;
+
+/** @hide */
+
+oneway interface IRecoverySystemProgressListener {
+ void onProgress(int progress);
+}
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 314b7d5491ff..dcc28d6f01a9 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -385,9 +385,9 @@ public final class PowerManager {
public static final int GO_TO_SLEEP_FLAG_NO_DOZE = 1 << 0;
/**
- * The value to pass as the 'reason' argument to reboot() to
- * reboot into recovery mode (for applying system updates, doing
- * factory resets, etc.).
+ * The value to pass as the 'reason' argument to reboot() to reboot into
+ * recovery mode for tasks other than applying system updates, such as
+ * doing factory resets.
* <p>
* Requires the {@link android.Manifest.permission#RECOVERY}
* permission (in addition to
@@ -398,6 +398,18 @@ public final class PowerManager {
public static final String REBOOT_RECOVERY = "recovery";
/**
+ * The value to pass as the 'reason' argument to reboot() to reboot into
+ * recovery mode for applying system updates.
+ * <p>
+ * Requires the {@link android.Manifest.permission#RECOVERY}
+ * permission (in addition to
+ * {@link android.Manifest.permission#REBOOT}).
+ * </p>
+ * @hide
+ */
+ public static final String REBOOT_RECOVERY_UPDATE = "recovery-update";
+
+ /**
* The value to pass as the 'reason' argument to reboot() when device owner requests a reboot on
* the device.
* @hide
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java
index 154c9bbab312..ddcd63520549 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;
@@ -66,15 +67,34 @@ 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 BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map");
- 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 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
@@ -287,6 +307,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.
@@ -301,30 +404,127 @@ 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 (!processed && filename.startsWith("/data/")) {
+ 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.
+ 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 command = filenameArg + localeArg;
+
+ 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 + " !!!");
+ }
- // If the package is on the /data partition, write the block map file
- // into COMMAND_FILE instead.
+ /**
+ * 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();
+
+ // 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 command = filenameArg + localeArg;
+
+ 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");
+ }
}
/**
@@ -434,27 +634,28 @@ 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();
- }
- // Having written the command file, go ahead and reboot
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- pm.reboot(PowerManager.REBOOT_RECOVERY);
+ // Write the command into BCB (bootloader control block).
+ RecoverySystem rs = (RecoverySystem) context.getSystemService(
+ Context.RECOVERY_SERVICE);
+ rs.setupBcb(command.toString());
- throw new IOException("Reboot failed (no permissions?)");
+ // 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?)");
+ }
}
/**
@@ -476,10 +677,10 @@ public class RecoverySystem {
// Only remove the OTA package if it's partially processed (uncrypt'd).
boolean reservePackage = BLOCK_MAP_FILE.exists();
- if (!reservePackage && UNCRYPT_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);
}
@@ -487,7 +688,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);
@@ -499,13 +700,13 @@ public class RecoverySystem {
// 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_FILE). This is to reduce the work for GmsCore
- // to avoid re-downloading everything again.
+ // 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_FILE.getName())) continue;
+ if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue;
recursiveDelete(new File(RECOVERY_DIR, names[i]));
}
@@ -533,6 +734,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.
*/
@@ -546,5 +780,14 @@ public class RecoverySystem {
/**
* @removed Was previously made visible by accident.
*/
- public RecoverySystem() { }
+ public RecoverySystem() {
+ mService = null;
+ }
+
+ /**
+ * @hide
+ */
+ public RecoverySystem(IRecoverySystem service) {
+ mService = service;
+ }
}
diff --git a/services/core/java/com/android/server/RecoverySystemService.java b/services/core/java/com/android/server/RecoverySystemService.java
new file mode 100644
index 000000000000..d237fe7b4c3e
--- /dev/null
+++ b/services/core/java/com/android/server/RecoverySystemService.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2016 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 com.android.server;
+
+import android.content.Context;
+import android.os.IRecoverySystem;
+import android.os.IRecoverySystemProgressListener;
+import android.os.RecoverySystem;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Slog;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * The recovery system service is responsible for coordinating recovery related
+ * functions on the device. It sets up (or clears) the bootloader control block
+ * (BCB), which will be read by the bootloader and the recovery image. It also
+ * triggers /system/bin/uncrypt via init to de-encrypt an OTA package on the
+ * /data partition so that it can be accessed under the recovery image.
+ */
+public final class RecoverySystemService extends SystemService {
+ private static final String TAG = "RecoverySystemService";
+ private static final boolean DEBUG = false;
+
+ // A pipe file to monitor the uncrypt progress.
+ private static final String UNCRYPT_STATUS_FILE = "/cache/recovery/uncrypt_status";
+ // Temporary command file to communicate between the system server and uncrypt.
+ private static final String COMMAND_FILE = "/cache/recovery/command";
+
+ private Context mContext;
+
+ public RecoverySystemService(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.RECOVERY_SERVICE, new BinderService());
+ }
+
+ private final class BinderService extends IRecoverySystem.Stub {
+ @Override // Binder call
+ public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
+ if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
+
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
+
+ // Write the filename into UNCRYPT_PACKAGE_FILE to be read by
+ // uncrypt.
+ RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
+
+ try (FileWriter uncryptFile = new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE)) {
+ uncryptFile.write(filename + "\n");
+ } catch (IOException e) {
+ Slog.e(TAG, "IOException when writing \"" + RecoverySystem.UNCRYPT_PACKAGE_FILE +
+ "\": " + e.getMessage());
+ return false;
+ }
+
+ // Create the status pipe file to communicate with uncrypt.
+ new File(UNCRYPT_STATUS_FILE).delete();
+ try {
+ Os.mkfifo(UNCRYPT_STATUS_FILE, 0600);
+ } catch (ErrnoException e) {
+ Slog.e(TAG, "ErrnoException when creating named pipe \"" + UNCRYPT_STATUS_FILE +
+ "\": " + e.getMessage());
+ return false;
+ }
+
+ // Trigger uncrypt via init.
+ SystemProperties.set("ctl.start", "uncrypt");
+
+ // Read the status from the pipe.
+ try (BufferedReader reader = new BufferedReader(new FileReader(UNCRYPT_STATUS_FILE))) {
+ int lastStatus = Integer.MIN_VALUE;
+ while (true) {
+ String str = reader.readLine();
+ try {
+ int status = Integer.parseInt(str);
+
+ // Avoid flooding the log with the same message.
+ if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
+ continue;
+ }
+ lastStatus = status;
+
+ if (status >= 0 && status <= 100) {
+ // Update status
+ Slog.i(TAG, "uncrypt read status: " + status);
+ if (listener != null) {
+ try {
+ listener.onProgress(status);
+ } catch (RemoteException unused) {
+ Slog.w(TAG, "RemoteException when posting progress");
+ }
+ }
+ if (status == 100) {
+ Slog.i(TAG, "uncrypt successfully finished.");
+ break;
+ }
+ } else {
+ // Error in /system/bin/uncrypt.
+ Slog.e(TAG, "uncrypt failed with status: " + status);
+ return false;
+ }
+ } catch (NumberFormatException unused) {
+ Slog.e(TAG, "uncrypt invalid status received: " + str);
+ return false;
+ }
+ }
+ } catch (IOException unused) {
+ Slog.e(TAG, "IOException when reading \"" + UNCRYPT_STATUS_FILE + "\".");
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override // Binder call
+ public boolean clearBcb() {
+ if (DEBUG) Slog.d(TAG, "clearBcb");
+ return setupOrClearBcb(false, null);
+ }
+
+ @Override // Binder call
+ public boolean setupBcb(String command) {
+ if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
+ return setupOrClearBcb(true, command);
+ }
+
+ private boolean setupOrClearBcb(boolean isSetup, String command) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
+
+ if (isSetup) {
+ // Set up the command file to be read by uncrypt.
+ try (FileWriter commandFile = new FileWriter(COMMAND_FILE)) {
+ commandFile.write(command + "\n");
+ } catch (IOException e) {
+ Slog.e(TAG, "IOException when writing \"" + COMMAND_FILE +
+ "\": " + e.getMessage());
+ return false;
+ }
+ }
+
+ // Create the status pipe file to communicate with uncrypt.
+ new File(UNCRYPT_STATUS_FILE).delete();
+ try {
+ Os.mkfifo(UNCRYPT_STATUS_FILE, 0600);
+ } catch (ErrnoException e) {
+ Slog.e(TAG, "ErrnoException when creating named pipe \"" +
+ UNCRYPT_STATUS_FILE + "\": " + e.getMessage());
+ return false;
+ }
+
+ if (isSetup) {
+ SystemProperties.set("ctl.start", "setup-bcb");
+ } else {
+ SystemProperties.set("ctl.start", "clear-bcb");
+ }
+
+ // Read the status from the pipe.
+ try (BufferedReader reader = new BufferedReader(new FileReader(UNCRYPT_STATUS_FILE))) {
+ while (true) {
+ String str = reader.readLine();
+ try {
+ int status = Integer.parseInt(str);
+
+ if (status == 100) {
+ Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear") +
+ " bcb successfully finished.");
+ break;
+ } else {
+ // Error in /system/bin/uncrypt.
+ Slog.e(TAG, "uncrypt failed with status: " + status);
+ return false;
+ }
+ } catch (NumberFormatException unused) {
+ Slog.e(TAG, "uncrypt invalid status received: " + str);
+ return false;
+ }
+ }
+ } catch (IOException unused) {
+ Slog.e(TAG, "IOException when reading \"" + UNCRYPT_STATUS_FILE + "\".");
+ return false;
+ }
+
+ // Delete the command file as we don't need it anymore.
+ new File(COMMAND_FILE).delete();
+ return true;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index dbaa598fb1b2..f901f9565fcc 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -2703,12 +2703,9 @@ public final class PowerManagerService extends SystemService
if (reason == null) {
reason = "";
}
- if (reason.equals(PowerManager.REBOOT_RECOVERY)) {
- // If we are rebooting to go into recovery, instead of
- // setting sys.powerctl directly we'll start the
- // pre-recovery service which will do some preparation for
- // recovery and then reboot for us.
- SystemProperties.set("ctl.start", "pre-recovery");
+ if (reason.equals(PowerManager.REBOOT_RECOVERY)
+ || reason.equals(PowerManager.REBOOT_RECOVERY_UPDATE)) {
+ SystemProperties.set("sys.powerctl", "reboot,recovery");
} else {
SystemProperties.set("sys.powerctl", "reboot," + reason);
}
@@ -3421,7 +3418,8 @@ public final class PowerManagerService extends SystemService
@Override // Binder call
public void reboot(boolean confirm, String reason, boolean wait) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT, null);
- if (PowerManager.REBOOT_RECOVERY.equals(reason)) {
+ if (PowerManager.REBOOT_RECOVERY.equals(reason)
+ || PowerManager.REBOOT_RECOVERY_UPDATE.equals(reason)) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
}
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index ac6a28e91ca8..26f9ffd8fa74 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -32,8 +32,10 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.FileUtils;
import android.os.Handler;
import android.os.PowerManager;
+import android.os.RecoverySystem;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -81,13 +83,9 @@ public final class ShutdownThread extends Thread {
private static Object sIsStartedGuard = new Object();
private static boolean sIsStarted = false;
- // uncrypt status files
- private static final String UNCRYPT_STATUS_FILE = "/cache/recovery/uncrypt_status";
- private static final String UNCRYPT_PACKAGE_FILE = "/cache/recovery/uncrypt_file";
-
private static boolean mReboot;
private static boolean mRebootSafeMode;
- private static boolean mRebootUpdate;
+ private static boolean mRebootHasProgressBar;
private static String mReason;
// Provides shutdown assurance in case the system_server is killed
@@ -213,7 +211,7 @@ public final class ShutdownThread extends Thread {
public static void reboot(final Context context, String reason, boolean confirm) {
mReboot = true;
mRebootSafeMode = false;
- mRebootUpdate = false;
+ mRebootHasProgressBar = false;
mReason = reason;
shutdownInner(context, confirm);
}
@@ -233,7 +231,7 @@ public final class ShutdownThread extends Thread {
mReboot = true;
mRebootSafeMode = true;
- mRebootUpdate = false;
+ mRebootHasProgressBar = false;
mReason = null;
shutdownInner(context, confirm);
}
@@ -250,10 +248,19 @@ public final class ShutdownThread extends Thread {
// Throw up a system dialog to indicate the device is rebooting / shutting down.
ProgressDialog pd = new ProgressDialog(context);
- // Path 1: Reboot to recovery and install the update
- // Condition: mReason == REBOOT_RECOVERY and mRebootUpdate == True
- // (mRebootUpdate is set by checking if /cache/recovery/uncrypt_file exists.)
- // UI: progress bar
+ // Path 1: Reboot to recovery for update
+ // Condition: mReason == REBOOT_RECOVERY_UPDATE
+ //
+ // Path 1a: uncrypt needed
+ // Condition: if /cache/recovery/uncrypt_file exists but
+ // /cache/recovery/block.map doesn't.
+ // UI: determinate progress bar (mRebootHasProgressBar == True)
+ //
+ // * Path 1a is expected to be removed once the GmsCore shipped on
+ // device always calls uncrypt prior to reboot.
+ //
+ // Path 1b: uncrypt already done
+ // UI: spinning circle only (no progress bar)
//
// Path 2: Reboot to recovery for factory reset
// Condition: mReason == REBOOT_RECOVERY
@@ -262,24 +269,31 @@ public final class ShutdownThread extends Thread {
// Path 3: Regular reboot / shutdown
// Condition: Otherwise
// UI: spinning circle only (no progress bar)
- if (PowerManager.REBOOT_RECOVERY.equals(mReason)) {
- mRebootUpdate = new File(UNCRYPT_PACKAGE_FILE).exists();
- if (mRebootUpdate) {
- pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_update_title));
- pd.setMessage(context.getText(
- com.android.internal.R.string.reboot_to_update_prepare));
+ if (PowerManager.REBOOT_RECOVERY_UPDATE.equals(mReason)) {
+ // We need the progress bar if uncrypt will be invoked during the
+ // reboot, which might be time-consuming.
+ mRebootHasProgressBar = RecoverySystem.UNCRYPT_PACKAGE_FILE.exists()
+ && !(RecoverySystem.BLOCK_MAP_FILE.exists());
+ pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_update_title));
+ if (mRebootHasProgressBar) {
pd.setMax(100);
- pd.setProgressNumberFormat(null);
- pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setProgress(0);
pd.setIndeterminate(false);
- } else {
- // Factory reset path. Set the dialog message accordingly.
- pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_reset_title));
+ pd.setProgressNumberFormat(null);
+ pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage(context.getText(
- com.android.internal.R.string.reboot_to_reset_message));
+ com.android.internal.R.string.reboot_to_update_prepare));
+ } else {
pd.setIndeterminate(true);
+ pd.setMessage(context.getText(
+ com.android.internal.R.string.reboot_to_update_reboot));
}
+ } else if (PowerManager.REBOOT_RECOVERY.equals(mReason)) {
+ // Factory reset path. Set the dialog message accordingly.
+ pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_reset_title));
+ pd.setMessage(context.getText(
+ com.android.internal.R.string.reboot_to_reset_message));
+ pd.setIndeterminate(true);
} else {
pd.setTitle(context.getText(com.android.internal.R.string.power_off));
pd.setMessage(context.getText(com.android.internal.R.string.shutdown_progress));
@@ -379,7 +393,7 @@ public final class ShutdownThread extends Thread {
if (delay <= 0) {
Log.w(TAG, "Shutdown broadcast timed out");
break;
- } else if (mRebootUpdate) {
+ } else if (mRebootHasProgressBar) {
int status = (int)((MAX_BROADCAST_TIME - delay) * 1.0 *
BROADCAST_STOP_PERCENT / MAX_BROADCAST_TIME);
sInstance.setRebootProgress(status, null);
@@ -390,7 +404,7 @@ public final class ShutdownThread extends Thread {
}
}
}
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(BROADCAST_STOP_PERCENT, null);
}
@@ -404,7 +418,7 @@ public final class ShutdownThread extends Thread {
} catch (RemoteException e) {
}
}
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(ACTIVITY_MANAGER_STOP_PERCENT, null);
}
@@ -415,13 +429,13 @@ public final class ShutdownThread extends Thread {
if (pm != null) {
pm.shutdown();
}
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(PACKAGE_MANAGER_STOP_PERCENT, null);
}
// Shutdown radios.
shutdownRadios(MAX_RADIO_WAIT_TIME);
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(RADIO_STOP_PERCENT, null);
}
@@ -455,7 +469,7 @@ public final class ShutdownThread extends Thread {
if (delay <= 0) {
Log.w(TAG, "Shutdown wait timed out");
break;
- } else if (mRebootUpdate) {
+ } else if (mRebootHasProgressBar) {
int status = (int)((MAX_SHUTDOWN_WAIT_TIME - delay) * 1.0 *
(MOUNT_SERVICE_STOP_PERCENT - RADIO_STOP_PERCENT) /
MAX_SHUTDOWN_WAIT_TIME);
@@ -468,10 +482,11 @@ public final class ShutdownThread extends Thread {
}
}
}
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
sInstance.setRebootProgress(MOUNT_SERVICE_STOP_PERCENT, null);
- // If it's to reboot to install update, invoke uncrypt via init service.
+ // If it's to reboot to install an update and uncrypt hasn't been
+ // done yet, trigger it now.
uncrypt();
}
@@ -549,7 +564,7 @@ public final class ShutdownThread extends Thread {
long delay = endTime - SystemClock.elapsedRealtime();
while (delay > 0) {
- if (mRebootUpdate) {
+ if (mRebootHasProgressBar) {
int status = (int)((timeout - delay) * 1.0 *
(RADIO_STOP_PERCENT - PACKAGE_MANAGER_STOP_PERCENT) / timeout);
status += PACKAGE_MANAGER_STOP_PERCENT;
@@ -651,66 +666,40 @@ public final class ShutdownThread extends Thread {
private void uncrypt() {
Log.i(TAG, "Calling uncrypt and monitoring the progress...");
+ final RecoverySystem.ProgressListener progressListener =
+ new RecoverySystem.ProgressListener() {
+ @Override
+ public void onProgress(int status) {
+ if (status >= 0 && status < 100) {
+ // Scale down to [MOUNT_SERVICE_STOP_PERCENT, 100).
+ status = (int)(status * (100.0 - MOUNT_SERVICE_STOP_PERCENT) / 100);
+ status += MOUNT_SERVICE_STOP_PERCENT;
+ CharSequence msg = mContext.getText(
+ com.android.internal.R.string.reboot_to_update_package);
+ sInstance.setRebootProgress(status, msg);
+ } else if (status == 100) {
+ CharSequence msg = mContext.getText(
+ com.android.internal.R.string.reboot_to_update_reboot);
+ sInstance.setRebootProgress(status, msg);
+ } else {
+ // Ignored
+ }
+ }
+ };
+
final boolean[] done = new boolean[1];
done[0] = false;
Thread t = new Thread() {
@Override
public void run() {
- // Create the status pipe file to communicate with /system/bin/uncrypt.
- new File(UNCRYPT_STATUS_FILE).delete();
+ RecoverySystem rs = (RecoverySystem) mContext.getSystemService(
+ Context.RECOVERY_SERVICE);
+ String filename = null;
try {
- Os.mkfifo(UNCRYPT_STATUS_FILE, 0600);
- } catch (ErrnoException e) {
- Log.w(TAG, "ErrnoException when creating named pipe \"" + UNCRYPT_STATUS_FILE +
- "\": " + e.getMessage());
- }
-
- SystemProperties.set("ctl.start", "uncrypt");
-
- // Read the status from the pipe.
- try (BufferedReader reader = new BufferedReader(
- new FileReader(UNCRYPT_STATUS_FILE))) {
-
- int lastStatus = Integer.MIN_VALUE;
- while (true) {
- String str = reader.readLine();
- try {
- int status = Integer.parseInt(str);
-
- // Avoid flooding the log with the same message.
- if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
- continue;
- }
- lastStatus = status;
-
- if (status >= 0 && status < 100) {
- // Update status
- Log.d(TAG, "uncrypt read status: " + status);
- // Scale down to [MOUNT_SERVICE_STOP_PERCENT, 100).
- status = (int)(status * (100.0 - MOUNT_SERVICE_STOP_PERCENT) / 100);
- status += MOUNT_SERVICE_STOP_PERCENT;
- CharSequence msg = mContext.getText(
- com.android.internal.R.string.reboot_to_update_package);
- sInstance.setRebootProgress(status, msg);
- } else if (status == 100) {
- Log.d(TAG, "uncrypt successfully finished.");
- CharSequence msg = mContext.getText(
- com.android.internal.R.string.reboot_to_update_reboot);
- sInstance.setRebootProgress(status, msg);
- break;
- } else {
- // Error in /system/bin/uncrypt. Or it's rebooting to recovery
- // to perform other operations (e.g. factory reset).
- Log.d(TAG, "uncrypt failed with status: " + status);
- break;
- }
- } catch (NumberFormatException unused) {
- Log.d(TAG, "uncrypt invalid status received: " + str);
- break;
- }
- }
- } catch (IOException unused) {
- Log.w(TAG, "IOException when reading \"" + UNCRYPT_STATUS_FILE + "\".");
+ filename = FileUtils.readTextFile(RecoverySystem.UNCRYPT_PACKAGE_FILE, 0, null);
+ rs.processPackage(mContext, new File(filename), progressListener);
+ } catch (IOException e) {
+ Log.e(TAG, "Error uncrypting file", e);
}
done[0] = true;
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c8545732864b..c75f98f63c79 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -341,8 +341,8 @@ public final class SystemServer {
// always make sure uncrypt gets executed properly when needed.
// If '/cache/recovery/block.map' hasn't been created, stop the
// reboot which will fail for sure, and get a chance to capture a
- // bugreport when that's still feasible. (Bug; 26444951)
- if (PowerManager.REBOOT_RECOVERY.equals(reason)) {
+ // bugreport when that's still feasible. (Bug: 26444951)
+ if (PowerManager.REBOOT_RECOVERY_UPDATE.equals(reason)) {
File packageFile = new File(UNCRYPT_PACKAGE_FILE);
if (packageFile.exists()) {
String filename = null;
@@ -833,6 +833,10 @@ public final class SystemServer {
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
+ if (!disableNonCoreServices) {
+ mSystemServiceManager.startService(RecoverySystemService.class);
+ }
+
/*
* MountService has a few dependencies: Notification Manager and
* AppWidget Provider. Make sure MountService is completely started