Add support for update-on-boot feature.

Add a separate system service RecoverySystemService to handle recovery
related requests (calling uncrypt to de-encrypt the OTA package on the
/data partition, setting up bootloader control block (aka BCB) and etc).

We used to trigger uncrypt in ShutdownThread before rebooting into
recovery. Now we expose new SystemApi (RecoverySystem.processPackage())
to allow the caller (e.g. GmsCore) to call that upfront before
initiating a reboot. This will reduce the reboot time and get rid of the
progress bar ("processing update package"). However, we need to reserve
the functionality in ShutdownThread to optionally call uncrypt if
finding that's still needed.

In order to support the update-on-boot feature, we also add new
SystemApis scheduleUpdateOnBoot() and cancelScheduledUpdate() into
android.os.RecoverySystem. They allow the caller (e.g. GmsCore) to
schedule / cancel an update by setting up the BCB, which will be read by
the bootloader and the recovery image. With the new SystemApis, an
update package can be processed (uncrypt'd) in the background and
scheduled to be installed at the next boot.

Bug: 26830925
Change-Id: Ic606fcf5b31c54ce54f0ab12c1768fef0fa64560
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 0000000..d237fe7
--- /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 dbaa598..f901f95 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 @@
         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 @@
         @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 ac6a28e..26f9ffd 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.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 @@
     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 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 @@
 
         mReboot = true;
         mRebootSafeMode = true;
-        mRebootUpdate = false;
+        mRebootHasProgressBar = false;
         mReason = null;
         shutdownInner(context, confirm);
     }
@@ -250,10 +248,19 @@
         // 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 @@
         // 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 @@
                 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 @@
                 }
             }
         }
-        if (mRebootUpdate) {
+        if (mRebootHasProgressBar) {
             sInstance.setRebootProgress(BROADCAST_STOP_PERCENT, null);
         }
 
@@ -404,7 +418,7 @@
             } catch (RemoteException e) {
             }
         }
-        if (mRebootUpdate) {
+        if (mRebootHasProgressBar) {
             sInstance.setRebootProgress(ACTIVITY_MANAGER_STOP_PERCENT, null);
         }
 
@@ -415,13 +429,13 @@
         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 @@
                 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 @@
                 }
             }
         }
-        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 @@
 
                 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 @@
     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 ac972a9..f53f0a9 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -341,8 +341,8 @@
             // 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;
@@ -832,6 +832,10 @@
                 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