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