Wait for uncrypt to finish before rebooting

/system/bin/uncrypt needs to be triggered to prepare the OTA package
before rebooting into the recovery. For larger packages, uncrypt may be
killed before it finishes the work after the timeout. Change to monitor
the uncrypt status and show the progress to user.

Needs matching changes in bootable/recovery/uncrypt, system/core and
external/sepolicy.

Also pick up the two NPE fixes in commits
9bb765448df43d41e0a3edb7de1d1641c9251c35 and
da3f63ffb87397943546a7c5c893ce98f2489df2.

Bug: 20012567
Bug: 20949086
(cherry picked from commit 90237f7beb55dae79cdcba5271f96be778573737)
Change-Id: Ibf2fc80032967e5f6cda3cd469005dd29665c87c
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java
index b879c83..52b10be 100644
--- a/core/java/android/os/RecoverySystem.java
+++ b/core/java/android/os/RecoverySystem.java
@@ -71,6 +71,7 @@
     /** Used to communicate with recovery.  See bootable/recovery/recovery.c. */
     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_";
 
@@ -333,8 +334,21 @@
     public static void installPackage(Context context, File packageFile)
         throws IOException {
         String filename = packageFile.getCanonicalPath();
+
+        FileWriter uncryptFile = new FileWriter(UNCRYPT_FILE);
+        try {
+            uncryptFile.write(filename + "\n");
+        } finally {
+            uncryptFile.close();
+        }
         Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
 
+        // If the package is on the /data partition, write the block map file
+        // into COMMAND_FILE 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);
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 34dc5ad..60cbf41 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -393,6 +393,10 @@
     <!-- Spoken description for ringer normal option. [CHAR LIMIT=NONE] -->
     <string name="silent_mode_ring">Ringer on</string>
 
+    <!-- Reboot to Recovery Progress Dialog. This is shown before it reboots to recovery. -->
+    <string name="reboot_to_recovery_title">Prepare for update</string>
+    <string name="reboot_to_recovery_progress">Processing the update package\u2026</string>
+
     <!-- Shutdown Progress Dialog. This is shown if the user chooses to power off the phone. -->
     <string name="shutdown_progress">Shutting down\u2026</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index eec989e..1bf2f53 100755
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -780,6 +780,8 @@
   <java-symbol type="string" name="mobile_provisioning_url" />
   <java-symbol type="string" name="mobile_redirected_provisioning_url" />
   <java-symbol type="string" name="quick_contacts_not_available" />
+  <java-symbol type="string" name="reboot_to_recovery_progress" />
+  <java-symbol type="string" name="reboot_to_recovery_title" />
   <java-symbol type="string" name="reboot_safemode_confirm" />
   <java-symbol type="string" name="reboot_safemode_title" />
   <java-symbol type="string" name="relationTypeAssistant" />
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 9786b42..32c0fb8 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -2357,8 +2357,7 @@
     /**
      * Low-level function to reboot the device. On success, this
      * function doesn't return. If more than 20 seconds passes from
-     * the time a reboot is requested (120 seconds for reboot to
-     * recovery), this method returns.
+     * the time a reboot is requested, this method returns.
      *
      * @param reason code to pass to the kernel (e.g. "recovery"), or null.
      */
@@ -2366,27 +2365,21 @@
         if (reason == null) {
             reason = "";
         }
-        long duration;
         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.
-            //
-            // This preparation can take more than 20 seconds if
-            // there's a very large update package, so lengthen the
-            // timeout.  We have seen 750MB packages take 3-4 minutes
             SystemProperties.set("ctl.start", "pre-recovery");
-            duration = 300 * 1000L;
         } else {
             SystemProperties.set("sys.powerctl", "reboot," + reason);
-            duration = 20 * 1000L;
         }
         try {
-            Thread.sleep(duration);
+            Thread.sleep(20 * 1000L);
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
+        Slog.wtf(TAG, "Unexpected return from lowLevelReboot!");
     }
 
     @Override // Watchdog.Monitor implementation
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index f61a0c8..927fc45 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
- 
+
 package com.android.server.power;
 
 import android.app.ActivityManagerNative;
@@ -43,6 +43,8 @@
 import android.os.SystemVibrator;
 import android.os.storage.IMountService;
 import android.os.storage.IMountShutdownObserver;
+import android.system.ErrnoException;
+import android.system.Os;
 
 import com.android.internal.telephony.ITelephony;
 import com.android.server.pm.PackageManagerService;
@@ -50,6 +52,11 @@
 import android.util.Log;
 import android.view.WindowManager;
 
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+
 public final class ShutdownThread extends Thread {
     // constants
     private static final String TAG = "ShutdownThread";
@@ -58,14 +65,18 @@
     private static final int MAX_BROADCAST_TIME = 10*1000;
     private static final int MAX_SHUTDOWN_WAIT_TIME = 20*1000;
     private static final int MAX_RADIO_WAIT_TIME = 12*1000;
+    private static final int MAX_UNCRYPT_WAIT_TIME = 15*60*1000;
 
     // length of vibration before shutting down
     private static final int SHUTDOWN_VIBRATE_MS = 500;
-    
+
     // state tracking
     private static Object sIsStartedGuard = new Object();
     private static boolean sIsStarted = false;
-    
+
+    // uncrypt status file
+    private static final String UNCRYPT_STATUS_FILE = "/cache/recovery/uncrypt_status";
+
     private static boolean mReboot;
     private static boolean mRebootSafeMode;
     private static String mRebootReason;
@@ -93,10 +104,11 @@
     private Handler mHandler;
 
     private static AlertDialog sConfirmDialog;
-    
+    private ProgressDialog mProgressDialog;
+
     private ShutdownThread() {
     }
- 
+
     /**
      * Request a clean shutdown, waiting for subsystems to clean up their
      * state etc.  Must be called from a Looper thread in which its UI
@@ -220,7 +232,11 @@
         // throw up an indeterminate system dialog to indicate radio is
         // shutting down.
         ProgressDialog pd = new ProgressDialog(context);
-        pd.setTitle(context.getText(com.android.internal.R.string.power_off));
+        if (PowerManager.REBOOT_RECOVERY.equals(mRebootReason)) {
+            pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_recovery_title));
+        } else {
+            pd.setTitle(context.getText(com.android.internal.R.string.power_off));
+        }
         pd.setMessage(context.getText(com.android.internal.R.string.shutdown_progress));
         pd.setIndeterminate(true);
         pd.setCancelable(false);
@@ -228,6 +244,7 @@
 
         pd.show();
 
+        sInstance.mProgressDialog = pd;
         sInstance.mContext = context;
         sInstance.mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
 
@@ -301,14 +318,14 @@
         }
 
         Log.i(TAG, "Sending shutdown broadcast...");
-        
+
         // First send the high-level shut down broadcast.
         mActionDone = false;
         Intent intent = new Intent(Intent.ACTION_SHUTDOWN);
         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         mContext.sendOrderedBroadcastAsUser(intent,
                 UserHandle.ALL, null, br, mHandler, 0, null, null);
-        
+
         final long endTime = SystemClock.elapsedRealtime() + MAX_BROADCAST_TIME;
         synchronized (mActionDoneSync) {
             while (!mActionDone) {
@@ -323,9 +340,9 @@
                 }
             }
         }
-        
+
         Log.i(TAG, "Shutting down activity manager...");
-        
+
         final IActivityManager am =
             ActivityManagerNative.asInterface(ServiceManager.checkService("activity"));
         if (am != null) {
@@ -384,9 +401,55 @@
             }
         }
 
+        // If it's to reboot into recovery, invoke uncrypt via init service.
+        if (PowerManager.REBOOT_RECOVERY.equals(mRebootReason)) {
+            uncrypt();
+        }
+
         rebootOrShutdown(mContext, mReboot, mRebootReason);
     }
 
+    private void prepareUncryptProgress() {
+        // Reset the dialog message to show the decrypt process.
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mProgressDialog != null) {
+                    mProgressDialog.dismiss();
+                }
+                // It doesn't work to change the style of the existing
+                // one. Have to create a new one.
+                ProgressDialog pd = new ProgressDialog(mContext);
+
+                pd.setTitle(mContext.getText(
+                        com.android.internal.R.string.reboot_to_recovery_title));
+                pd.setMessage(mContext.getText(
+                        com.android.internal.R.string.reboot_to_recovery_progress));
+                pd.setIndeterminate(false);
+                pd.setMax(100);
+                pd.setCancelable(false);
+                pd.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+                pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+                pd.setProgressNumberFormat(null);
+                pd.setProgress(0);
+
+                mProgressDialog = pd;
+                mProgressDialog.show();
+            }
+        });
+    }
+
+    private void setUncryptProgress(final int progress) {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mProgressDialog != null) {
+                    mProgressDialog.setProgress(progress);
+                }
+            }
+        });
+    }
+
     private void shutdownRadios(int timeout) {
         // If a radio is wedged, disabling it may hang so we do this work in another thread,
         // just in case.
@@ -531,4 +594,78 @@
         Log.i(TAG, "Performing low-level shutdown...");
         PowerManagerService.lowLevelShutdown();
     }
+
+    private void uncrypt() {
+        Log.i(TAG, "Calling uncrypt and monitoring the progress...");
+
+        // Update the ProcessDialog message and style.
+        sInstance.prepareUncryptProgress();
+
+        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();
+                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 last_status = 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 == last_status && last_status != Integer.MIN_VALUE) {
+                                continue;
+                            }
+                            last_status = status;
+
+                            if (status >= 0 && status < 100) {
+                                // Update status
+                                Log.d(TAG, "uncrypt read status: " + status);
+                                sInstance.setUncryptProgress(status);
+                            } else if (status == 100) {
+                                Log.d(TAG, "uncrypt successfully finished.");
+                                sInstance.setUncryptProgress(status);
+                                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 + "\".");
+                }
+                done[0] = true;
+            }
+        };
+        t.start();
+
+        try {
+            t.join(MAX_UNCRYPT_WAIT_TIME);
+        } catch (InterruptedException unused) {
+        }
+        if (!done[0]) {
+            Log.w(TAG, "Timed out waiting for uncrypt.");
+        }
+    }
 }