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