From 9ac551161b9d6df2f248aa3b016d6c2b665e9da2 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Mon, 27 Jun 2022 13:23:36 -0600 Subject: [PATCH] Progress on permissions sync transport migration. An earlier change introduced a socketpair() based CDM transport, which is robust "streaming" interface between two devices. This change updates the existing permissions sync logic to use this new transport for its payloads. It also begins returning success or failure messages so the requesting side knows when the permissions have been applied. We also rename the package and class hosting the transport logic, and migrate our socketpair() based transport logic there. Since the initial version of this protocol won't be using protobufs, and doesn't need to implement manual pagination, this change also removes that logic to reduce the size of our backports. Finally, since this transport isn't ready to be used by third-party apps, we add security checks to only allow built-in apps. Bug: 237030169 Test: atest CompanionTests Change-Id: Ia0dab24d44f1973d5af6ad8a2abb198408a681df --- core/api/current.txt | 7 - .../companion/CompanionDeviceManager.java | 23 +- .../companion/CompanionDeviceService.java | 31 +- .../android/companion/CompanionException.java | 5 +- .../companion/ICompanionDeviceManager.aidl | 2 - .../companion/ICompanionDeviceService.aidl | 1 - .../companion/SystemDataTransportTest.java | 180 ++++++---- services/companion/Android.bp | 4 - .../CompanionApplicationController.java | 23 -- .../CompanionDeviceManagerService.java | 100 ++---- .../CompanionDeviceServiceConnector.java | 6 - .../CompanionDeviceShellCommand.java | 36 -- .../datatransfer/CompanionMessageInfo.java | 50 --- .../CompanionMessageProcessor.java | 229 ------------- .../SystemDataTransferProcessor.java | 172 ++-------- .../companion/proto/companion_message.proto | 57 ---- .../CompanionSecureCommunicationsManager.java | 121 ------- .../server/companion/securechannel/OWNERS | 4 - .../transport/CompanionTransportManager.java | 318 ++++++++++++++++++ 19 files changed, 527 insertions(+), 842 deletions(-) delete mode 100644 services/companion/java/com/android/server/companion/datatransfer/CompanionMessageInfo.java delete mode 100644 services/companion/java/com/android/server/companion/datatransfer/CompanionMessageProcessor.java delete mode 100644 services/companion/java/com/android/server/companion/proto/companion_message.proto delete mode 100644 services/companion/java/com/android/server/companion/securechannel/CompanionSecureCommunicationsManager.java delete mode 100644 services/companion/java/com/android/server/companion/securechannel/OWNERS create mode 100644 services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java diff --git a/core/api/current.txt b/core/api/current.txt index 10957ff8bb58b..15aa18871448d 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)); + } + } + } + } +}