Merge "Make RecoverySystemService more testable"

This commit is contained in:
Kenny Root
2019-11-22 16:14:38 +00:00
committed by Gerrit Code Review
4 changed files with 648 additions and 208 deletions

View File

@@ -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";
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;
}
// 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);
}
}
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.");
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 @@ public final class RecoverySystemService extends SystemService {
}
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);
}
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}