Merge "Make RecoverySystemService more testable"
diff --git a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
index d78aaa5..fe18fbf 100644
--- a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
+++ b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
@@ -27,6 +27,7 @@
 import android.os.SystemProperties;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.SystemService;
 
 import libcore.io.IoUtils;
@@ -35,6 +36,7 @@
 import java.io.DataOutputStream;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 
 /**
  * The recovery system service is responsible for coordinating recovery related
@@ -43,7 +45,7 @@
  * 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 {
+public class RecoverySystemService extends IRecoverySystem.Stub {
     private static final String TAG = "RecoverySystemService";
     private static final boolean DEBUG = false;
 
@@ -51,191 +53,321 @@
     private static final String UNCRYPT_SOCKET = "uncrypt";
 
     // The init services that communicate with /system/bin/uncrypt.
-    private static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
-    private static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
-    private static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";
-
-    private static final int SOCKET_CONNECTION_MAX_RETRY = 30;
+    @VisibleForTesting
+    static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
+    @VisibleForTesting
+    static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
+    @VisibleForTesting
+    static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";
 
     private static final Object sRequestLock = new Object();
 
-    private Context mContext;
+    private static final int SOCKET_CONNECTION_MAX_RETRY = 30;
 
-    public RecoverySystemService(Context context) {
-        super(context);
-        mContext = context;
+    private final Injector mInjector;
+    private final Context mContext;
+
+    static class Injector {
+        protected final Context mContext;
+
+        Injector(Context context) {
+            mContext = context;
+        }
+
+        public Context getContext() {
+            return mContext;
+        }
+
+        public PowerManager getPowerManager() {
+            return (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+        }
+
+        public String systemPropertiesGet(String key) {
+            return SystemProperties.get(key);
+        }
+
+        public void systemPropertiesSet(String key, String value) {
+            SystemProperties.set(key, value);
+        }
+
+        public boolean uncryptPackageFileDelete() {
+            return RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
+        }
+
+        public String getUncryptPackageFileName() {
+            return RecoverySystem.UNCRYPT_PACKAGE_FILE.getName();
+        }
+
+        public FileWriter getUncryptPackageFileWriter() throws IOException {
+            return new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE);
+        }
+
+        public UncryptSocket connectService() {
+            UncryptSocket socket = new UncryptSocket();
+            if (!socket.connectService()) {
+                socket.close();
+                return null;
+            }
+            return socket;
+        }
+
+        public void threadSleep(long millis) throws InterruptedException {
+            Thread.sleep(millis);
+        }
     }
 
-    @Override
-    public void onStart() {
-        publishBinderService(Context.RECOVERY_SERVICE, new BinderService());
+    /**
+     * Handles the lifecycle events for the RecoverySystemService.
+     */
+    public static final class Lifecycle extends SystemService {
+        public Lifecycle(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onStart() {
+            RecoverySystemService recoverySystemService = new RecoverySystemService(getContext());
+            publishBinderService(Context.RECOVERY_SERVICE, recoverySystemService);
+        }
     }
 
-    private final class BinderService extends IRecoverySystem.Stub {
-        @Override // Binder call
-        public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
-            if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
+    private RecoverySystemService(Context context) {
+        this(new Injector(context));
+    }
 
-            synchronized (sRequestLock) {
-                mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
+    @VisibleForTesting
+    RecoverySystemService(Injector injector) {
+        mInjector = injector;
+        mContext = injector.getContext();
+    }
 
-                final boolean available = checkAndWaitForUncryptService();
-                if (!available) {
-                    Slog.e(TAG, "uncrypt service is unavailable.");
-                    return false;
-                }
+    @Override // Binder call
+    public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
+        if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
 
-                // Write the filename into UNCRYPT_PACKAGE_FILE to be read by
-                // uncrypt.
-                RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
+        synchronized (sRequestLock) {
+            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
 
-                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);
-                    return false;
-                }
+            if (!checkAndWaitForUncryptService()) {
+                Slog.e(TAG, "uncrypt service is unavailable.");
+                return false;
+            }
 
-                // Trigger uncrypt via init.
-                SystemProperties.set("ctl.start", "uncrypt");
+            // Write the filename into uncrypt package file to be read by
+            // uncrypt.
+            mInjector.uncryptPackageFileDelete();
 
-                // Connect to the uncrypt service socket.
-                LocalSocket socket = connectService();
-                if (socket == null) {
-                    Slog.e(TAG, "Failed to connect to uncrypt socket");
-                    return false;
-                }
+            try (FileWriter uncryptFile = mInjector.getUncryptPackageFileWriter()) {
+                uncryptFile.write(filename + "\n");
+            } catch (IOException e) {
+                Slog.e(TAG, "IOException when writing \""
+                        + mInjector.getUncryptPackageFileName() + "\":", e);
+                return false;
+            }
 
-                // Read the status from the socket.
-                DataInputStream dis = null;
-                DataOutputStream dos = null;
-                try {
-                    dis = new DataInputStream(socket.getInputStream());
-                    dos = new DataOutputStream(socket.getOutputStream());
-                    int lastStatus = Integer.MIN_VALUE;
-                    while (true) {
-                        int status = dis.readInt();
-                        // Avoid flooding the log with the same message.
-                        if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
-                            continue;
-                        }
-                        lastStatus = status;
+            // Trigger uncrypt via init.
+            mInjector.systemPropertiesSet("ctl.start", "uncrypt");
 
-                        if (status >= 0 && status <= 100) {
-                            // Update status
-                            Slog.i(TAG, "uncrypt read status: " + status);
-                            if (listener != null) {
-                                try {
-                                    listener.onProgress(status);
-                                } catch (RemoteException ignored) {
-                                    Slog.w(TAG, "RemoteException when posting progress");
-                                }
-                            }
-                            if (status == 100) {
-                                Slog.i(TAG, "uncrypt successfully finished.");
-                                // Ack receipt of the final status code. uncrypt
-                                // waits for the ack so the socket won't be
-                                // destroyed before we receive the code.
-                                dos.writeInt(0);
-                                break;
-                            }
-                        } else {
-                            // Error in /system/bin/uncrypt.
-                            Slog.e(TAG, "uncrypt failed with status: " + status);
-                            // Ack receipt of the final status code. uncrypt waits
-                            // for the ack so the socket won't be destroyed before
-                            // we receive the code.
-                            dos.writeInt(0);
-                            return false;
-                        }
+            // Connect to the uncrypt service socket.
+            UncryptSocket socket = mInjector.connectService();
+            if (socket == null) {
+                Slog.e(TAG, "Failed to connect to uncrypt socket");
+                return false;
+            }
+
+            // Read the status from the socket.
+            try {
+                int lastStatus = Integer.MIN_VALUE;
+                while (true) {
+                    int status = socket.getPercentageUncrypted();
+                    // Avoid flooding the log with the same message.
+                    if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
+                        continue;
                     }
-                } catch (IOException e) {
-                    Slog.e(TAG, "IOException when reading status: ", e);
-                    return false;
-                } finally {
-                    IoUtils.closeQuietly(dis);
-                    IoUtils.closeQuietly(dos);
-                    IoUtils.closeQuietly(socket);
-                }
+                    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 ignored) {
+                                Slog.w(TAG, "RemoteException when posting progress");
+                            }
+                        }
+                        if (status == 100) {
+                            Slog.i(TAG, "uncrypt successfully finished.");
+                            // Ack receipt of the final status code. uncrypt
+                            // waits for the ack so the socket won't be
+                            // destroyed before we receive the code.
+                            socket.sendAck();
+                            break;
+                        }
+                    } else {
+                        // Error in /system/bin/uncrypt.
+                        Slog.e(TAG, "uncrypt failed with status: " + status);
+                        // Ack receipt of the final status code. uncrypt waits
+                        // for the ack so the socket won't be destroyed before
+                        // we receive the code.
+                        socket.sendAck();
+                        return false;
+                    }
+                }
+            } catch (IOException e) {
+                Slog.e(TAG, "IOException when reading status: ", e);
+                return false;
+            } finally {
+                socket.close();
+            }
+
+            return true;
+        }
+    }
+
+    @Override // Binder call
+    public boolean clearBcb() {
+        if (DEBUG) Slog.d(TAG, "clearBcb");
+        synchronized (sRequestLock) {
+            return setupOrClearBcb(false, null);
+        }
+    }
+
+    @Override // Binder call
+    public boolean setupBcb(String command) {
+        if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
+        synchronized (sRequestLock) {
+            return setupOrClearBcb(true, command);
+        }
+    }
+
+    @Override // Binder call
+    public void rebootRecoveryWithCommand(String command) {
+        if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");
+        synchronized (sRequestLock) {
+            if (!setupOrClearBcb(true, command)) {
+                return;
+            }
+
+            // Having set up the BCB, go ahead and reboot.
+            PowerManager pm = mInjector.getPowerManager();
+            pm.reboot(PowerManager.REBOOT_RECOVERY);
+        }
+    }
+
+    /**
+     * Check if any of the init services is still running. If so, we cannot
+     * start a new uncrypt/setup-bcb/clear-bcb service right away; otherwise
+     * it may break the socket communication since init creates / deletes
+     * the socket (/dev/socket/uncrypt) on service start / exit.
+     */
+    private boolean checkAndWaitForUncryptService() {
+        for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
+            final String uncryptService = mInjector.systemPropertiesGet(INIT_SERVICE_UNCRYPT);
+            final String setupBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_SETUP_BCB);
+            final String clearBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_CLEAR_BCB);
+            final boolean busy = "running".equals(uncryptService)
+                    || "running".equals(setupBcbService) || "running".equals(clearBcbService);
+            if (DEBUG) {
+                Slog.i(TAG, "retry: " + retry + " busy: " + busy
+                        + " uncrypt: [" + uncryptService + "]"
+                        + " setupBcb: [" + setupBcbService + "]"
+                        + " clearBcb: [" + clearBcbService + "]");
+            }
+
+            if (!busy) {
                 return true;
             }
-        }
 
-        @Override // Binder call
-        public boolean clearBcb() {
-            if (DEBUG) Slog.d(TAG, "clearBcb");
-            synchronized (sRequestLock) {
-                return setupOrClearBcb(false, null);
+            try {
+                mInjector.threadSleep(1000);
+            } catch (InterruptedException e) {
+                Slog.w(TAG, "Interrupted:", e);
             }
         }
 
-        @Override // Binder call
-        public boolean setupBcb(String command) {
-            if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
-            synchronized (sRequestLock) {
-                return setupOrClearBcb(true, command);
-            }
-        }
+        return false;
+    }
 
-        @Override // Binder call
-        public void rebootRecoveryWithCommand(String command) {
-            if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");
-            synchronized (sRequestLock) {
-                if (!setupOrClearBcb(true, command)) {
-                    return;
-                }
+    private boolean setupOrClearBcb(boolean isSetup, String command) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
 
-                // Having set up the BCB, go ahead and reboot.
-                PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
-                pm.reboot(PowerManager.REBOOT_RECOVERY);
-            }
-        }
-
-        /**
-         * Check if any of the init services is still running. If so, we cannot
-         * start a new uncrypt/setup-bcb/clear-bcb service right away; otherwise
-         * it may break the socket communication since init creates / deletes
-         * the socket (/dev/socket/uncrypt) on service start / exit.
-         */
-        private boolean checkAndWaitForUncryptService() {
-            for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
-                final String uncryptService = SystemProperties.get(INIT_SERVICE_UNCRYPT);
-                final String setupBcbService = SystemProperties.get(INIT_SERVICE_SETUP_BCB);
-                final String clearBcbService = SystemProperties.get(INIT_SERVICE_CLEAR_BCB);
-                final boolean busy = "running".equals(uncryptService) ||
-                        "running".equals(setupBcbService) || "running".equals(clearBcbService);
-                if (DEBUG) {
-                    Slog.i(TAG, "retry: " + retry + " busy: " + busy +
-                            " uncrypt: [" + uncryptService + "]" +
-                            " setupBcb: [" + setupBcbService + "]" +
-                            " clearBcb: [" + clearBcbService + "]");
-                }
-
-                if (!busy) {
-                    return true;
-                }
-
-                try {
-                    Thread.sleep(1000);
-                } catch (InterruptedException e) {
-                    Slog.w(TAG, "Interrupted:", e);
-                }
-            }
-
+        final boolean available = checkAndWaitForUncryptService();
+        if (!available) {
+            Slog.e(TAG, "uncrypt service is unavailable.");
             return false;
         }
 
-        private LocalSocket connectService() {
-            LocalSocket socket = new LocalSocket();
+        if (isSetup) {
+            mInjector.systemPropertiesSet("ctl.start", "setup-bcb");
+        } else {
+            mInjector.systemPropertiesSet("ctl.start", "clear-bcb");
+        }
+
+        // Connect to the uncrypt service socket.
+        UncryptSocket socket = mInjector.connectService();
+        if (socket == null) {
+            Slog.e(TAG, "Failed to connect to uncrypt socket");
+            return false;
+        }
+
+        try {
+            // Send the BCB commands if it's to setup BCB.
+            if (isSetup) {
+                socket.sendCommand(command);
+            }
+
+            // Read the status from the socket.
+            int status = socket.getPercentageUncrypted();
+
+            // Ack receipt of the status code. uncrypt waits for the ack so
+            // the socket won't be destroyed before we receive the code.
+            socket.sendAck();
+
+            if (status == 100) {
+                Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear")
+                        + " bcb successfully finished.");
+            } else {
+                // Error in /system/bin/uncrypt.
+                Slog.e(TAG, "uncrypt failed with status: " + status);
+                return false;
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "IOException when communicating with uncrypt:", e);
+            return false;
+        } finally {
+            socket.close();
+        }
+
+        return true;
+    }
+
+    /**
+     * Provides a wrapper for the low-level details of framing packets sent to the uncrypt
+     * socket.
+     */
+    public static class UncryptSocket {
+        private LocalSocket mLocalSocket;
+        private DataInputStream mInputStream;
+        private DataOutputStream mOutputStream;
+
+        /**
+         * Attempt to connect to the uncrypt service. Connection will be retried for up to
+         * {@link #SOCKET_CONNECTION_MAX_RETRY} times. If the connection is unsuccessful, the
+         * socket will be closed. If the connection is successful, the connection must be closed
+         * by the caller.
+         *
+         * @return true if connection was successful, false if unsuccessful
+         */
+        public boolean connectService() {
+            mLocalSocket = new LocalSocket();
             boolean done = false;
             // The uncrypt socket will be created by init upon receiving the
             // service request. It may not be ready by this point. So we will
             // keep retrying until success or reaching timeout.
             for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
                 try {
-                    socket.connect(new LocalSocketAddress(UNCRYPT_SOCKET,
+                    mLocalSocket.connect(new LocalSocketAddress(UNCRYPT_SOCKET,
                             LocalSocketAddress.Namespace.RESERVED));
                     done = true;
                     break;
@@ -249,71 +381,69 @@
             }
             if (!done) {
                 Slog.e(TAG, "Timed out connecting to uncrypt socket");
-                return null;
-            }
-            return socket;
-        }
-
-        private boolean setupOrClearBcb(boolean isSetup, String command) {
-            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
-
-            final boolean available = checkAndWaitForUncryptService();
-            if (!available) {
-                Slog.e(TAG, "uncrypt service is unavailable.");
+                close();
                 return false;
             }
 
-            if (isSetup) {
-                SystemProperties.set("ctl.start", "setup-bcb");
-            } else {
-                SystemProperties.set("ctl.start", "clear-bcb");
-            }
-
-            // Connect to the uncrypt service socket.
-            LocalSocket socket = connectService();
-            if (socket == null) {
-                Slog.e(TAG, "Failed to connect to uncrypt socket");
-                return false;
-            }
-
-            DataInputStream dis = null;
-            DataOutputStream dos = null;
             try {
-                dis = new DataInputStream(socket.getInputStream());
-                dos = new DataOutputStream(socket.getOutputStream());
-
-                // Send the BCB commands if it's to setup BCB.
-                if (isSetup) {
-                    byte[] cmdUtf8 = command.getBytes("UTF-8");
-                    dos.writeInt(cmdUtf8.length);
-                    dos.write(cmdUtf8, 0, cmdUtf8.length);
-                }
-
-                // Read the status from the socket.
-                int status = dis.readInt();
-
-                // Ack receipt of the status code. uncrypt waits for the ack so
-                // the socket won't be destroyed before we receive the code.
-                dos.writeInt(0);
-
-                if (status == 100) {
-                    Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear") +
-                            " bcb successfully finished.");
-                } else {
-                    // Error in /system/bin/uncrypt.
-                    Slog.e(TAG, "uncrypt failed with status: " + status);
-                    return false;
-                }
+                mInputStream = new DataInputStream(mLocalSocket.getInputStream());
+                mOutputStream = new DataOutputStream(mLocalSocket.getOutputStream());
             } catch (IOException e) {
-                Slog.e(TAG, "IOException when communicating with uncrypt:", e);
+                close();
                 return false;
-            } finally {
-                IoUtils.closeQuietly(dis);
-                IoUtils.closeQuietly(dos);
-                IoUtils.closeQuietly(socket);
             }
 
             return true;
         }
+
+        /**
+         * Sends a command to the uncrypt service.
+         *
+         * @param command command to send to the uncrypt service
+         * @throws IOException if the socket is closed or there was an error writing to the socket
+         */
+        public void sendCommand(String command) throws IOException {
+            if (mLocalSocket.isClosed()) {
+                throw new IOException("socket is closed");
+            }
+
+            byte[] cmdUtf8 = command.getBytes(StandardCharsets.UTF_8);
+            mOutputStream.writeInt(cmdUtf8.length);
+            mOutputStream.write(cmdUtf8, 0, cmdUtf8.length);
+        }
+
+        /**
+         * Reads the status from the uncrypt service which is usually represented as a percentage.
+         * @return an integer representing the percentage completed
+         * @throws IOException if the socket was closed or there was an error reading the socket
+         */
+        public int getPercentageUncrypted() throws IOException {
+            if (mLocalSocket.isClosed()) {
+                throw new IOException("socket is closed");
+            }
+
+            return mInputStream.readInt();
+        }
+
+        /**
+         * Sends a confirmation to the uncrypt service.
+         * @throws IOException if the socket was closed or there was an error writing to the socket
+         */
+        public void sendAck() throws IOException {
+            if (mLocalSocket.isClosed()) {
+                throw new IOException("socket is closed");
+            }
+
+            mOutputStream.writeInt(0);
+        }
+
+        /**
+         * Closes the socket and all underlying data streams.
+         */
+        public void close() {
+            IoUtils.closeQuietly(mInputStream);
+            IoUtils.closeQuietly(mOutputStream);
+            IoUtils.closeQuietly(mLocalSocket);
+        }
     }
 }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c1b7a50..313f884 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -699,7 +699,7 @@
 
         // Bring up recovery system in case a rescue party needs a reboot
         traceBeginAndSlog("StartRecoverySystemService");
-        mSystemServiceManager.startService(RecoverySystemService.class);
+        mSystemServiceManager.startService(RecoverySystemService.Lifecycle.class);
         traceEnd();
 
         // Now that we have the bare essentials of the OS up and running, take
diff --git a/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTest.java b/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTest.java
new file mode 100644
index 0000000..1f312bf
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2019 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.recoverysystem;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.IPowerManager;
+import android.os.IRecoverySystemProgressListener;
+import android.os.Looper;
+import android.os.PowerManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileWriter;
+
+/**
+ * atest FrameworksServicesTests:RecoverySystemServiceTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RecoverySystemServiceTest {
+    private RecoverySystemService mRecoverySystemService;
+    private RecoverySystemServiceTestable.FakeSystemProperties mSystemProperties;
+    private RecoverySystemService.UncryptSocket mUncryptSocket;
+    private Context mContext;
+    private IPowerManager mIPowerManager;
+    private FileWriter mUncryptUpdateFileWriter;
+
+    @Before
+    public void setup() {
+        mContext = mock(Context.class);
+        mSystemProperties = new RecoverySystemServiceTestable.FakeSystemProperties();
+        mUncryptSocket = mock(RecoverySystemService.UncryptSocket.class);
+        mUncryptUpdateFileWriter = mock(FileWriter.class);
+
+        Looper looper = InstrumentationRegistry.getContext().getMainLooper();
+        mIPowerManager = mock(IPowerManager.class);
+        PowerManager powerManager = new PowerManager(mock(Context.class), mIPowerManager,
+                new Handler(looper));
+
+        mRecoverySystemService = new RecoverySystemServiceTestable(mContext, mSystemProperties,
+                powerManager, mUncryptUpdateFileWriter, mUncryptSocket);
+    }
+
+    @Test
+    public void clearBcb_success() throws Exception {
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        when(mUncryptSocket.getPercentageUncrypted()).thenReturn(100);
+
+        assertThat(mRecoverySystemService.clearBcb(), is(true));
+
+        assertThat(mSystemProperties.getCtlStart(), is("clear-bcb"));
+        verify(mUncryptSocket).sendAck();
+        verify(mUncryptSocket).close();
+    }
+
+    @Test
+    public void clearBcb_uncrypt_failure() throws Exception {
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        when(mUncryptSocket.getPercentageUncrypted()).thenReturn(0);
+
+        assertThat(mRecoverySystemService.clearBcb(), is(false));
+
+        assertThat(mSystemProperties.getCtlStart(), is("clear-bcb"));
+        verify(mUncryptSocket).sendAck();
+        verify(mUncryptSocket).close();
+    }
+
+    @Test(expected = SecurityException.class)
+    public void clearBcb_noPerm() {
+        doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        mRecoverySystemService.clearBcb();
+    }
+
+    @Test
+    public void setupBcb_success() throws Exception {
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        when(mUncryptSocket.getPercentageUncrypted()).thenReturn(100);
+
+        assertThat(mRecoverySystemService.setupBcb("foo"), is(true));
+
+        assertThat(mSystemProperties.getCtlStart(), is("setup-bcb"));
+        verify(mUncryptSocket).sendCommand("foo");
+        verify(mUncryptSocket).sendAck();
+        verify(mUncryptSocket).close();
+    }
+
+    @Test
+    public void setupBcb_uncrypt_failure() throws Exception {
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        when(mUncryptSocket.getPercentageUncrypted()).thenReturn(0);
+
+        assertThat(mRecoverySystemService.setupBcb("foo"), is(false));
+
+        assertThat(mSystemProperties.getCtlStart(), is("setup-bcb"));
+        verify(mUncryptSocket).sendCommand("foo");
+        verify(mUncryptSocket).sendAck();
+        verify(mUncryptSocket).close();
+    }
+
+    @Test(expected = SecurityException.class)
+    public void setupBcb_noPerm() {
+        doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        mRecoverySystemService.setupBcb("foo");
+    }
+
+    @Test
+    public void rebootRecoveryWithCommand_success() throws Exception {
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        when(mUncryptSocket.getPercentageUncrypted()).thenReturn(100);
+
+        mRecoverySystemService.rebootRecoveryWithCommand("foo");
+
+        assertThat(mSystemProperties.getCtlStart(), is("setup-bcb"));
+        verify(mUncryptSocket).sendCommand("foo");
+        verify(mUncryptSocket).sendAck();
+        verify(mUncryptSocket).close();
+        verify(mIPowerManager).reboot(anyBoolean(), eq("recovery"), anyBoolean());
+    }
+
+    @Test
+    public void rebootRecoveryWithCommand_failure() throws Exception {
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        when(mUncryptSocket.getPercentageUncrypted()).thenReturn(0);
+
+        mRecoverySystemService.rebootRecoveryWithCommand("foo");
+
+        assertThat(mSystemProperties.getCtlStart(), is("setup-bcb"));
+        verify(mUncryptSocket).sendCommand("foo");
+        verify(mUncryptSocket).sendAck();
+        verify(mUncryptSocket).close();
+        verifyNoMoreInteractions(mIPowerManager);
+    }
+
+    @Test(expected = SecurityException.class)
+    public void rebootRecoveryWithCommand_noPerm() {
+        doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        mRecoverySystemService.rebootRecoveryWithCommand("foo");
+    }
+
+    @Test
+    public void uncrypt_success() throws Exception {
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(android.Manifest.permission.RECOVERY), any());
+        when(mUncryptSocket.getPercentageUncrypted()).thenReturn(0, 5, 25, 50, 90, 99, 100);
+
+        IRecoverySystemProgressListener listener = mock(IRecoverySystemProgressListener.class);
+        assertThat(mRecoverySystemService.uncrypt("foo.zip", listener), is(true));
+
+        assertThat(mSystemProperties.getCtlStart(), is("uncrypt"));
+        verify(mUncryptSocket, times(7)).getPercentageUncrypted();
+        verify(mUncryptSocket).sendAck();
+        verify(mUncryptSocket).close();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTestable.java b/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTestable.java
new file mode 100644
index 0000000..a986b71
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTestable.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2019 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.recoverysystem;
+
+import android.content.Context;
+import android.os.PowerManager;
+
+import java.io.FileWriter;
+
+public class RecoverySystemServiceTestable extends RecoverySystemService {
+    private static class MockInjector extends RecoverySystemService.Injector {
+        private final FakeSystemProperties mSystemProperties;
+        private final PowerManager mPowerManager;
+        private final FileWriter mUncryptPackageFileWriter;
+        private final UncryptSocket mUncryptSocket;
+
+        MockInjector(Context context, FakeSystemProperties systemProperties,
+                PowerManager powerManager, FileWriter uncryptPackageFileWriter,
+                UncryptSocket uncryptSocket) {
+            super(context);
+            mSystemProperties = systemProperties;
+            mPowerManager = powerManager;
+            mUncryptPackageFileWriter = uncryptPackageFileWriter;
+            mUncryptSocket = uncryptSocket;
+        }
+
+        @Override
+        public PowerManager getPowerManager() {
+            return mPowerManager;
+        }
+
+        @Override
+        public String systemPropertiesGet(String key) {
+            return mSystemProperties.get(key);
+        }
+
+        @Override
+        public void systemPropertiesSet(String key, String value) {
+            mSystemProperties.set(key, value);
+        }
+
+        @Override
+        public boolean uncryptPackageFileDelete() {
+            return true;
+        }
+
+        @Override
+        public String getUncryptPackageFileName() {
+            return "mock-file.txt";
+        }
+
+        @Override
+        public FileWriter getUncryptPackageFileWriter() {
+            return mUncryptPackageFileWriter;
+        }
+
+        @Override
+        public UncryptSocket connectService() {
+            return mUncryptSocket;
+        }
+
+        @Override
+        public void threadSleep(long millis) {
+        }
+    }
+
+    RecoverySystemServiceTestable(Context context, FakeSystemProperties systemProperties,
+            PowerManager powerManager, FileWriter uncryptPackageFileWriter,
+            UncryptSocket uncryptSocket) {
+        super(new MockInjector(context, systemProperties, powerManager, uncryptPackageFileWriter,
+                uncryptSocket));
+    }
+
+    public static class FakeSystemProperties {
+        private String mCtlStart = null;
+
+        public String get(String key) {
+            if (RecoverySystemService.INIT_SERVICE_UNCRYPT.equals(key)
+                    || RecoverySystemService.INIT_SERVICE_SETUP_BCB.equals(key)
+                    || RecoverySystemService.INIT_SERVICE_CLEAR_BCB.equals(key)) {
+                return null;
+            } else {
+                throw new IllegalArgumentException("unexpected test key: " + key);
+            }
+        }
+
+        public void set(String key, String value) {
+            if ("ctl.start".equals(key)) {
+                mCtlStart = value;
+            } else {
+                throw new IllegalArgumentException("unexpected test key: " + key);
+            }
+        }
+
+        public String getCtlStart() {
+            return mCtlStart;
+        }
+    }
+}