Merge "Progress on permissions sync transport migration."
This commit is contained in:
@@ -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 {
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
set noparent
|
||||
|
||||
sergeynv@google.com
|
||||
ewol@google.com
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user