Merge "Make RecoverySystemService more testable"
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user