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 android.content.Context;
20 import android.os.SystemClock;
21 import android.test.InstrumentationTestCase;
22 import android.util.Log;
23 
24 import com.android.internal.util.HexDump;
25 
26 import libcore.util.EmptyArray;
27 
28 import java.io.ByteArrayInputStream;
29 import java.io.ByteArrayOutputStream;
30 import java.io.FilterInputStream;
31 import java.io.FilterOutputStream;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.OutputStream;
35 import java.nio.ByteBuffer;
36 import java.nio.charset.StandardCharsets;
37 import java.util.List;
38 import java.util.Random;
39 
40 public class SystemDataTransportTest extends InstrumentationTestCase {
41     private static final String TAG = "SystemDataTransportTest";
42 
43     private static final int MESSAGE_INVALID = 0xF00DCAFE;
44 
45     private static final int MESSAGE_REQUEST_INVALID = 0x63636363; // ????
46     private static final int MESSAGE_REQUEST_PING = 0x63807378; // ?PIN
47 
48     private static final int MESSAGE_RESPONSE_INVALID = 0x33333333; // !!!!
49     private static final int MESSAGE_RESPONSE_SUCCESS = 0x33838567; // !SUC
50     private static final int MESSAGE_RESPONSE_FAILURE = 0x33706573; // !FAI
51 
52     private Context mContext;
53     private CompanionDeviceManager mCdm;
54     private int mAssociationId;
55 
56     @Override
setUp()57     protected void setUp() throws Exception {
58         super.setUp();
59 
60         mContext = getInstrumentation().getTargetContext();
61         mCdm = mContext.getSystemService(CompanionDeviceManager.class);
62         mAssociationId = createAssociation();
63         mCdm.enableSecureTransport(false);
64     }
65 
66     @Override
tearDown()67     protected void tearDown() throws Exception {
68         super.tearDown();
69 
70         mCdm.disassociate(mAssociationId);
71         mCdm.enableSecureTransport(true);
72     }
73 
testPingHandRolled()74     public void testPingHandRolled() {
75         // NOTE: These packets are explicitly hand-rolled to verify wire format;
76         // the remainder of the tests are fine using generated packets
77 
78         // MESSAGE_REQUEST_PING with payload "HELLO WORLD!"
79         final byte[] input = new byte[] {
80                 0x63, (byte) 0x80, 0x73, 0x78, // message: MESSAGE_REQUEST_PING
81                 0x00, 0x00, 0x00, 0x2A, // sequence: 42
82                 0x00, 0x00, 0x00, 0x0C, // length: 12
83                 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21,
84         };
85         // MESSAGE_RESPONSE_SUCCESS with payload "HELLO WORLD!"
86         final byte[] expected = new byte[] {
87                 0x33, (byte) 0x83, (byte) 0x85, 0x67, // message: MESSAGE_RESPONSE_SUCCESS
88                 0x00, 0x00, 0x00, 0x2A, // sequence: 42
89                 0x00, 0x00, 0x00, 0x0C, // length: 12
90                 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21,
91         };
92         assertTransportBehavior(input, expected);
93     }
94 
testPingTrickle()95     public void testPingTrickle() {
96         final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, TAG);
97         final byte[] expected = generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, TAG);
98 
99         final ByteArrayInputStream in = new ByteArrayInputStream(input);
100         final ByteArrayOutputStream out = new ByteArrayOutputStream();
101         mCdm.attachSystemDataTransport(mAssociationId, new TrickleInputStream(in), out);
102 
103         final byte[] actual = waitForByteArray(out, expected.length);
104         assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
105     }
106 
testPingDelay()107     public void testPingDelay() {
108         final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, TAG);
109         final byte[] expected = generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, TAG);
110 
111         final ByteArrayInputStream in = new ByteArrayInputStream(input);
112         final ByteArrayOutputStream out = new ByteArrayOutputStream();
113         mCdm.attachSystemDataTransport(mAssociationId, new DelayingInputStream(in, 1000),
114                 new DelayingOutputStream(out, 1000));
115 
116         final byte[] actual = waitForByteArray(out, expected.length);
117         assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
118     }
119 
testPingGiant()120     public void testPingGiant() {
121         final byte[] blob = new byte[500_000];
122         new Random().nextBytes(blob);
123 
124         final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, blob);
125         final byte[] expected = generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, blob);
126         assertTransportBehavior(input, expected);
127     }
128 
testMultiplePingPing()129     public void testMultiplePingPing() {
130         final byte[] input = concat(
131                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, "red"),
132                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
133         final byte[] expected = concat(
134                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, "red"),
135                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green"));
136         assertTransportBehavior(input, expected);
137     }
138 
testMultipleInvalidPing()139     public void testMultipleInvalidPing() {
140         final byte[] input = concat(
141                 generatePacket(MESSAGE_INVALID, /* sequence */ 1, "red"),
142                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
143         final byte[] expected =
144                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green");
145         assertTransportBehavior(input, expected);
146     }
147 
testMultipleInvalidRequestPing()148     public void testMultipleInvalidRequestPing() {
149         final byte[] input = concat(
150                 generatePacket(MESSAGE_REQUEST_INVALID, /* sequence */ 1, "red"),
151                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
152         final byte[] expected = concat(
153                 generatePacket(MESSAGE_RESPONSE_FAILURE, /* sequence */ 1),
154                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green"));
155         assertTransportBehavior(input, expected);
156     }
157 
testMultipleInvalidResponsePing()158     public void testMultipleInvalidResponsePing() {
159         final byte[] input = concat(
160                 generatePacket(MESSAGE_RESPONSE_INVALID, /* sequence */ 1, "red"),
161                 generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 2, "green"));
162         final byte[] expected =
163                 generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 2, "green");
164         assertTransportBehavior(input, expected);
165     }
166 
testDoubleAttach()167     public void testDoubleAttach() {
168         // Connect an empty connection that is stalled out
169         final InputStream in = new EmptyInputStream();
170         final OutputStream out = new ByteArrayOutputStream();
171         mCdm.attachSystemDataTransport(mAssociationId, in, out);
172         SystemClock.sleep(1000);
173 
174         // Attach a second transport that has some packets; it should disconnect
175         // the first transport and start replying on the second one
176         testPingHandRolled();
177     }
178 
concat(byte[]... blobs)179     public static byte[] concat(byte[]... blobs) {
180         int length = 0;
181         for (byte[] blob : blobs) {
182             length += blob.length;
183         }
184         final ByteBuffer buf = ByteBuffer.allocate(length);
185         for (byte[] blob : blobs) {
186             buf.put(blob);
187         }
188         return buf.array();
189     }
190 
generatePacket(int message, int sequence)191     public static byte[] generatePacket(int message, int sequence) {
192         return generatePacket(message, sequence, EmptyArray.BYTE);
193     }
194 
generatePacket(int message, int sequence, String data)195     public static byte[] generatePacket(int message, int sequence, String data) {
196         return generatePacket(message, sequence, data.getBytes(StandardCharsets.UTF_8));
197     }
198 
generatePacket(int message, int sequence, byte[] data)199     public static byte[] generatePacket(int message, int sequence, byte[] data) {
200         return ByteBuffer.allocate(data.length + 12)
201                 .putInt(message)
202                 .putInt(sequence)
203                 .putInt(data.length)
204                 .put(data)
205                 .array();
206     }
207 
createAssociation()208     private int createAssociation() {
209         getInstrumentation().getUiAutomation().executeShellCommand("cmd companiondevice associate "
210                 + mContext.getUserId() + " " + mContext.getPackageName() + " AA:BB:CC:DD:EE:FF");
211         List<AssociationInfo> infos;
212         for (int i = 0; i < 100; i++) {
213             infos = mCdm.getMyAssociations();
214             if (!infos.isEmpty()) {
215                 return infos.get(0).getId();
216             } else {
217                 SystemClock.sleep(100);
218             }
219         }
220         throw new AssertionError();
221     }
222 
assertTransportBehavior(byte[] input, byte[] expected)223     private void assertTransportBehavior(byte[] input, byte[] expected) {
224         final ByteArrayInputStream in = new ByteArrayInputStream(input);
225         final ByteArrayOutputStream out = new ByteArrayOutputStream();
226         mCdm.attachSystemDataTransport(mAssociationId, in, out);
227 
228         final byte[] actual = waitForByteArray(out, expected.length);
229         assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual));
230     }
231 
waitForByteArray(ByteArrayOutputStream out, int size)232     private static byte[] waitForByteArray(ByteArrayOutputStream out, int size) {
233         int i = 0;
234         while (out.size() < size) {
235             SystemClock.sleep(100);
236             if (i++ % 10 == 0) {
237                 Log.w(TAG, "Waiting for data...");
238             }
239             if (i > 100) {
240                 fail();
241             }
242         }
243         return out.toByteArray();
244     }
245 
246     private static class EmptyInputStream extends InputStream {
247         @Override
read()248         public int read() throws IOException {
249             throw new UnsupportedOperationException();
250         }
251 
252         @Override
read(byte[] b, int off, int len)253         public int read(byte[] b, int off, int len) throws IOException {
254             // Instead of hanging indefinitely, wait a bit and claim that
255             // nothing was read, without hitting EOF
256             SystemClock.sleep(100);
257             return 0;
258         }
259     }
260 
261     private static class DelayingInputStream extends FilterInputStream {
262         private final long mDelay;
263 
DelayingInputStream(InputStream in, long delay)264         DelayingInputStream(InputStream in, long delay) {
265             super(in);
266             mDelay = delay;
267         }
268 
269         @Override
read(byte[] b, int off, int len)270         public int read(byte[] b, int off, int len) throws IOException {
271             SystemClock.sleep(mDelay);
272             return super.read(b, off, len);
273         }
274     }
275 
276     private static class DelayingOutputStream extends FilterOutputStream {
277         private final long mDelay;
278 
DelayingOutputStream(OutputStream out, long delay)279         DelayingOutputStream(OutputStream out, long delay) {
280             super(out);
281             mDelay = delay;
282         }
283 
284         @Override
write(byte[] b, int off, int len)285         public void write(byte[] b, int off, int len) throws IOException {
286             SystemClock.sleep(mDelay);
287             super.write(b, off, len);
288         }
289     }
290 
291     private static class TrickleInputStream extends FilterInputStream {
TrickleInputStream(InputStream in)292         TrickleInputStream(InputStream in) {
293             super(in);
294         }
295 
296         @Override
read(byte[] b, int off, int len)297         public int read(byte[] b, int off, int len) throws IOException {
298             return super.read(b, off, 1);
299         }
300     }
301 }
302