• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.companion;
18 
19 import static android.companion.CompanionDeviceManager.MESSAGE_ONEWAY_PING;
20 import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_PING;
21 
22 import android.content.Context;
23 import android.os.SystemClock;
24 import android.test.InstrumentationTestCase;
25 import android.util.Log;
26 
27 import com.android.internal.util.HexDump;
28 
29 import libcore.util.EmptyArray;
30 
31 import java.io.ByteArrayInputStream;
32 import java.io.ByteArrayOutputStream;
33 import java.io.FilterInputStream;
34 import java.io.FilterOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.OutputStream;
38 import java.nio.ByteBuffer;
39 import java.nio.charset.StandardCharsets;
40 import java.util.List;
41 import java.util.Random;
42 import java.util.concurrent.CountDownLatch;
43 import java.util.concurrent.TimeUnit;
44 
45 /**
46  * Tests that CDM can intake incoming messages in the system data transport and output results.
47  *
48  * Build/Install/Run: atest CompanionTests:SystemDataTransportTest
49  */
50 public class SystemDataTransportTest extends InstrumentationTestCase {
51     private static final String TAG = "SystemDataTransportTest";
52 
53     private static final int MESSAGE_INVALID = 0xF00DCAFE;
54     private static final int MESSAGE_ONEWAY_INVALID = 0x43434343; // ++++
55     private static final int MESSAGE_RESPONSE_INVALID = 0x33333333; // !!!!
56     private static final int MESSAGE_REQUEST_INVALID = 0x63636363; // ????
57 
58     private static final int MESSAGE_RESPONSE_SUCCESS = 0x33838567; // !SUC
59     private static final int MESSAGE_RESPONSE_FAILURE = 0x33706573; // !FAI
60 
61     private Context mContext;
62     private CompanionDeviceManager mCdm;
63     private int mAssociationId;
64 
65     @Override
setUp()66     protected void setUp() throws Exception {
67         super.setUp();
68 
69         mContext = getInstrumentation().getTargetContext();
70         mCdm = mContext.getSystemService(CompanionDeviceManager.class);
71         mAssociationId = createAssociation();
72         mCdm.enableSecureTransport(false);
73     }
74 
75     @Override
tearDown()76     protected void tearDown() throws Exception {
77         super.tearDown();
78 
79         mCdm.disassociate(mAssociationId);
80         mCdm.enableSecureTransport(true);
81     }
82 
testPingHandRolled()83     public void testPingHandRolled() {
84         // NOTE: These packets are explicitly hand-rolled to verify wire format;
85         // the remainder of the tests are fine using generated packets
86 
87         // MESSAGE_REQUEST_PING with payload "HELLO WORLD!"
88         final byte[] input = new byte[] {
89                 0x63, (byte) 0x80, 0x73, 0x78, // message: MESSAGE_REQUEST_PING
90                 0x00, 0x00, 0x00, 0x2A, // sequence: 42
91                 0x00, 0x00, 0x00, 0x0C, // length: 12
92                 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21,
93         };
94         // MESSAGE_RESPONSE_SUCCESS with payload "HELLO WORLD!"
95         final byte[] expected = new byte[] {
96                 0x33, (byte) 0x83, (byte) 0x85, 0x67, // message: MESSAGE_RESPONSE_SUCCESS
97                 0x00, 0x00, 0x00, 0x2A, // sequence: 42
98                 0x00, 0x00, 0x00, 0x0C, // length: 12
99                 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21,
100         };
101         assertTransportBehavior(input, expected);
102     }
103 
testPingTrickle()104     public void testPingTrickle() {
105         final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, TAG);
106         final byte[] expected = generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, TAG);
107 
108         final ByteArrayInputStream in = new ByteArrayInputStream(input);
109         final ByteArrayOutputStream out = new ByteArrayOutputStream();
110         mCdm.attachSystemDataTransport(mAssociationId, new TrickleInputStream(in), out);
111 
112         final byte[] actual = waitForByteArray(out, expected.length);
113         assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
114     }
115 
testPingDelay()116     public void testPingDelay() {
117         final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, TAG);
118         final byte[] expected = generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, TAG);
119 
120         final ByteArrayInputStream in = new ByteArrayInputStream(input);
121         final ByteArrayOutputStream out = new ByteArrayOutputStream();
122         mCdm.attachSystemDataTransport(mAssociationId, new DelayingInputStream(in, 1000),
123                 new DelayingOutputStream(out, 1000));
124 
125         final byte[] actual = waitForByteArray(out, expected.length);
126         assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
127     }
128 
testPingGiant()129     public void testPingGiant() {
130         final byte[] blob = new byte[500_000];
131         new Random().nextBytes(blob);
132 
133         final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, blob);
134     }
135 
testMultiplePingPing()136     public void testMultiplePingPing() {
137         final byte[] input = concat(
138                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, "red"),
139                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
140         final byte[] expected = concat(
141                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, "red"),
142                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green"));
143         assertTransportBehavior(input, expected);
144     }
145 
testMultipleInvalidPing()146     public void testMultipleInvalidPing() {
147         final byte[] input = concat(
148                 generatePacket(MESSAGE_INVALID, /* sequence */ 1, "red"),
149                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
150         final byte[] expected =
151                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green");
152         assertTransportBehavior(input, expected);
153     }
154 
testMultipleInvalidRequestPing()155     public void testMultipleInvalidRequestPing() {
156         final byte[] input = concat(
157                 generatePacket(MESSAGE_REQUEST_INVALID, /* sequence */ 1, "red"),
158                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
159         final byte[] expected = concat(
160                 generatePacket(MESSAGE_RESPONSE_FAILURE, /* sequence */ 1),
161                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green"));
162         assertTransportBehavior(input, expected);
163     }
164 
testMultipleInvalidResponsePing()165     public void testMultipleInvalidResponsePing() {
166         final byte[] input = concat(
167                 generatePacket(MESSAGE_RESPONSE_INVALID, /* sequence */ 1, "red"),
168                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
169         final byte[] expected =
170                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green");
171         assertTransportBehavior(input, expected);
172     }
173 
testDoubleAttach()174     public void testDoubleAttach() {
175         // Connect an empty connection that is stalled out
176         final InputStream in = new EmptyInputStream();
177         final OutputStream out = new ByteArrayOutputStream();
178         mCdm.attachSystemDataTransport(mAssociationId, in, out);
179         SystemClock.sleep(1000);
180 
181         // Attach a second transport that has some packets; it should disconnect
182         // the first transport and start replying on the second one
183         testPingHandRolled();
184     }
185 
testInvalidOnewayMessages()186     public void testInvalidOnewayMessages() throws InterruptedException {
187         // Add a callback
188         final CountDownLatch received = new CountDownLatch(1);
189         mCdm.addOnMessageReceivedListener(Runnable::run, MESSAGE_ONEWAY_INVALID,
190                 (id, data) -> received.countDown());
191 
192         final byte[] input = generatePacket(MESSAGE_ONEWAY_INVALID, /* sequence */ 1);
193         final ByteArrayInputStream in = new ByteArrayInputStream(input);
194         final ByteArrayOutputStream out = new ByteArrayOutputStream();
195         mCdm.attachSystemDataTransport(mAssociationId, in, out);
196 
197         // Assert that a one-way message was ignored (does not trigger a callback)
198         assertFalse(received.await(5, TimeUnit.SECONDS));
199 
200         // There should not be a response to one-way messages
201         assertEquals(0, out.toByteArray().length);
202     }
203 
204 
testOnewayMessages()205     public void testOnewayMessages() throws InterruptedException {
206         // Add a callback
207         final CountDownLatch received = new CountDownLatch(1);
208         mCdm.addOnMessageReceivedListener(Runnable::run, MESSAGE_ONEWAY_PING,
209                 (id, data) -> received.countDown());
210 
211         final byte[] input = generatePacket(MESSAGE_ONEWAY_PING, /* sequence */ 1);
212         final ByteArrayInputStream in = new ByteArrayInputStream(input);
213         final ByteArrayOutputStream out = new ByteArrayOutputStream();
214         mCdm.attachSystemDataTransport(mAssociationId, in, out);
215 
216         // Assert that a one-way message was received
217         assertTrue(received.await(1, TimeUnit.SECONDS));
218 
219         // There should not be a response to one-way messages
220         assertEquals(0, out.toByteArray().length);
221     }
222 
testDisassociationCleanup()223     public void testDisassociationCleanup() throws InterruptedException {
224         // Create a new association
225         final int associationId = createAssociation();
226 
227         // Subscribe to transport updates for new association
228         final CountDownLatch attached = new CountDownLatch(1);
229         final CountDownLatch detached = new CountDownLatch(1);
230         mCdm.addOnTransportsChangedListener(Runnable::run, associations -> {
231             if (associations.stream()
232                     .anyMatch(association -> associationId == association.getId())) {
233                 attached.countDown();
234             } else if (attached.getCount() == 0) {
235                 detached.countDown();
236             }
237         });
238 
239         final ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
240         final ByteArrayOutputStream out = new ByteArrayOutputStream();
241         mCdm.attachSystemDataTransport(associationId, in, out);
242 
243         // Assert that the transport is attached
244         assertTrue(attached.await(1, TimeUnit.SECONDS));
245 
246         // When CDM disassociates, any transport attached to that associated device should detach
247         mCdm.disassociate(associationId);
248 
249         // Assert that the transport is detached
250         assertTrue(detached.await(1, TimeUnit.SECONDS));
251     }
252 
concat(byte[]... blobs)253     public static byte[] concat(byte[]... blobs) {
254         int length = 0;
255         for (byte[] blob : blobs) {
256             length += blob.length;
257         }
258         final ByteBuffer buf = ByteBuffer.allocate(length);
259         for (byte[] blob : blobs) {
260             buf.put(blob);
261         }
262         return buf.array();
263     }
264 
generatePacket(int message, int sequence)265     public static byte[] generatePacket(int message, int sequence) {
266         return generatePacket(message, sequence, EmptyArray.BYTE);
267     }
268 
generatePacket(int message, int sequence, String data)269     public static byte[] generatePacket(int message, int sequence, String data) {
270         return generatePacket(message, sequence, data.getBytes(StandardCharsets.UTF_8));
271     }
272 
generatePacket(int message, int sequence, byte[] data)273     public static byte[] generatePacket(int message, int sequence, byte[] data) {
274         return ByteBuffer.allocate(data.length + 12)
275                 .putInt(message)
276                 .putInt(sequence)
277                 .putInt(data.length)
278                 .put(data)
279                 .array();
280     }
281 
createAssociation()282     private int createAssociation() {
283         List<AssociationInfo> before = mCdm.getMyAssociations();
284         getInstrumentation().getUiAutomation().executeShellCommand("cmd companiondevice associate "
285                 + mContext.getUserId() + " " + mContext.getPackageName() + " AA:BB:CC:DD:EE:FF");
286         List<AssociationInfo> infos;
287         for (int i = 0; i < 100; i++) {
288             infos = mCdm.getMyAssociations();
289             if (infos.size() != before.size()) {
290                 infos.removeAll(before);
291                 return infos.get(0).getId();
292             } else {
293                 SystemClock.sleep(100);
294             }
295         }
296         throw new AssertionError();
297     }
298 
assertTransportBehavior(byte[] input, byte[] expected)299     private void assertTransportBehavior(byte[] input, byte[] expected) {
300         final ByteArrayInputStream in = new ByteArrayInputStream(input);
301         final ByteArrayOutputStream out = new ByteArrayOutputStream();
302         mCdm.attachSystemDataTransport(mAssociationId, in, out);
303 
304         final byte[] actual = waitForByteArray(out, expected.length);
305         assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
306     }
307 
waitForByteArray(ByteArrayOutputStream out, int size)308     private static byte[] waitForByteArray(ByteArrayOutputStream out, int size) {
309         int i = 0;
310         while (out.size() < size) {
311             SystemClock.sleep(100);
312             if (i++ % 10 == 0) {
313                 Log.w(TAG, "Waiting for data...");
314             }
315             if (i > 100) {
316                 fail();
317             }
318         }
319         return out.toByteArray();
320     }
321 
322     private static class EmptyInputStream extends InputStream {
323         @Override
read()324         public int read() throws IOException {
325             throw new UnsupportedOperationException();
326         }
327 
328         @Override
read(byte[] b, int off, int len)329         public int read(byte[] b, int off, int len) throws IOException {
330             // Instead of hanging indefinitely, wait a bit and claim that
331             // nothing was read, without hitting EOF
332             SystemClock.sleep(100);
333             return 0;
334         }
335     }
336 
337     private static class DelayingInputStream extends FilterInputStream {
338         private final long mDelay;
339 
DelayingInputStream(InputStream in, long delay)340         DelayingInputStream(InputStream in, long delay) {
341             super(in);
342             mDelay = delay;
343         }
344 
345         @Override
read(byte[] b, int off, int len)346         public int read(byte[] b, int off, int len) throws IOException {
347             SystemClock.sleep(mDelay);
348             return super.read(b, off, len);
349         }
350     }
351 
352     private static class DelayingOutputStream extends FilterOutputStream {
353         private final long mDelay;
354 
DelayingOutputStream(OutputStream out, long delay)355         DelayingOutputStream(OutputStream out, long delay) {
356             super(out);
357             mDelay = delay;
358         }
359 
360         @Override
write(byte[] b, int off, int len)361         public void write(byte[] b, int off, int len) throws IOException {
362             SystemClock.sleep(mDelay);
363             super.write(b, off, len);
364         }
365     }
366 
367     private static class TrickleInputStream extends FilterInputStream {
TrickleInputStream(InputStream in)368         TrickleInputStream(InputStream in) {
369             super(in);
370         }
371 
372         @Override
read(byte[] b, int off, int len)373         public int read(byte[] b, int off, int len) throws IOException {
374             return super.read(b, off, 1);
375         }
376     }
377 }
378