diff options
| author | 2019-11-22 16:14:38 +0000 | |
|---|---|---|
| committer | 2019-11-22 16:14:38 +0000 | |
| commit | 49af39e751a777f5bb05b4c9e5491e8762f2fc9a (patch) | |
| tree | c1d6f66352d28f56e41a7140ea15763fc032f91b | |
| parent | 87ed7d7816001ce31e581e6f801209472c324bd1 (diff) | |
| parent | d508e1e61f77baaef750c2f33f8ebbc31c46324e (diff) | |
Merge "Make RecoverySystemService more testable"
4 files changed, 634 insertions, 194 deletions
diff --git a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java index d78aaa5f9c9d..fe18fbf2a782 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.RemoteException; 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.DataInputStream; 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 @@ import java.io.IOException; * 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 @@ public final class RecoverySystemService extends SystemService { 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"; + @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 static final int SOCKET_CONNECTION_MAX_RETRY = 30; - private static final Object sRequestLock = new Object(); + 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); + } - private Context mContext; + public String systemPropertiesGet(String key) { + return SystemProperties.get(key); + } - public RecoverySystemService(Context context) { - super(context); - mContext = context; + 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; - - 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; + // Trigger uncrypt via init. + mInjector.systemPropertiesSet("ctl.start", "uncrypt"); + + // 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; + } + 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"); } - } 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; } + 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 { - IoUtils.closeQuietly(dis); - IoUtils.closeQuietly(dos); - IoUtils.closeQuietly(socket); } - - return true; + } 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 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 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); } + } - @Override // Binder call - public void rebootRecoveryWithCommand(String command) { - if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]"); - synchronized (sRequestLock) { - if (!setupOrClearBcb(true, command)) { - return; - } + /** + * 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 + "]"); + } - // Having set up the BCB, go ahead and reboot. - PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - pm.reboot(PowerManager.REBOOT_RECOVERY); + if (!busy) { + return true; + } + + try { + mInjector.threadSleep(1000); + } catch (InterruptedException e) { + Slog.w(TAG, "Interrupted:", e); } } - /** - * 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 + "]"); - } + return false; + } - if (!busy) { - return true; - } + private boolean setupOrClearBcb(boolean isSetup, String command) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null); - 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; + } + + 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(); } - private LocalSocket connectService() { - LocalSocket socket = new LocalSocket(); + 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 @@ public final class RecoverySystemService extends SystemService { } if (!done) { Slog.e(TAG, "Timed out connecting to uncrypt socket"); - return null; + close(); + return false; } - 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."); + try { + mInputStream = new DataInputStream(mLocalSocket.getInputStream()); + mOutputStream = new DataOutputStream(mLocalSocket.getOutputStream()); + } catch (IOException e) { + close(); return false; } - if (isSetup) { - SystemProperties.set("ctl.start", "setup-bcb"); - } else { - SystemProperties.set("ctl.start", "clear-bcb"); - } + return true; + } - // Connect to the uncrypt service socket. - LocalSocket socket = connectService(); - if (socket == null) { - Slog.e(TAG, "Failed to connect to uncrypt socket"); - return false; + /** + * 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"); } - 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); - } + byte[] cmdUtf8 = command.getBytes(StandardCharsets.UTF_8); + mOutputStream.writeInt(cmdUtf8.length); + mOutputStream.write(cmdUtf8, 0, cmdUtf8.length); + } - // Read the status from the socket. - int status = dis.readInt(); + /** + * 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"); + } - // 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); + return mInputStream.readInt(); + } - 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 { - IoUtils.closeQuietly(dis); - IoUtils.closeQuietly(dos); - IoUtils.closeQuietly(socket); + /** + * 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"); } - return true; + 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 c1b7a50a8623..313f884c08cb 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -699,7 +699,7 @@ public final class SystemServer { // 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 000000000000..1f312bf1296d --- /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 000000000000..a986b71d556f --- /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; + } + } +} |