diff --git a/core/api/current.txt b/core/api/current.txt index 1de8bc2cbb9e8..38f60a10852aa 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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) 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 extends android.os.Parcelable { } diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 1b51faf3d4299..633b734d4adc6 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -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 { * *

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.

* * @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); } diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java index 83d2713ea1146..a79983a653f08 100644 --- a/core/java/android/companion/CompanionDeviceService.java +++ b/core/java/android/companion/CompanionDeviceService.java @@ -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)); - } } } diff --git a/core/java/android/companion/CompanionException.java b/core/java/android/companion/CompanionException.java index fb78e8df446e4..9a92401b8d673 100644 --- a/core/java/android/companion/CompanionException.java +++ b/core/java/android/companion/CompanionException.java @@ -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 */ diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index 42f908396799d..6e6e187656399 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -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); diff --git a/core/java/android/companion/ICompanionDeviceService.aidl b/core/java/android/companion/ICompanionDeviceService.aidl index 3c90b86ca8c03..fa6850882132e 100644 --- a/core/java/android/companion/ICompanionDeviceService.aidl +++ b/core/java/android/companion/ICompanionDeviceService.aidl @@ -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); } diff --git a/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java index be04b6c91a8aa..6bd498c583db9 100644 --- a/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java +++ b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java @@ -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 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) { diff --git a/services/companion/Android.bp b/services/companion/Android.bp index d3ef6dc472568..cdeb2dcf87e90 100644 --- a/services/companion/Android.bp +++ b/services/companion/Android.bp @@ -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", diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java index 2ab1aa80176ad..2acb65eafac0b 100644 --- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java +++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java @@ -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"); diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index c3545619bcb93..fa043f8e02afb 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -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); } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java index 7360f0846ed3d..a288f7b2993c7 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java @@ -99,12 +99,6 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl 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. * diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index 0b7bc03eebba8..322ad46e697a9 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -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."); diff --git a/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageInfo.java b/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageInfo.java deleted file mode 100644 index 91ad93c982400..0000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageInfo.java +++ /dev/null @@ -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; - } -} diff --git a/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageProcessor.java deleted file mode 100644 index 98a00aa6fadc1..0000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageProcessor.java +++ /dev/null @@ -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>> mAssociationsMessagesMap = - new HashMap<>(); - // Association id -> next parent id - private final Map 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> associationMessages = - mAssociationsMessagesMap.getOrDefault(associationId, new HashMap<>()); - List 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; - } -} diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index 7eede552ac712..f0a0492e3806f 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -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 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); - } - } - } } diff --git a/services/companion/java/com/android/server/companion/proto/companion_message.proto b/services/companion/java/com/android/server/companion/proto/companion_message.proto deleted file mode 100644 index 893a0dbfc8524..0000000000000 --- a/services/companion/java/com/android/server/companion/proto/companion_message.proto +++ /dev/null @@ -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; - } -} diff --git a/services/companion/java/com/android/server/companion/securechannel/CompanionSecureCommunicationsManager.java b/services/companion/java/com/android/server/companion/securechannel/CompanionSecureCommunicationsManager.java deleted file mode 100644 index 16a74ecd2068f..0000000000000 --- a/services/companion/java/com/android/server/companion/securechannel/CompanionSecureCommunicationsManager.java +++ /dev/null @@ -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); - } -} diff --git a/services/companion/java/com/android/server/companion/securechannel/OWNERS b/services/companion/java/com/android/server/companion/securechannel/OWNERS deleted file mode 100644 index ecb97f4021368..0000000000000 --- a/services/companion/java/com/android/server/companion/securechannel/OWNERS +++ /dev/null @@ -1,4 +0,0 @@ -set noparent - -sergeynv@google.com -ewol@google.com diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java new file mode 100644 index 0000000000000..f01bdf2103bd1 --- /dev/null +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -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 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> 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 requestForResponse(int message, byte[] data) { + final int sequence = mNextSequence.incrementAndGet(); + final CompletableFuture 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 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)); + } + } + } + } +}