Merge "Progress on permissions sync transport migration."

This commit is contained in:
Jeff Sharkey
2022-06-30 01:25:49 +00:00
committed by Android (Google) Code Review
19 changed files with 527 additions and 842 deletions

View File

@@ -9010,8 +9010,6 @@ package android.companion {
method @Deprecated public boolean hasNotificationAccess(android.content.ComponentName);
method public void requestNotificationAccess(android.content.ComponentName);
method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException;
method @Deprecated public void startSystemDataTransfer(int) throws android.companion.DeviceNotAssociatedException;
method public void startSystemDataTransfer(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.companion.CompanionException>) throws android.companion.DeviceNotAssociatedException;
method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException;
field public static final String EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION";
field @Deprecated public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE";
@@ -9027,19 +9025,14 @@ package android.companion {
public abstract class CompanionDeviceService extends android.app.Service {
ctor public CompanionDeviceService();
method @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public final void dispatchMessageToSystem(int, int, @NonNull byte[]) throws android.companion.DeviceNotAssociatedException;
method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
method @Deprecated @MainThread public void onDeviceAppeared(@NonNull String);
method @MainThread public void onDeviceAppeared(@NonNull android.companion.AssociationInfo);
method @Deprecated @MainThread public void onDeviceDisappeared(@NonNull String);
method @MainThread public void onDeviceDisappeared(@NonNull android.companion.AssociationInfo);
method public void onMessageDispatchedFromSystem(int, int, @NonNull byte[]);
field public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService";
}
public class CompanionException extends java.lang.RuntimeException {
}
public interface DeviceFilter<D extends android.os.Parcelable> extends android.os.Parcelable {
}

View File

@@ -800,18 +800,15 @@ public final class CompanionDeviceManager {
*
* @hide
*/
@Deprecated
@RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
public void dispatchMessage(int messageId, int associationId, @NonNull byte[] message)
throws DeviceNotAssociatedException {
try {
mService.dispatchMessage(messageId, associationId, message);
} catch (RemoteException e) {
ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class);
throw e.rethrowFromSystemServer();
}
Log.w(LOG_TAG, "dispatchMessage replaced by attachSystemDataTransport");
}
/** {@hide} */
@RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
public final void attachSystemDataTransport(int associationId, @NonNull InputStream in,
@NonNull OutputStream out) throws DeviceNotAssociatedException {
synchronized (mTransports) {
@@ -830,6 +827,7 @@ public final class CompanionDeviceManager {
}
/** {@hide} */
@RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
public final void detachSystemDataTransport(int associationId)
throws DeviceNotAssociatedException {
synchronized (mTransports) {
@@ -927,7 +925,7 @@ public final class CompanionDeviceManager {
*
* <p>The permission transfer doesn't happen immediately after the call or user consented.
* The app needs to trigger the system data transfer manually by calling
* {@link #startSystemDataTransfer(int)}, when it confirms the communication channel between
* {@code #startSystemDataTransfer(int)}, when it confirms the communication channel between
* the two devices is established.</p>
*
* @param associationId The unique {@link AssociationInfo#getId ID} assigned to the association
@@ -965,8 +963,8 @@ public final class CompanionDeviceManager {
* @param associationId The unique {@link AssociationInfo#getId ID} assigned to the Association
* of the companion device recorded by CompanionDeviceManager
* @throws DeviceNotAssociatedException Exception if the companion device is not associated
*
* @deprecated Use {@link #startSystemDataTransfer(int, Executor, OutcomeReceiver)} instead.
* @hide
*/
@Deprecated
@UserHandleAware
@@ -993,6 +991,7 @@ public final class CompanionDeviceManager {
* @param executor The executor which will be used to invoke the result callback.
* @param result The callback to notify the app of the result of the system data transfer.
* @throws DeviceNotAssociatedException Exception if the companion device is not associated
* @hide
*/
@UserHandleAware
public void startSystemDataTransfer(
@@ -1125,12 +1124,14 @@ public final class CompanionDeviceManager {
public void start() throws IOException {
final ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair();
mLocalIn = new ParcelFileDescriptor.AutoCloseInputStream(pair[0]);
mLocalOut = new ParcelFileDescriptor.AutoCloseOutputStream(pair[0]);
final ParcelFileDescriptor localFd = pair[0];
final ParcelFileDescriptor remoteFd = pair[1];
mLocalIn = new ParcelFileDescriptor.AutoCloseInputStream(localFd);
mLocalOut = new ParcelFileDescriptor.AutoCloseOutputStream(localFd);
try {
mService.attachSystemDataTransport(mContext.getPackageName(),
mContext.getUserId(), mAssociationId, pair[1]);
mContext.getUserId(), mAssociationId, remoteFd);
} catch (RemoteException e) {
throw new IOException("Failed to configure transport", e);
}

View File

@@ -156,9 +156,12 @@ public abstract class CompanionDeviceService extends Service {
* @param messageId system assigned id of the message to be sent
* @param associationId association id of the associated device
* @param message message to be sent
* @hide
*/
@Deprecated
public void onMessageDispatchedFromSystem(int messageId, int associationId,
@NonNull byte[] message) {
Log.w(LOG_TAG, "Replaced by attachSystemDataTransport");
// do nothing. Companion apps can override this function for system to send messages.
}
@@ -185,22 +188,14 @@ public abstract class CompanionDeviceService extends Service {
* @param messageId id of the message
* @param associationId id of the associated device
* @param message message received from the associated device
* @hide
*/
@Deprecated
@RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
public final void dispatchMessageToSystem(int messageId, int associationId,
@NonNull byte[] message)
throws DeviceNotAssociatedException {
if (getBaseContext() == null) {
Log.e(LOG_TAG, "Dispatch failed. Start your service before calling this method.");
return;
}
CompanionDeviceManager companionDeviceManager =
getSystemService(CompanionDeviceManager.class);
if (companionDeviceManager != null) {
companionDeviceManager.dispatchMessage(messageId, associationId, message);
} else {
Log.e(LOG_TAG, "CompanionDeviceManager is null. Can't dispatch messages.");
}
Log.w(LOG_TAG, "Replaced by attachSystemDataTransport");
}
/**
@@ -223,10 +218,13 @@ public abstract class CompanionDeviceService extends Service {
* device
* @hide
*/
@RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
public final void attachSystemDataTransport(int associationId, @NonNull InputStream in,
@NonNull OutputStream out) throws DeviceNotAssociatedException {
getSystemService(CompanionDeviceManager.class)
.attachSystemDataTransport(associationId, in, out);
.attachSystemDataTransport(associationId,
Objects.requireNonNull(in),
Objects.requireNonNull(out));
}
/**
@@ -236,6 +234,7 @@ public abstract class CompanionDeviceService extends Service {
* @param associationId id of the associated device
* @hide
*/
@RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
public final void detachSystemDataTransport(int associationId)
throws DeviceNotAssociatedException {
getSystemService(CompanionDeviceManager.class)
@@ -299,13 +298,5 @@ public abstract class CompanionDeviceService extends Service {
public void onDeviceDisappeared(AssociationInfo associationInfo) {
mMainHandler.postAtFrontOfQueue(() -> mService.onDeviceDisappeared(associationInfo));
}
@Override
public void onMessageDispatchedFromSystem(int messageId, int associationId,
@NonNull byte[] message) {
mMainHandler.postAtFrontOfQueue(
() -> mService.onMessageDispatchedFromSystem(messageId, associationId,
message));
}
}
}

View File

@@ -19,7 +19,10 @@ package android.companion;
import android.annotation.NonNull;
/**
* {@code CompanionException} can be thrown during the companion system data transfer process.
* {@code CompanionException} can be thrown during the companion system data
* transfer process.
*
* @hide
*/
public class CompanionException extends RuntimeException {
/** @hide */

View File

@@ -63,8 +63,6 @@ interface ICompanionDeviceManager {
void createAssociation(in String packageName, in String macAddress, int userId,
in byte[] certificate);
void dispatchMessage(int messageId, int associationId, in byte[] message);
void addOnAssociationsChangedListener(IOnAssociationsChangedListener listener, int userId);
void removeOnAssociationsChangedListener(IOnAssociationsChangedListener listener, int userId);

View File

@@ -22,5 +22,4 @@ import android.companion.AssociationInfo;
oneway interface ICompanionDeviceService {
void onDeviceAppeared(in AssociationInfo associationInfo);
void onDeviceDisappeared(in AssociationInfo associationInfo);
void onMessageDispatchedFromSystem(in int messageId, in int associationId, in byte[] message);
}

View File

@@ -16,12 +16,15 @@
package android.companion;
import android.content.Context;
import android.os.SystemClock;
import android.test.InstrumentationTestCase;
import android.util.Log;
import com.android.internal.util.HexDump;
import libcore.util.EmptyArray;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
@@ -31,69 +34,81 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Random;
public class SystemDataTransportTest extends InstrumentationTestCase {
private static final String TAG = "SystemDataTransportTest";
private static final int COMMAND_INVALID = 0xF00DCAFE;
private static final int COMMAND_PING_V0 = 0x50490000;
private static final int COMMAND_PONG_V0 = 0x504F0000;
private static final int MESSAGE_INVALID = 0xF00DCAFE;
private static final int MESSAGE_REQUEST_INVALID = 0x63636363; // ????
private static final int MESSAGE_REQUEST_PING = 0x63807378; // ?PIN
private static final int MESSAGE_RESPONSE_INVALID = 0x33333333; // !!!!
private static final int MESSAGE_RESPONSE_SUCCESS = 0x33838567; // !SUC
private static final int MESSAGE_RESPONSE_FAILURE = 0x33706573; // !FAI
private Context mContext;
private CompanionDeviceManager mCdm;
private int mAssociationId;
@Override
protected void setUp() throws Exception {
super.setUp();
mCdm = getInstrumentation().getTargetContext()
.getSystemService(CompanionDeviceManager.class);
mContext = getInstrumentation().getTargetContext();
mCdm = mContext.getSystemService(CompanionDeviceManager.class);
mAssociationId = createAssociation();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
mCdm.disassociate(mAssociationId);
}
public void testPingHandRolled() {
// NOTE: These packets are explicitly hand-rolled to verify wire format;
// the remainder of the tests are fine using generated packets
// PING v0 with payload "HELLO WORLD!"
// MESSAGE_REQUEST_PING with payload "HELLO WORLD!"
final byte[] input = new byte[] {
0x50, 0x49, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0C,
0x63, (byte) 0x80, 0x73, 0x78, // message: MESSAGE_REQUEST_PING
0x00, 0x00, 0x00, 0x2A, // sequence: 42
0x00, 0x00, 0x00, 0x0C, // length: 12
0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21,
};
// PONG v0 with payload "HELLO WORLD!"
// MESSAGE_RESPONSE_SUCCESS with payload "HELLO WORLD!"
final byte[] expected = new byte[] {
0x50, 0x4F, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0C,
0x33, (byte) 0x83, (byte) 0x85, 0x67, // message: MESSAGE_RESPONSE_SUCCESS
0x00, 0x00, 0x00, 0x2A, // sequence: 42
0x00, 0x00, 0x00, 0x0C, // length: 12
0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21,
};
final ByteArrayInputStream in = new ByteArrayInputStream(input);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
mCdm.attachSystemDataTransport(42, in, out);
final byte[] actual = waitForByteArray(out, expected.length);
assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
assertTransportBehavior(input, expected);
}
public void testPingTrickle() {
final byte[] input = generatePacket(COMMAND_PING_V0, TAG);
final byte[] expected = generatePacket(COMMAND_PONG_V0, TAG);
final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, TAG);
final byte[] expected = generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, TAG);
final ByteArrayInputStream in = new ByteArrayInputStream(input);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
mCdm.attachSystemDataTransport(42, new TrickleInputStream(in), out);
mCdm.attachSystemDataTransport(mAssociationId, new TrickleInputStream(in), out);
final byte[] actual = waitForByteArray(out, expected.length);
assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
}
public void testPingDelay() {
final byte[] input = generatePacket(COMMAND_PING_V0, TAG);
final byte[] expected = generatePacket(COMMAND_PONG_V0, TAG);
final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, TAG);
final byte[] expected = generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, TAG);
final ByteArrayInputStream in = new ByteArrayInputStream(input);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
mCdm.attachSystemDataTransport(42, new DelayingInputStream(in, 1000),
mCdm.attachSystemDataTransport(mAssociationId, new DelayingInputStream(in, 1000),
new DelayingOutputStream(out, 1000));
final byte[] actual = waitForByteArray(out, expected.length);
@@ -104,49 +119,54 @@ public class SystemDataTransportTest extends InstrumentationTestCase {
final byte[] blob = new byte[500_000];
new Random().nextBytes(blob);
final byte[] input = generatePacket(COMMAND_PING_V0, blob);
final byte[] expected = generatePacket(COMMAND_PONG_V0, blob);
final ByteArrayInputStream in = new ByteArrayInputStream(input);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
mCdm.attachSystemDataTransport(42, in, out);
final byte[] actual = waitForByteArray(out, expected.length);
assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, blob);
final byte[] expected = generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, blob);
assertTransportBehavior(input, expected);
}
public void testMutiplePingPing() {
final byte[] input = concat(generatePacket(COMMAND_PING_V0, "red"),
generatePacket(COMMAND_PING_V0, "green"));
final byte[] expected = concat(generatePacket(COMMAND_PONG_V0, "red"),
generatePacket(COMMAND_PONG_V0, "green"));
final ByteArrayInputStream in = new ByteArrayInputStream(input);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
mCdm.attachSystemDataTransport(42, in, out);
final byte[] actual = waitForByteArray(out, expected.length);
assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
final byte[] input = concat(
generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, "red"),
generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
final byte[] expected = concat(
generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, "red"),
generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green"));
assertTransportBehavior(input, expected);
}
public void testMultipleInvalidPing() {
final byte[] input = concat(generatePacket(COMMAND_INVALID, "red"),
generatePacket(COMMAND_PING_V0, "green"));
final byte[] expected = generatePacket(COMMAND_PONG_V0, "green");
final byte[] input = concat(
generatePacket(MESSAGE_INVALID, /* sequence */ 1, "red"),
generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
final byte[] expected =
generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green");
assertTransportBehavior(input, expected);
}
final ByteArrayInputStream in = new ByteArrayInputStream(input);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
mCdm.attachSystemDataTransport(42, in, out);
public void testMultipleInvalidRequestPing() {
final byte[] input = concat(
generatePacket(MESSAGE_REQUEST_INVALID, /* sequence */ 1, "red"),
generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
final byte[] expected = concat(
generatePacket(MESSAGE_RESPONSE_FAILURE, /* sequence */ 1),
generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green"));
assertTransportBehavior(input, expected);
}
final byte[] actual = waitForByteArray(out, expected.length);
assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
public void testMultipleInvalidResponsePing() {
final byte[] input = concat(
generatePacket(MESSAGE_RESPONSE_INVALID, /* sequence */ 1, "red"),
generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
final byte[] expected =
generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green");
assertTransportBehavior(input, expected);
}
public void testDoubleAttach() {
// Connect an empty connection that is stalled out
final InputStream in = new EmptyInputStream();
final OutputStream out = new ByteArrayOutputStream();
mCdm.attachSystemDataTransport(42, in, out);
mCdm.attachSystemDataTransport(mAssociationId, in, out);
SystemClock.sleep(1000);
// Attach a second transport that has some packets; it should disconnect
@@ -154,17 +174,57 @@ public class SystemDataTransportTest extends InstrumentationTestCase {
testPingHandRolled();
}
public static byte[] concat(byte[] a, byte[] b) {
return ByteBuffer.allocate(a.length + b.length).put(a).put(b).array();
public static byte[] concat(byte[]... blobs) {
int length = 0;
for (byte[] blob : blobs) {
length += blob.length;
}
final ByteBuffer buf = ByteBuffer.allocate(length);
for (byte[] blob : blobs) {
buf.put(blob);
}
return buf.array();
}
public static byte[] generatePacket(int command, String data) {
return generatePacket(command, data.getBytes(StandardCharsets.UTF_8));
public static byte[] generatePacket(int message, int sequence) {
return generatePacket(message, sequence, EmptyArray.BYTE);
}
public static byte[] generatePacket(int command, byte[] data) {
return ByteBuffer.allocate(data.length + 8)
.putInt(command).putInt(data.length).put(data).array();
public static byte[] generatePacket(int message, int sequence, String data) {
return generatePacket(message, sequence, data.getBytes(StandardCharsets.UTF_8));
}
public static byte[] generatePacket(int message, int sequence, byte[] data) {
return ByteBuffer.allocate(data.length + 12)
.putInt(message)
.putInt(sequence)
.putInt(data.length)
.put(data)
.array();
}
private int createAssociation() {
getInstrumentation().getUiAutomation().executeShellCommand("cmd companiondevice associate "
+ mContext.getUserId() + " " + mContext.getPackageName() + " AA:BB:CC:DD:EE:FF");
List<AssociationInfo> infos;
for (int i = 0; i < 100; i++) {
infos = mCdm.getMyAssociations();
if (!infos.isEmpty()) {
return infos.get(0).getId();
} else {
SystemClock.sleep(100);
}
}
throw new AssertionError();
}
private void assertTransportBehavior(byte[] input, byte[] expected) {
final ByteArrayInputStream in = new ByteArrayInputStream(input);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
mCdm.attachSystemDataTransport(mAssociationId, in, out);
final byte[] actual = waitForByteArray(out, expected.length);
assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
}
private static byte[] waitForByteArray(ByteArrayOutputStream out, int size) {

View File

@@ -11,7 +11,6 @@ filegroup {
name: "services.companion-sources",
srcs: [
"java/**/*.java",
"java/**/*.proto",
],
path: "java",
visibility: ["//frameworks/base/services"],
@@ -20,9 +19,6 @@ filegroup {
java_library_static {
name: "services.companion",
defaults: ["platform_service_defaults"],
proto: {
type: "stream",
},
srcs: [":services.companion-sources"],
libs: [
"app-compat-annotations",

View File

@@ -244,29 +244,6 @@ public class CompanionApplicationController {
primaryServiceConnector.postOnDeviceDisappeared(association);
}
/** Pass an encrypted secure message to the companion application for transporting. */
public void dispatchMessage(@UserIdInt int userId, @NonNull String packageName,
int associationId, int messageId, @NonNull byte[] message) {
if (DEBUG) {
Log.i(TAG, "dispatchMessage() u" + userId + "/" + packageName
+ " associationId=" + associationId);
}
final CompanionDeviceServiceConnector primaryServiceConnector =
getPrimaryServiceConnector(userId, packageName);
if (primaryServiceConnector == null) {
if (DEBUG) {
Log.e(TAG, "dispatchMessage(): "
+ "u" + userId + "/" + packageName + " is NOT bound.");
Log.d(TAG, "Stacktrace", new Throwable());
}
return;
}
primaryServiceConnector.postOnMessageDispatchedFromSystem(associationId, messageId,
message);
}
void dump(@NonNull PrintWriter out) {
out.append("Companion Device Application Controller: \n");

View File

@@ -106,11 +106,10 @@ import com.android.internal.util.DumpUtils;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.companion.datatransfer.CompanionMessageProcessor;
import com.android.server.companion.datatransfer.SystemDataTransferProcessor;
import com.android.server.companion.datatransfer.SystemDataTransferRequestStore;
import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
import com.android.server.companion.securechannel.CompanionSecureCommunicationsManager;
import com.android.server.companion.transport.CompanionTransportManager;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -151,10 +150,9 @@ public class CompanionDeviceManagerService extends SystemService {
private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
private AssociationRequestsProcessor mAssociationRequestsProcessor;
private SystemDataTransferProcessor mSystemDataTransferProcessor;
private CompanionMessageProcessor mCompanionMessageProcessor;
private CompanionDevicePresenceMonitor mDevicePresenceMonitor;
private CompanionApplicationController mCompanionAppController;
private CompanionSecureCommunicationsManager mSecureCommsManager;
private CompanionTransportManager mTransportManager;
private final ActivityTaskManagerInternal mAtmInternal;
private final ActivityManagerInternal mAmInternal;
@@ -238,11 +236,9 @@ public class CompanionDeviceManagerService extends SystemService {
/* cdmService */this, mAssociationStore);
mCompanionAppController = new CompanionApplicationController(
context, mApplicationControllerCallback);
mSecureCommsManager = new CompanionSecureCommunicationsManager(
mAssociationStore, mCompanionAppController);
mCompanionMessageProcessor = new CompanionMessageProcessor(mSecureCommsManager);
mTransportManager = new CompanionTransportManager(context);
mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mAssociationStore,
mSystemDataTransferRequestStore, mCompanionMessageProcessor);
mSystemDataTransferRequestStore, mTransportManager);
// Publish "binder" service.
final CompanionDeviceManagerImpl impl = new CompanionDeviceManagerImpl();
@@ -319,18 +315,32 @@ public class CompanionDeviceManagerService extends SystemService {
MINUTES.toMillis(10));
}
@Nullable
@NonNull
AssociationInfo getAssociationWithCallerChecks(
@UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
final AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress(
AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress(
userId, packageName, macAddress);
return sanitizeWithCallerChecks(getContext(), association);
association = sanitizeWithCallerChecks(getContext(), association);
if (association != null) {
return association;
} else {
throw new IllegalArgumentException("Association does not exist "
+ "or the caller does not have permissions to manage it "
+ "(ie. it belongs to a different package or a different user).");
}
}
@Nullable
@NonNull
AssociationInfo getAssociationWithCallerChecks(int associationId) {
final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
return sanitizeWithCallerChecks(getContext(), association);
AssociationInfo association = mAssociationStore.getAssociationById(associationId);
association = sanitizeWithCallerChecks(getContext(), association);
if (association != null) {
return association;
} else {
throw new IllegalArgumentException("Association does not exist "
+ "or the caller does not have permissions to manage it "
+ "(ie. it belongs to a different package or a different user).");
}
}
private void onDeviceAppearedInternal(int associationId) {
@@ -609,12 +619,6 @@ public class CompanionDeviceManagerService extends SystemService {
final AssociationInfo association =
getAssociationWithCallerChecks(userId, packageName, deviceMacAddress);
if (association == null) {
throw new IllegalArgumentException("Association does not exist "
+ "or the caller does not have permissions to manage it "
+ "(ie. it belongs to a different package or a different user).");
}
disassociateInternal(association.getId());
}
@@ -622,15 +626,9 @@ public class CompanionDeviceManagerService extends SystemService {
public void disassociate(int associationId) {
if (DEBUG) Log.i(TAG, "disassociate() associationId=" + associationId);
final AssociationInfo association = getAssociationWithCallerChecks(associationId);
if (association == null) {
throw new IllegalArgumentException("Association with ID " + associationId + " "
+ "does not exist "
+ "or belongs to a different package "
+ "or belongs to a different user");
}
disassociateInternal(associationId);
final AssociationInfo association =
getAssociationWithCallerChecks(associationId);
disassociateInternal(association.getId());
}
@Override
@@ -697,27 +695,6 @@ public class CompanionDeviceManagerService extends SystemService {
registerDevicePresenceListenerActive(callingPackage, deviceAddress, false);
}
@Override
public void dispatchMessage(int messageId, int associationId, @NonNull byte[] message) {
if (DEBUG) {
Log.i(TAG, "dispatchMessage() associationId=" + associationId + "\n"
+ " message(Base64)=" + Base64.encodeToString(message, 0));
}
getContext().enforceCallingOrSelfPermission(DELIVER_COMPANION_MESSAGES,
"dispatchMessage");
AssociationInfo association = getAssociationWithCallerChecks(associationId);
if (association == null) {
throw new IllegalArgumentException("Association with ID " + associationId + " "
+ "does not exist "
+ "or belongs to a different package "
+ "or belongs to a different user");
}
mSecureCommsManager.receiveSecureMessage(messageId, associationId, message);
}
@Override
public PendingIntent buildPermissionTransferUserConsentIntent(String packageName,
int userId, int associationId) {
@@ -735,14 +712,14 @@ public class CompanionDeviceManagerService extends SystemService {
@Override
public void attachSystemDataTransport(String packageName, int userId, int associationId,
ParcelFileDescriptor fd) {
mSystemDataTransferProcessor.attachSystemDataTransport(packageName, userId,
associationId, fd);
getAssociationWithCallerChecks(associationId);
mTransportManager.attachSystemDataTransport(packageName, userId, associationId, fd);
}
@Override
public void detachSystemDataTransport(String packageName, int userId, int associationId) {
mSystemDataTransferProcessor.detachSystemDataTransport(packageName, userId,
associationId);
getAssociationWithCallerChecks(associationId);
mTransportManager.detachSystemDataTransport(packageName, userId, associationId);
}
@Override
@@ -750,13 +727,6 @@ public class CompanionDeviceManagerService extends SystemService {
if (DEBUG) Log.i(TAG, "notifyDevice_Appeared() id=" + associationId);
AssociationInfo association = getAssociationWithCallerChecks(associationId);
if (association == null) {
throw new IllegalArgumentException("Association with ID " + associationId + " "
+ "does not exist "
+ "or belongs to a different package "
+ "or belongs to a different user");
}
if (!association.isSelfManaged()) {
throw new IllegalArgumentException("Association with ID " + associationId
+ " is not self-managed. notifyDeviceAppeared(int) can only be called for"
@@ -777,13 +747,6 @@ public class CompanionDeviceManagerService extends SystemService {
if (DEBUG) Log.i(TAG, "notifyDevice_Disappeared() id=" + associationId);
final AssociationInfo association = getAssociationWithCallerChecks(associationId);
if (association == null) {
throw new IllegalArgumentException("Association with ID " + associationId + " "
+ "does not exist "
+ "or belongs to a different package "
+ "or belongs to a different user");
}
if (!association.isSelfManaged()) {
throw new IllegalArgumentException("Association with ID " + associationId
+ " is not self-managed. notifyDeviceAppeared(int) can only be called for"
@@ -892,7 +855,6 @@ public class CompanionDeviceManagerService extends SystemService {
final CompanionDeviceShellCommand cmd = new CompanionDeviceShellCommand(
CompanionDeviceManagerService.this,
mAssociationStore,
mSecureCommsManager,
mDevicePresenceMonitor);
cmd.exec(this, in, out, err, args, callback, resultReceiver);
}

View File

@@ -99,12 +99,6 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe
post(companionService -> companionService.onDeviceDisappeared(associationInfo));
}
void postOnMessageDispatchedFromSystem(int associationId, int messageId,
@NonNull byte[] message) {
post(companionService ->
companionService.onMessageDispatchedFromSystem(messageId, associationId, message));
}
/**
* Post "unbind" job, which will run *after* all previously posted jobs complete.
*

View File

@@ -16,14 +16,9 @@
package com.android.server.companion;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.companion.AssociationInfo;
import android.os.Binder;
import android.os.ShellCommand;
import android.util.Base64;
import com.android.server.companion.securechannel.CompanionSecureCommunicationsManager;
import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
@@ -35,16 +30,13 @@ class CompanionDeviceShellCommand extends ShellCommand {
private final CompanionDeviceManagerService mService;
private final AssociationStore mAssociationStore;
private final CompanionSecureCommunicationsManager mSecureCommsManager;
private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
CompanionDeviceShellCommand(CompanionDeviceManagerService service,
AssociationStore associationStore,
CompanionSecureCommunicationsManager secureCommsManager,
CompanionDevicePresenceMonitor devicePresenceMonitor) {
mService = service;
mAssociationStore = associationStore;
mSecureCommsManager = secureCommsManager;
mDevicePresenceMonitor = devicePresenceMonitor;
}
@@ -92,32 +84,6 @@ class CompanionDeviceShellCommand extends ShellCommand {
mService.loadAssociationsFromDisk();
break;
case "send-secure-message":
associationId = getNextIntArgRequired();
final byte[] message;
// The message should be either a UTF-8 String or Base64-encoded data.
final boolean isBase64 = "--base64".equals(getNextOption());
if (isBase64) {
final String base64encodedMessage = getNextArgRequired();
message = Base64.decode(base64encodedMessage, 0);
} else {
// We treat the rest of the command as the message, which should contain at
// least one word (hence getNextArg_Required() below), but there may be
// more.
final StringBuilder sb = new StringBuilder(getNextArgRequired());
// Pick up the rest.
for (String word : peekRemainingArgs()) {
sb.append(" ").append(word);
}
// And now convert to byte[]...
message = sb.toString().getBytes(UTF_8);
}
mSecureCommsManager.sendSecureMessage(associationId, /* messageId */ 0,
message);
break;
case "simulate-device-appeared":
associationId = getNextIntArgRequired();
mDevicePresenceMonitor.simulateDeviceAppeared(associationId);
@@ -167,8 +133,6 @@ class CompanionDeviceShellCommand extends ShellCommand {
pw.println(" Create a new Association.");
pw.println(" disassociate USER_ID PACKAGE MAC_ADDRESS");
pw.println(" Remove an existing Association.");
pw.println(" send-secure-message ASSOCIATION_ID [--base64] MESSAGE");
pw.println(" Send a secure message to an associated companion device.");
pw.println(" clear-association-memory-cache");
pw.println(" Clear the in-memory association cache and reload all association ");
pw.println(" information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY.");

View File

@@ -1,50 +0,0 @@
/*
* Copyright (C) 2022 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.companion.datatransfer;
class CompanionMessageInfo {
private final long mId;
private final int mPage;
private final int mTotal;
private final int mType;
private final byte[] mData;
CompanionMessageInfo(long id, int page, int total, int type, byte[] data) {
mId = id;
mPage = page;
mTotal = total;
mType = type;
mData = data;
}
public long getId() {
return mId;
}
public int getPage() {
return mPage;
}
public int getType() {
return mType;
}
public byte[] getData() {
return mData;
}
}

View File

@@ -1,229 +0,0 @@
/*
* Copyright (C) 2022 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.companion.datatransfer;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import com.android.server.companion.proto.CompanionMessage;
import com.android.server.companion.securechannel.CompanionSecureCommunicationsManager;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class builds and reads CompanionMessage. And also paginate and combine messages.
*/
public class CompanionMessageProcessor {
private static final String LOG_TAG = CompanionMessageProcessor.class.getSimpleName();
/** Listener for incoming complete messages. */
interface Listener {
/** When a complete message is received from the companion app. */
void onCompleteMessageReceived(@NonNull CompanionMessageInfo message);
}
// Rough size for each CompanionMessage, each message can exceed 50K for a little, but not
// too much. Hard limit is 100K, WCS data processing limit. Closer to 100K, less stable at
// the WCS data processing layer. Refer to
// https://developers.google.com/android/reference/com/google/android/gms/wearable/MessageClient
// #public-abstract-taskinteger-sendmessage-string-nodeid,-string-path,-byte[]-data
private static final int MESSAGE_SIZE_IN_BYTES = 50000;
private final CompanionSecureCommunicationsManager mSecureCommsManager;
@Nullable
private Listener mListener;
// Association id -> (parent id -> received messages)
private final Map<Integer, Map<Integer, List<CompanionMessageInfo>>> mAssociationsMessagesMap =
new HashMap<>();
// Association id -> next parent id
private final Map<Integer, Integer> mNextParentId = new HashMap<>();
public CompanionMessageProcessor(CompanionSecureCommunicationsManager secureCommsManager) {
mSecureCommsManager = secureCommsManager;
mSecureCommsManager.setListener(this::onDecryptedMessageReceived);
}
public void setListener(@NonNull Listener listener) {
mListener = listener;
}
/**
* Paginate the data into multiple messages with size limit. And dispatch the messages to the
* companion app.
*/
public void paginateAndDispatchMessagesToApp(byte[] data, int messageType,
String packageName, int userId, int associationId) {
Slog.i(LOG_TAG, "Paginating " + data.length + " bytes.");
final int totalMessageCount = (data.length / MESSAGE_SIZE_IN_BYTES)
+ ((data.length % MESSAGE_SIZE_IN_BYTES == 0) ? 0 : 1);
int parentMessageId = findNextParentId(associationId, totalMessageCount);
for (int i = 0; i < totalMessageCount; i++) {
ProtoOutputStream proto = new ProtoOutputStream();
int messageId = parentMessageId + i + 1;
proto.write(CompanionMessage.ID, messageId);
long paginationInfoToken = proto.start(CompanionMessage.PAGINATION_INFO);
proto.write(CompanionMessage.PaginationInfo.PARENT_ID, parentMessageId);
proto.write(CompanionMessage.PaginationInfo.PAGE, i + 1);
proto.write(CompanionMessage.PaginationInfo.TOTAL, totalMessageCount);
proto.end(paginationInfoToken);
proto.write(CompanionMessage.TYPE, messageType);
byte[] currentData = Arrays.copyOfRange(data, i * MESSAGE_SIZE_IN_BYTES,
Math.min((i + 1) * MESSAGE_SIZE_IN_BYTES, data.length));
proto.write(CompanionMessage.DATA, currentData);
byte[] message = proto.getBytes();
Slog.i(LOG_TAG, "Sending [" + message.length + "] bytes to " + packageName);
mSecureCommsManager.sendSecureMessage(associationId, messageId, message);
}
}
/**
* Process the message and store it. If all the messages with the same parent id have been
* received, return the message with combined message data. Otherwise, return null if there's
* still data parts missing.
*/
public CompanionMessageInfo onDecryptedMessageReceived(int messageId, int associationId,
byte[] message) {
Slog.i(LOG_TAG, "Partial message received, size [" + message.length
+ "], reading from protobuf.");
ProtoInputStream proto = new ProtoInputStream(message);
try {
int id = 0;
int parentId = 0;
int page = 0;
int total = 0;
int type = CompanionMessage.UNKNOWN;
byte[] data = null;
// Read proto data
while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (proto.getFieldNumber()) {
case (int) CompanionMessage.ID:
id = proto.readInt(CompanionMessage.ID);
break;
case (int) CompanionMessage.PAGINATION_INFO:
long paginationToken = proto.start(CompanionMessage.PAGINATION_INFO);
while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (proto.getFieldNumber()) {
case (int) CompanionMessage.PaginationInfo.PARENT_ID:
parentId = proto.readInt(
CompanionMessage.PaginationInfo.PARENT_ID);
break;
case (int) CompanionMessage.PaginationInfo.PAGE:
page = proto.readInt(CompanionMessage.PaginationInfo.PAGE);
break;
case (int) CompanionMessage.PaginationInfo.TOTAL:
total = proto.readInt(CompanionMessage.PaginationInfo.TOTAL);
break;
default:
Slog.e(LOG_TAG, "Unexpected field id "
+ proto.getFieldNumber() + " for PaginationInfo.");
break;
}
}
proto.end(paginationToken);
break;
case (int) CompanionMessage.TYPE:
type = proto.readInt(CompanionMessage.TYPE);
break;
case (int) CompanionMessage.DATA:
data = proto.readBytes(CompanionMessage.DATA);
break;
default:
Slog.e(LOG_TAG, "Unexpected field id " + proto.getFieldNumber()
+ " for CompanionMessage.");
break;
}
}
if (id == messageId) {
CompanionMessageInfo messageInfo = new CompanionMessageInfo(id, page, total, type,
data);
// Add the message into mAssociationsMessagesMap
Map<Integer, List<CompanionMessageInfo>> associationMessages =
mAssociationsMessagesMap.getOrDefault(associationId, new HashMap<>());
List<CompanionMessageInfo> childMessages = associationMessages.getOrDefault(
parentId, new ArrayList<>());
childMessages.add(messageInfo);
associationMessages.put(parentId, childMessages);
mAssociationsMessagesMap.put(associationId, associationMessages);
// Check if all the messages with the same parentId are received.
if (childMessages.size() == total) {
Slog.i(LOG_TAG, "All [" + total + "] messages are received for parentId ["
+ parentId + "]. Processing.");
childMessages.sort(Comparator.comparing(CompanionMessageInfo::getPage));
ByteArrayOutputStream stream = new ByteArrayOutputStream();
for (int i = 0; i < childMessages.size(); i++) {
stream.write(childMessages.get(i).getData());
}
mAssociationsMessagesMap.remove(parentId);
mListener.onCompleteMessageReceived(
new CompanionMessageInfo(parentId, 0, total, type,
stream.toByteArray()));
} else {
Slog.i(LOG_TAG, "[" + childMessages.size() + "/" + total
+ "] messages are received for parentId [" + parentId + "]");
}
} else {
Slog.e(LOG_TAG, "Message id mismatch.");
return null;
}
} catch (IOException e) {
Slog.e(LOG_TAG, "Can't read proto from the message.");
return null;
}
return null;
}
/**
* Find the next parent id from [1, Integer.MAX_VALUE].
* The parent and child ids are incremental.
*/
private int findNextParentId(int associationId, int totalMessageCount) {
int nextParentId = mNextParentId.getOrDefault(associationId, 1);
// If the last child message id exceeds the Integer range, start from 1 again.
if (nextParentId > Integer.MAX_VALUE - totalMessageCount - 1) {
nextParentId = 1;
}
mNextParentId.put(associationId, nextParentId + totalMessageCount + 1);
return nextParentId;
}
}

View File

@@ -38,34 +38,27 @@ import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.UserHandle;
import android.permission.PermissionControllerManager;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.server.companion.AssociationStore;
import com.android.server.companion.CompanionDeviceManagerService;
import com.android.server.companion.PermissionsUtils;
import com.android.server.companion.proto.CompanionMessage;
import com.android.server.companion.transport.CompanionTransportManager;
import libcore.io.IoUtils;
import libcore.io.Streams;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* This processor builds user consent intent for a given SystemDataTransferRequest and processes the
* request when the system is ready (a secure channel is established between the handhold and the
* companion device).
* This processor builds user consent intent for a
* {@link SystemDataTransferRequest} and processes the request once a channel is
* established between the local and remote companion device.
*/
public class SystemDataTransferProcessor {
@@ -85,21 +78,19 @@ public class SystemDataTransferProcessor {
private final Context mContext;
private final AssociationStore mAssociationStore;
private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
private final CompanionMessageProcessor mCompanionMessageProcessor;
private final CompanionTransportManager mTransportManager;
private final PermissionControllerManager mPermissionControllerManager;
private final ExecutorService mExecutor;
@GuardedBy("mTransports")
private final SparseArray<Transport> mTransports = new SparseArray<>();
public SystemDataTransferProcessor(CompanionDeviceManagerService service,
AssociationStore associationStore,
SystemDataTransferRequestStore systemDataTransferRequestStore,
CompanionMessageProcessor companionMessageProcessor) {
CompanionTransportManager transportManager) {
mContext = service.getContext();
mAssociationStore = associationStore;
mSystemDataTransferRequestStore = systemDataTransferRequestStore;
mCompanionMessageProcessor = companionMessageProcessor;
mCompanionMessageProcessor.setListener(this::onCompleteMessageReceived);
mTransportManager = transportManager;
mTransportManager.setListener(this::onReceivePermissionRestore);
mPermissionControllerManager = mContext.getSystemService(PermissionControllerManager.class);
mExecutor = Executors.newSingleThreadExecutor();
}
@@ -194,73 +185,42 @@ public class SystemDataTransferProcessor {
return;
}
// TODO: Establish a secure channel
// Start permission sync
final long callingIdentityToken = Binder.clearCallingIdentity();
try {
// TODO: refactor to work with streams of data
mPermissionControllerManager.getRuntimePermissionBackup(UserHandle.of(userId),
mExecutor,
backup -> mCompanionMessageProcessor.paginateAndDispatchMessagesToApp(backup,
CompanionMessage.PERMISSION_SYNC, packageName, userId, associationId));
mExecutor, backup -> {
Future<?> result = mTransportManager
.requestPermissionRestore(associationId, backup);
try {
result.get(15, TimeUnit.SECONDS);
try {
callback.onResult();
} catch (RemoteException ignored) {
}
} catch (Exception e) {
try {
callback.onError(e.getMessage());
} catch (RemoteException ignored) {
}
}
});
} finally {
Binder.restoreCallingIdentity(callingIdentityToken);
}
}
public void attachSystemDataTransport(String packageName, int userId, int associationId,
ParcelFileDescriptor fd) {
synchronized (mTransports) {
// TODO: restore once testing has evolved
// resolveAssociation(packageName, userId, associationId);
if (mTransports.contains(associationId)) {
detachSystemDataTransport(packageName, userId, associationId);
}
final Transport transport = new Transport(fd);
transport.start();
mTransports.put(associationId, transport);
}
}
public void detachSystemDataTransport(String packageName, int userId, int associationId) {
synchronized (mTransports) {
// TODO: restore once testing has evolved
// resolveAssociation(packageName, userId, associationId);
final Transport transport = mTransports.get(associationId);
if (transport != null) {
mTransports.delete(associationId);
transport.stop();
}
}
}
/**
* Process a complete decrypted message reported by the companion app.
*/
public void onCompleteMessageReceived(@NonNull CompanionMessageInfo completeMessage) {
switch (completeMessage.getType()) {
case CompanionMessage.PERMISSION_SYNC:
processPermissionSyncMessage(completeMessage);
break;
default:
Slog.e(LOG_TAG, "Unknown message type [" + completeMessage.getType()
+ "]. Unable to process.");
}
}
private void processPermissionSyncMessage(CompanionMessageInfo messageInfo) {
private void onReceivePermissionRestore(byte[] message) {
Slog.i(LOG_TAG, "Applying permissions.");
// Start applying permissions
UserHandle user = mContext.getUser();
final long callingIdentityToken = Binder.clearCallingIdentity();
try {
// TODO: refactor to work with streams of data
mPermissionControllerManager.stageAndApplyRuntimePermissionsBackup(
messageInfo.getData(), user);
message, user);
} finally {
Slog.i(LOG_TAG, "Permissions applied.");
Binder.restoreCallingIdentity(callingIdentityToken);
}
}
@@ -291,74 +251,4 @@ public class SystemDataTransferProcessor {
Slog.e(LOG_TAG, "Unknown result code:" + resultCode);
}
};
private class Transport {
private final InputStream mRemoteIn;
private final OutputStream mRemoteOut;
private volatile boolean mStopped;
public Transport(ParcelFileDescriptor fd) {
mRemoteIn = new ParcelFileDescriptor.AutoCloseInputStream(fd);
mRemoteOut = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
}
public void start() {
new Thread(() -> {
try {
while (!mStopped) {
processNextCommand();
}
} catch (IOException e) {
if (!mStopped) {
Slog.w(LOG_TAG, "Trouble during transport", e);
stop();
}
}
}).start();
}
public void stop() {
mStopped = true;
IoUtils.closeQuietly(mRemoteIn);
IoUtils.closeQuietly(mRemoteOut);
}
private void processNextCommand() throws IOException {
Slog.d(LOG_TAG, "Waiting for next command...");
// Read message header
final byte[] headerBytes = new byte[8];
Streams.readFully(mRemoteIn, headerBytes);
final ByteBuffer header = ByteBuffer.wrap(headerBytes);
final int command = header.getInt();
final int length = header.getInt();
Slog.d(LOG_TAG, "Received command 0x" + Integer.toHexString(command)
+ " length " + length);
switch (command) {
case 0x50490000: // PI(NG) version 0
// Repeat back the given payload, within reason
final int target = Math.min(length, 1_000_000);
final byte[] payload = new byte[target];
Streams.readFully(mRemoteIn, payload);
Streams.skipByReading(mRemoteIn, length - target);
// Respond with PO(NG) version 0
header.rewind();
header.putInt(0x504F0000);
header.putInt(target);
mRemoteOut.write(header.array());
mRemoteOut.write(payload);
break;
default:
// Emit local warning, and skip message to
// handle next one
Slog.w(LOG_TAG, "Unknown command 0x" + Integer.toHexString(command));
mRemoteIn.skip(length);
}
}
}
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright (C) 2022 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.
*/
syntax = "proto3";
option java_multiple_files = true;
package com.android.server.companion.proto;
/* Represents a message between companion devices */
message CompanionMessage {
int32 id = 1;
PaginationInfo paginationInfo = 2;
Type type = 3;
// message body data
bytes data = 4;
/* Message pagination info */
message PaginationInfo {
// id of the parent message, which should be the same for all the paginated messages
int32 parentId = 1;
// page number of the current message in [1, total]
int32 page = 2;
// total number of messages
int32 total = 3;
}
/* Message type */
enum Type {
// default value for proto3
UNKNOWN = 0;
// handshake message to establish secure channel
SECURE_CHANNEL_HANDSHAKE = 1;
// permission sync
PERMISSION_SYNC = 2;
}
}

View File

@@ -1,121 +0,0 @@
/*
* Copyright (C) 2022 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.companion.securechannel;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.companion.AssociationInfo;
import android.util.Base64;
import android.util.Log;
import android.util.Slog;
import com.android.server.companion.AssociationStore;
import com.android.server.companion.CompanionApplicationController;
/** Secure Comms Manager */
@SuppressLint("LongLogTag")
public class CompanionSecureCommunicationsManager {
static final String TAG = "CompanionDevice_SecureComms";
static final boolean DEBUG = false;
/** Listener for incoming decrypted messages. */
public interface Listener {
/** When an incoming message is decrypted. */
void onDecryptedMessageReceived(int messageId, int associationId, byte[] message);
}
private final AssociationStore mAssociationStore;
private final CompanionApplicationController mCompanionAppController;
@Nullable
private Listener mListener;
/** Constructor */
public CompanionSecureCommunicationsManager(AssociationStore associationStore,
CompanionApplicationController companionApplicationController) {
mAssociationStore = associationStore;
mCompanionAppController = companionApplicationController;
}
public void setListener(@NonNull Listener listener) {
mListener = listener;
}
/**
* Send a data to the associated companion device via secure channel (establishing one if
* needed).
* @param associationId associationId of the "recipient" companion device.
* @param messageId id of the message
* @param message data to be sent securely.
*/
public void sendSecureMessage(int associationId, int messageId, @NonNull byte[] message) {
if (DEBUG) {
Log.d(TAG, "sendSecureMessage() associationId=" + associationId + "\n"
+ " message (Base64)=\"" + Base64.encodeToString(message, 0) + "\"");
}
final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
if (association == null) {
throw new IllegalArgumentException(
"Association with ID " + associationId + " does not exist");
}
if (DEBUG) Log.d(TAG, " association=" + association);
final int userId = association.getUserId();
final String packageName = association.getPackageName();
// Bind to the app if it hasn't been bound.
if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
Slog.d(TAG, "userId [" + userId + "] packageName [" + packageName
+ "] is not bound. Binding it now to send a secure message.");
mCompanionAppController.bindCompanionApplication(userId, packageName,
association.isSelfManaged());
// TODO(b/202926196): implement: encrypt and pass on the companion application for
// transporting
mCompanionAppController.dispatchMessage(userId, packageName, associationId, messageId,
message);
Slog.d(TAG, "Unbinding userId [" + userId + "] packageName [" + packageName
+ "]");
mCompanionAppController.unbindCompanionApplication(userId, packageName);
}
// TODO(b/202926196): implement: encrypt and pass on the companion application for
// transporting
mCompanionAppController.dispatchMessage(userId, packageName, associationId, messageId,
message);
}
/**
* Decrypt and dispatch message received from an associated companion device.
* @param associationId associationId of the "sender" companion device.
* @param encryptedMessage data.
*/
public void receiveSecureMessage(int messageId, int associationId,
@NonNull byte[] encryptedMessage) {
if (DEBUG) {
Log.d(TAG, "sendSecureMessage() associationId=" + associationId + "\n"
+ " message (Base64)=\"" + Base64.encodeToString(encryptedMessage, 0) + "\"");
}
// TODO(b/202926196): implement: decrypt and dispatch
mListener.onDecryptedMessageReceived(messageId, associationId, encryptedMessage);
}
}

View File

@@ -1,4 +0,0 @@
set noparent
sergeynv@google.com
ewol@google.com

View File

@@ -0,0 +1,318 @@
/*
* Copyright (C) 2022 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.companion.transport;
import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManagerInternal;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Binder;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import libcore.io.IoUtils;
import libcore.io.Streams;
import libcore.util.EmptyArray;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
@SuppressLint("LongLogTag")
public class CompanionTransportManager {
private static final String TAG = "CompanionTransportManager";
// TODO: flip to false
private static final boolean DEBUG = true;
private static final int HEADER_LENGTH = 12;
// TODO: refactor message processing to use streams to remove this limit
private static final int MAX_PAYLOAD_LENGTH = 1_000_000;
private static final int MESSAGE_REQUEST_PING = 0x63807378; // ?PIN
private static final int MESSAGE_REQUEST_PERMISSION_RESTORE = 0x63826983; // ?RES
private static final int MESSAGE_RESPONSE_SUCCESS = 0x33838567; // !SUC
private static final int MESSAGE_RESPONSE_FAILURE = 0x33706573; // !FAI
private static boolean isRequest(int message) {
return (message & 0xFF000000) == 0x63000000;
}
private static boolean isResponse(int message) {
return (message & 0xFF000000) == 0x33000000;
}
public interface Listener {
void onRequestPermissionRestore(byte[] data);
}
private final Context mContext;
@GuardedBy("mTransports")
private final SparseArray<Transport> mTransports = new SparseArray<>();
@Nullable
private Listener mListener;
public CompanionTransportManager(Context context) {
mContext = context;
}
public void setListener(@NonNull Listener listener) {
mListener = listener;
}
/**
* For the moment, we only offer transporting of system data to built-in
* companion apps; future work will improve the security model to support
* third-party companion apps.
*/
private void enforceCallerCanTransportSystemData(String packageName, int userId) {
mContext.enforceCallingOrSelfPermission(DELIVER_COMPANION_MESSAGES, TAG);
try {
final ApplicationInfo info = mContext.getPackageManager().getApplicationInfoAsUser(
packageName, 0, userId);
final int instrumentationUid = LocalServices.getService(ActivityManagerInternal.class)
.getInstrumentationSourceUid(Binder.getCallingUid());
if (!Build.isDebuggable() && !info.isSystemApp()
&& instrumentationUid == android.os.Process.INVALID_UID) {
throw new SecurityException("Transporting of system data currently only available "
+ "to built-in companion apps or tests");
}
} catch (NameNotFoundException e) {
throw new IllegalArgumentException(e);
}
}
public void attachSystemDataTransport(String packageName, int userId, int associationId,
ParcelFileDescriptor fd) {
enforceCallerCanTransportSystemData(packageName, userId);
synchronized (mTransports) {
if (mTransports.contains(associationId)) {
detachSystemDataTransport(packageName, userId, associationId);
}
final Transport transport = new Transport(associationId, fd);
transport.start();
mTransports.put(associationId, transport);
}
}
public void detachSystemDataTransport(String packageName, int userId, int associationId) {
enforceCallerCanTransportSystemData(packageName, userId);
synchronized (mTransports) {
final Transport transport = mTransports.get(associationId);
if (transport != null) {
mTransports.delete(associationId);
transport.stop();
}
}
}
public Future<?> requestPermissionRestore(int associationId, byte[] data) {
synchronized (mTransports) {
final Transport transport = mTransports.get(associationId);
if (transport != null) {
return transport.requestForResponse(MESSAGE_REQUEST_PERMISSION_RESTORE, data);
} else {
return CompletableFuture.failedFuture(new IOException("Missing transport"));
}
}
}
private class Transport {
private final int mAssociationId;
private final InputStream mRemoteIn;
private final OutputStream mRemoteOut;
private final AtomicInteger mNextSequence = new AtomicInteger();
@GuardedBy("mPendingRequests")
private final SparseArray<CompletableFuture<byte[]>> mPendingRequests = new SparseArray<>();
private volatile boolean mStopped;
public Transport(int associationId, ParcelFileDescriptor fd) {
mAssociationId = associationId;
mRemoteIn = new ParcelFileDescriptor.AutoCloseInputStream(fd);
mRemoteOut = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
}
public void start() {
new Thread(() -> {
try {
while (!mStopped) {
receiveMessage();
}
} catch (IOException e) {
if (!mStopped) {
Slog.w(TAG, "Trouble during transport", e);
stop();
}
}
}).start();
}
public void stop() {
mStopped = true;
IoUtils.closeQuietly(mRemoteIn);
IoUtils.closeQuietly(mRemoteOut);
}
public Future<byte[]> requestForResponse(int message, byte[] data) {
final int sequence = mNextSequence.incrementAndGet();
final CompletableFuture<byte[]> pending = new CompletableFuture<>();
synchronized (mPendingRequests) {
mPendingRequests.put(sequence, pending);
}
try {
sendMessage(message, sequence, data);
} catch (IOException e) {
pending.completeExceptionally(e);
}
return pending;
}
private void sendMessage(int message, int sequence, @NonNull byte[] data)
throws IOException {
if (DEBUG) {
Slog.d(TAG, "Sending message 0x" + Integer.toHexString(message)
+ " sequence " + sequence + " length " + data.length
+ " to association " + mAssociationId);
}
synchronized (mRemoteOut) {
final ByteBuffer header = ByteBuffer.allocate(HEADER_LENGTH)
.putInt(message)
.putInt(sequence)
.putInt(data.length);
mRemoteOut.write(header.array());
mRemoteOut.write(data);
mRemoteOut.flush();
}
}
private void receiveMessage() throws IOException {
if (DEBUG) {
Slog.d(TAG, "Waiting for next message...");
}
final byte[] headerBytes = new byte[HEADER_LENGTH];
Streams.readFully(mRemoteIn, headerBytes);
final ByteBuffer header = ByteBuffer.wrap(headerBytes);
final int message = header.getInt();
final int sequence = header.getInt();
final int length = header.getInt();
if (DEBUG) {
Slog.d(TAG, "Received message 0x" + Integer.toHexString(message)
+ " sequence " + sequence + " length " + length
+ " from association " + mAssociationId);
}
if (length > MAX_PAYLOAD_LENGTH) {
Slog.w(TAG, "Ignoring message 0x" + Integer.toHexString(message)
+ " sequence " + sequence + " length " + length
+ " from association " + mAssociationId + " beyond maximum length");
Streams.skipByReading(mRemoteIn, length);
return;
}
final byte[] data = new byte[length];
Streams.readFully(mRemoteIn, data);
if (isRequest(message)) {
processRequest(message, sequence, data);
} else if (isResponse(message)) {
processResponse(message, sequence, data);
} else {
Slog.w(TAG, "Unknown message " + Integer.toHexString(message));
}
}
private void processRequest(int message, int sequence, byte[] data)
throws IOException {
switch (message) {
case MESSAGE_REQUEST_PING: {
sendMessage(MESSAGE_RESPONSE_SUCCESS, sequence, data);
break;
}
case MESSAGE_REQUEST_PERMISSION_RESTORE: {
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
&& !Build.isDebuggable()) {
Slog.w(TAG, "Restoring permissions only supported on watches");
sendMessage(MESSAGE_RESPONSE_FAILURE, sequence, EmptyArray.BYTE);
break;
}
try {
mListener.onRequestPermissionRestore(data);
sendMessage(MESSAGE_RESPONSE_SUCCESS, sequence, EmptyArray.BYTE);
} catch (Exception e) {
Slog.w(TAG, "Failed to restore permissions");
sendMessage(MESSAGE_RESPONSE_FAILURE, sequence, EmptyArray.BYTE);
}
break;
}
default: {
Slog.w(TAG, "Unknown request " + Integer.toHexString(message));
sendMessage(MESSAGE_RESPONSE_FAILURE, sequence, EmptyArray.BYTE);
break;
}
}
}
private void processResponse(int message, int sequence, byte[] data) {
final CompletableFuture<byte[]> future;
synchronized (mPendingRequests) {
future = mPendingRequests.removeReturnOld(sequence);
}
if (future == null) {
Slog.w(TAG, "Ignoring unknown sequence " + sequence);
return;
}
switch (message) {
case MESSAGE_RESPONSE_SUCCESS: {
future.complete(data);
}
case MESSAGE_RESPONSE_FAILURE: {
future.completeExceptionally(new RuntimeException("Remote failure"));
}
default: {
Slog.w(TAG, "Ignoring unknown response " + Integer.toHexString(message));
}
}
}
}
}