1 /* 2 * Copyright (C) 2024 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.accessibilityservice.cts; 18 19 import static android.Manifest.permission.MANAGE_ACCESSIBILITY; 20 import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND; 21 import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS; 22 23 import static com.google.common.truth.Truth.assertThat; 24 25 import static org.junit.Assert.assertThrows; 26 import static org.junit.Assume.assumeFalse; 27 import static org.junit.Assume.assumeTrue; 28 29 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule; 30 import android.accessibility.cts.common.InstrumentedAccessibilityService; 31 import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule; 32 import android.accessibilityservice.AccessibilityService; 33 import android.accessibilityservice.BrailleDisplayController; 34 import android.app.Instrumentation; 35 import android.app.UiAutomation; 36 import android.bluetooth.BluetoothDevice; 37 import android.bluetooth.BluetoothManager; 38 import android.content.pm.PackageManager; 39 import android.hardware.input.InputManager; 40 import android.hardware.usb.UsbDevice; 41 import android.hardware.usb.UsbManager; 42 import android.os.Build; 43 import android.os.Bundle; 44 import android.os.Handler; 45 import android.os.IBinder; 46 import android.os.ParcelFileDescriptor; 47 import android.os.SystemProperties; 48 import android.platform.test.annotations.AppModeFull; 49 import android.platform.test.annotations.Presubmit; 50 import android.platform.test.annotations.RequiresFlagsEnabled; 51 import android.platform.test.flag.junit.CheckFlagsRule; 52 import android.platform.test.flag.junit.DeviceFlagsValueProvider; 53 import android.view.accessibility.Flags; 54 55 import androidx.annotation.NonNull; 56 import androidx.test.ext.junit.runners.AndroidJUnit4; 57 import androidx.test.platform.app.InstrumentationRegistry; 58 59 import com.android.compatibility.common.util.ApiTest; 60 import com.android.compatibility.common.util.CddTest; 61 import com.android.compatibility.common.util.TestUtils; 62 63 import junit.framework.AssertionFailedError; 64 65 import org.junit.After; 66 import org.junit.AfterClass; 67 import org.junit.Before; 68 import org.junit.BeforeClass; 69 import org.junit.Rule; 70 import org.junit.Test; 71 import org.junit.rules.RuleChain; 72 import org.junit.runner.RunWith; 73 74 import java.io.File; 75 import java.io.IOException; 76 import java.io.InputStream; 77 import java.io.OutputStream; 78 import java.nio.file.Path; 79 import java.util.ArrayList; 80 import java.util.HashMap; 81 import java.util.List; 82 import java.util.concurrent.Executor; 83 import java.util.concurrent.atomic.AtomicBoolean; 84 import java.util.concurrent.atomic.AtomicInteger; 85 import java.util.concurrent.atomic.AtomicReference; 86 import java.util.function.Consumer; 87 88 /** 89 * Tests for {@link BrailleDisplayController} APIs. 90 */ 91 @RunWith(AndroidJUnit4.class) 92 @CddTest(requirements = {"3.10/C-1-1,C-1-2"}) 93 @Presubmit 94 @AppModeFull 95 @RequiresFlagsEnabled(Flags.FLAG_BRAILLE_DISPLAY_HID) 96 public class BrailleDisplayControllerTest { 97 private static final long CALLBACK_TIMEOUT_MS = 5000; 98 private static final byte[] DESCRIPTOR1 = {0x05, 0x41, 0x01, 0x0A}; 99 private static final byte[] DESCRIPTOR2 = {0x05, 0x41, 0x01, 0x0B}; 100 private static final String BT_ADDRESS1 = "00:11:22:33:AA:BB"; 101 private static final String BT_ADDRESS2 = "22:33:44:55:AA:BB"; 102 // "Real" HIDRAW node files are used for the majority of tests, created 103 // by the 'hid' command line tool. 104 private static final String HIDRAW_NODE_PREFIX = "/dev/hidraw"; 105 // Fake test files are used to test input and writing, which are not supported 106 // by the 'hid' command line tool. 107 // These files are in /data/system so that system_server can read/write from them. 108 // Note: this also requires userdebug/eng builds and using shell commands to 109 // create/read/write from files in this directory, because this test app only has 110 // normal app privileges but userdebug-shell can act as the system user. 111 private static final String FAKE_HIDRAW_DIR = 112 "/data/system/" + BrailleDisplayControllerTest.class.getSimpleName(); 113 114 private static Instrumentation sInstrumentation; 115 private static UiAutomation sUiAutomation; 116 private static String sHidrawNode0, sHidrawNode1; 117 118 private final InstrumentedAccessibilityServiceTestRule<StubBrailleDisplayAccessibilityService> 119 mServiceRule = new InstrumentedAccessibilityServiceTestRule<>( 120 StubBrailleDisplayAccessibilityService.class); 121 private final CheckFlagsRule mCheckFlagsRule = 122 DeviceFlagsValueProvider.createCheckFlagsRule(sUiAutomation); 123 private final AccessibilityDumpOnFailureRule mDumpOnFailureRule = 124 new AccessibilityDumpOnFailureRule(); 125 126 private BluetoothDevice mBluetoothDevice1; 127 private BluetoothDevice mBluetoothDevice2; 128 private StubBrailleDisplayAccessibilityService mService; 129 private BrailleDisplayController mController; 130 private Executor mExecutor; 131 private boolean mIsUserdebugOrEng; 132 133 // Tracks the added and removed HIDRAW device nodes. 134 private int mDeviceCount; 135 private final Object mDeviceWaitObject = new Object(); 136 137 // Default implementation of BrailleDisplayCallback 138 private static class TestBrailleDisplayCallback implements 139 BrailleDisplayController.BrailleDisplayCallback { 140 141 @Override onConnected(@onNull byte[] hidDescriptor)142 public void onConnected(@NonNull byte[] hidDescriptor) { 143 144 } 145 146 @Override onConnectionFailed(int error)147 public void onConnectionFailed(int error) { 148 149 } 150 151 @Override onInput(@onNull byte[] input)152 public void onInput(@NonNull byte[] input) { 153 154 } 155 156 @Override onDisconnected()157 public void onDisconnected() { 158 159 } 160 } 161 162 @Rule 163 public RuleChain mRuleChain = RuleChain 164 .outerRule(mServiceRule) 165 .around(mCheckFlagsRule) 166 .around(mDumpOnFailureRule); 167 168 @BeforeClass oneTimeSetup()169 public static void oneTimeSetup() throws Exception { 170 sInstrumentation = InstrumentationRegistry.getInstrumentation(); 171 sUiAutomation = sInstrumentation.getUiAutomation( 172 UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES); 173 sHidrawNode0 = null; 174 sHidrawNode1 = null; 175 for (int i = 0; i < 100 /*arbitrary max search limit*/; i++) { 176 String path = HIDRAW_NODE_PREFIX + i; 177 if (!new File(path).exists()) { 178 if (sHidrawNode0 == null) { 179 sHidrawNode0 = path; 180 } else if (sHidrawNode1 == null) { 181 sHidrawNode1 = path; 182 break; 183 } 184 } 185 } 186 assertThat(sHidrawNode0).isNotNull(); 187 assertThat(sHidrawNode1).isNotNull(); 188 } 189 190 @AfterClass finalCleanup()191 public static void finalCleanup() { 192 sHidrawNode0 = null; 193 sHidrawNode1 = null; 194 } 195 196 @Before setup()197 public void setup() throws Exception { 198 assumeTrue(SystemProperties.getBoolean("ro.accessibility.support_hidraw", true)); 199 PackageManager pm = sInstrumentation.getContext().getPackageManager(); 200 assumeFalse(pm.hasSystemFeature(PackageManager.FEATURE_WATCH)); 201 202 mService = mServiceRule.getService(); 203 assertThat(mService).isNotNull(); 204 mController = mService.getBrailleDisplayController(); 205 mExecutor = mService.getMainExecutor(); 206 BluetoothManager bluetoothManager = 207 sInstrumentation.getContext().getSystemService(BluetoothManager.class); 208 assumeTrue(bluetoothManager != null); 209 mBluetoothDevice1 = bluetoothManager.getAdapter().getRemoteDevice(BT_ADDRESS1); 210 mBluetoothDevice2 = bluetoothManager.getAdapter().getRemoteDevice(BT_ADDRESS2); 211 mIsUserdebugOrEng = !"user".equals(Build.TYPE); 212 if (mIsUserdebugOrEng) { 213 executeSystemShellCommand("mkdir " + FAKE_HIDRAW_DIR); 214 TestUtils.waitUntil(FAKE_HIDRAW_DIR + " should exist", () -> 215 !executeSystemShellCommand("ls -l " + FAKE_HIDRAW_DIR).isEmpty()); 216 } 217 mDeviceCount = 0; 218 InputManager inputManager = sInstrumentation.getContext().getSystemService( 219 InputManager.class); 220 assertThat(inputManager).isNotNull(); 221 inputManager.registerInputDeviceListener(new InputManager.InputDeviceListener() { 222 @Override 223 public void onInputDeviceAdded(int deviceId) { 224 synchronized (mDeviceWaitObject) { 225 mDeviceCount++; 226 mDeviceWaitObject.notifyAll(); 227 } 228 } 229 230 @Override 231 public void onInputDeviceRemoved(int deviceId) { 232 synchronized (mDeviceWaitObject) { 233 mDeviceCount--; 234 mDeviceWaitObject.notifyAll(); 235 } 236 } 237 238 @Override 239 public void onInputDeviceChanged(int deviceId) { 240 } 241 }, new Handler(mService.getMainLooper())); 242 } 243 244 @After cleanup()245 public void cleanup() throws Exception { 246 if (mController != null) { 247 mController.disconnect(); 248 } 249 if (mIsUserdebugOrEng) { 250 executeSystemShellCommand("rm -rf " + FAKE_HIDRAW_DIR); 251 } 252 // Individual tests should clean up their own test HIDRAW nodes, 253 // but this can sometimes take a moment to propagate. 254 TestUtils.waitOn(mDeviceWaitObject, () -> { 255 synchronized (mDeviceWaitObject) { 256 return mDeviceCount == 0; 257 } 258 }, CALLBACK_TIMEOUT_MS, "Expected all HIDRAW devices removed"); 259 } 260 setTestData(List<Bundle> testData)261 private void setTestData(List<Bundle> testData) { 262 setTestData(mService, testData); 263 } 264 setTestData(AccessibilityService service, List<Bundle> testData)265 private static void setTestData(AccessibilityService service, List<Bundle> testData) { 266 sUiAutomation.adoptShellPermissionIdentity(MANAGE_ACCESSIBILITY); 267 BrailleDisplayController.setTestBrailleDisplayData(service, testData); 268 sUiAutomation.dropShellPermissionIdentity(); 269 } 270 getTestBrailleDisplay(String path, byte[] descriptor, String uniq, boolean isBluetooth)271 private static Bundle getTestBrailleDisplay(String path, byte[] descriptor, String uniq, 272 boolean isBluetooth) { 273 Bundle bundle = new Bundle(); 274 bundle.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path); 275 bundle.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor); 276 bundle.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq); 277 bundle.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, isBluetooth); 278 return bundle; 279 } 280 281 /** 282 * Creates a test /dev/hidraw* node using the 'hid' command line tool. 283 * 284 * @return An OutputStream that keeps the 'hid' command alive. Closing this stream will 285 * stop the command and delete the HIDRAW node. 286 */ createTestHidrawNode(String expectedPath)287 private OutputStream createTestHidrawNode(String expectedPath) throws Exception { 288 assertThat(new File(expectedPath).exists()).isFalse(); 289 290 // See frameworks/base/cmds/hid/README.md for the expected format. 291 // These values are all valid defaults copied from cts/tests/tests/hardware/res/raw, 292 // but none are actually read in the tests because the tests use the data provided by 293 // BrailleDisplayController.setTestBrailleDisplayData. 294 String registerCommand = """ 295 { 296 "id": ID, 297 "command": "register", 298 "name": "fake device", 299 "bus": "bluetooth", 300 "vid": 0x0001, 301 "pid": 0x0001, 302 "source": "GAMEPAD", 303 "descriptor": [ 304 0x05, 0x41, 0x09, 0x05, 0xa1, 0x01, 0x05, 0x01, 0x09, 0x01, 0xa1, 0x00, 0x09, 305 0x30, 0x09, 0x31, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 306 0x02, 0xc0, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0a, 0x15, 0x00, 0x25, 0x01, 0x75, 307 0x01, 0x95, 0x0a, 0x81, 0x02, 0x95, 0x01, 0x75, 0x06, 0x81, 0x01, 0xc0 308 ] 309 } 310 """.replace("ID", expectedPath.equals(sHidrawNode0) ? "0" : "1"); 311 312 final int expectedDeviceCount; 313 synchronized (mDeviceWaitObject) { 314 expectedDeviceCount = mDeviceCount + 1; 315 } 316 ParcelFileDescriptor hidCommandInput = sUiAutomation.executeShellCommandRw("hid -")[1]; 317 OutputStream hidCommand = 318 new ParcelFileDescriptor.AutoCloseOutputStream(hidCommandInput); 319 hidCommand.write(registerCommand.getBytes()); 320 hidCommand.flush(); 321 TestUtils.waitOn(mDeviceWaitObject, () -> { 322 synchronized (mDeviceWaitObject) { 323 return mDeviceCount == expectedDeviceCount; 324 } 325 }, CALLBACK_TIMEOUT_MS, "Expected HIDRAW device to be created"); 326 return hidCommand; 327 } 328 329 /** 330 * Runs a shell command as the {@code system} user. 331 * 332 * <p>Supports redirection (e.g. {@code echo hello > file}) and returns the output as a String. 333 */ executeSystemShellCommand(String command)334 private static String executeSystemShellCommand(String command) throws Exception { 335 // The standard UiAutomation#executeShellCommand is implemented by Runtime#exec 336 // which doesn't support using redirection. 337 // This implementation works around this by executing `sh` directly, and then 338 // providing the requested shell command as stdin for `sh`. 339 ParcelFileDescriptor[] stdoutStdin = sUiAutomation.executeShellCommandRw("su system sh"); 340 ParcelFileDescriptor stdout = stdoutStdin[0]; 341 ParcelFileDescriptor stdin = stdoutStdin[1]; 342 try (OutputStream stream = new ParcelFileDescriptor.AutoCloseOutputStream(stdin)) { 343 stream.write(command.getBytes()); 344 } 345 try (InputStream stream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) { 346 return new String(stream.readAllBytes()); 347 } 348 } 349 createFakeHidrawNode(String name)350 private static String createFakeHidrawNode(String name) throws Exception { 351 String path = Path.of(FAKE_HIDRAW_DIR, name).toString(); 352 executeSystemShellCommand("touch " + path); 353 TestUtils.waitUntil(path + " should exist", () -> 354 !executeSystemShellCommand("ls " + path).isEmpty()); 355 return path; 356 } 357 expectConnectionSuccess(BrailleDisplayController controller, Executor executor, BluetoothDevice device, Consumer<byte[]> onInput, Runnable onDisconnected)358 private static byte[] expectConnectionSuccess(BrailleDisplayController controller, 359 Executor executor, BluetoothDevice device, Consumer<byte[]> onInput, 360 Runnable onDisconnected) throws Exception { 361 final AtomicReference<byte[]> connectedDeviceDescriptor = new AtomicReference<>(); 362 BrailleDisplayController.BrailleDisplayCallback callback = 363 new TestBrailleDisplayCallback() { 364 @Override 365 public void onConnected(@NonNull byte[] hidDescriptor) { 366 connectedDeviceDescriptor.set(hidDescriptor); 367 } 368 369 @Override 370 public void onInput(@NonNull byte[] input) { 371 if (onInput != null) { 372 onInput.accept(input); 373 } 374 } 375 376 @Override 377 public void onDisconnected() { 378 if (onDisconnected != null) { 379 onDisconnected.run(); 380 } 381 } 382 }; 383 if (executor == null) { 384 controller.connect(device, callback); 385 } else { 386 controller.connect(device, executor, callback); 387 } 388 389 390 TestUtils.waitUntil("Expected connection success", (int) CALLBACK_TIMEOUT_MS / 1000, 391 () -> connectedDeviceDescriptor.get() != null); 392 return connectedDeviceDescriptor.get(); 393 } 394 expectConnectionSuccess(BrailleDisplayController controller, Executor executor, BluetoothDevice device)395 private static byte[] expectConnectionSuccess(BrailleDisplayController controller, 396 Executor executor, BluetoothDevice device) throws Exception { 397 return expectConnectionSuccess(controller, executor, device, bytes -> { 398 }, () -> { 399 }); 400 } 401 expectConnectionFailed(BrailleDisplayController controller, Executor executor, BluetoothDevice device)402 private static int expectConnectionFailed(BrailleDisplayController controller, 403 Executor executor, BluetoothDevice device) throws Exception { 404 final AtomicInteger errorCode = new AtomicInteger(0); 405 BrailleDisplayController.BrailleDisplayCallback callback = 406 new TestBrailleDisplayCallback() { 407 @Override 408 public void onConnectionFailed(int error) { 409 errorCode.set(error); 410 } 411 }; 412 controller.connect(device, executor, callback); 413 414 TestUtils.waitUntil("Expected connection failed", (int) CALLBACK_TIMEOUT_MS / 1000, 415 () -> errorCode.get() != 0); 416 return errorCode.get(); 417 } 418 expectFileContents(String filePath, String expectedFileContents)419 private static void expectFileContents(String filePath, String expectedFileContents) 420 throws Exception { 421 AtomicReference<String> fileContents = new AtomicReference<>(); 422 try { 423 TestUtils.waitUntil("", 424 (int) (CALLBACK_TIMEOUT_MS / 1000), 425 () -> { 426 fileContents.set(executeSystemShellCommand("cat " + filePath)); 427 return expectedFileContents.equals(fileContents.get()); 428 }); 429 } catch (AssertionFailedError e) { 430 // TestUtils.waitUntil(String, ...) requires a constant error message before failure 431 // even occurs, so use a try-catch to append a more useful error message built from 432 // the last known file contents after failure. 433 throw new AssertionFailedError("Expected output '" + expectedFileContents 434 + "', received '" + fileContents.get() + "'\n" + e.getMessage()); 435 } 436 } 437 438 @Test 439 @ApiTest(apis = { 440 "android.accessibilityservice.BrailleDisplayController#connect", 441 "android.accessibilityservice.BrailleDisplayController" 442 + ".BrailleDisplayCallback#onConnected", 443 "android.accessibilityservice.BrailleDisplayController#isConnected", 444 }) connect()445 public void connect() throws Exception { 446 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 447 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, true); 448 setTestData(List.of(testBD)); 449 450 byte[] connectedDeviceDescriptor = expectConnectionSuccess(mController, mExecutor, 451 mBluetoothDevice1); 452 453 assertThat(connectedDeviceDescriptor).isEqualTo(DESCRIPTOR1); 454 assertThat(mController.isConnected()).isTrue(); 455 } 456 } 457 458 @Test 459 @ApiTest(apis = { 460 "android.accessibilityservice.BrailleDisplayController#connect", 461 "android.accessibilityservice.BrailleDisplayController" 462 + ".BrailleDisplayCallback#onConnected", 463 "android.accessibilityservice.BrailleDisplayController#isConnected", 464 }) connect_defaultExecutor_isSuccessful()465 public void connect_defaultExecutor_isSuccessful() throws Exception { 466 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 467 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, true); 468 setTestData(List.of(testBD)); 469 470 byte[] connectedDeviceDescriptor = expectConnectionSuccess(mController, /*executor=*/ 471 null, 472 mBluetoothDevice1); 473 474 assertThat(connectedDeviceDescriptor).isEqualTo(DESCRIPTOR1); 475 assertThat(mController.isConnected()).isTrue(); 476 } 477 } 478 479 @Test 480 @ApiTest(apis = { 481 "android.accessibilityservice.BrailleDisplayController#connect", 482 "android.accessibilityservice.BrailleDisplayController" 483 + ".BrailleDisplayCallback#onConnected", 484 "android.accessibilityservice.BrailleDisplayController" 485 + ".BrailleDisplayCallback#onConnectionFailed", 486 }) connect_alreadyConnected_throwsIllegalStateException()487 public void connect_alreadyConnected_throwsIllegalStateException() 488 throws Exception { 489 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 490 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, true); 491 setTestData(List.of(testBD)); 492 expectConnectionSuccess(mController, mExecutor, mBluetoothDevice1); 493 494 assertThrows(IllegalStateException.class, 495 () -> mController.connect(mBluetoothDevice1, mExecutor, 496 new TestBrailleDisplayCallback())); 497 } 498 } 499 500 @Test 501 @ApiTest(apis = { 502 "android.accessibilityservice.BrailleDisplayController#connect", 503 "android.accessibilityservice.BrailleDisplayController" 504 + ".BrailleDisplayCallback#onConnectionFailed", 505 "android.accessibilityservice.BrailleDisplayController" 506 + ".BrailleDisplayCallback#FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND", 507 }) connect_wrongBusType_returnsNotFoundError()508 public void connect_wrongBusType_returnsNotFoundError() throws Exception { 509 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 510 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, 511 /*isBluetooth=*/false); 512 setTestData(List.of(testBD)); 513 514 int errorCode = expectConnectionFailed(mController, mExecutor, mBluetoothDevice1); 515 516 assertThat(errorCode).isEqualTo(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND); 517 } 518 } 519 520 @Test 521 @ApiTest(apis = { 522 "android.accessibilityservice.BrailleDisplayController#connect", 523 "android.accessibilityservice.BrailleDisplayController" 524 + ".BrailleDisplayCallback#onConnectionFailed", 525 "android.accessibilityservice.BrailleDisplayController" 526 + ".BrailleDisplayCallback#FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND", 527 }) connect_wrongUniq_returnsNotFoundError()528 public void connect_wrongUniq_returnsNotFoundError() throws Exception { 529 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 530 String wrongUniq = mBluetoothDevice1.getAddress() + "_extra"; 531 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, wrongUniq, true); 532 setTestData(List.of(testBD)); 533 534 int errorCode = expectConnectionFailed(mController, mExecutor, mBluetoothDevice1); 535 536 assertThat(errorCode).isEqualTo(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND); 537 } 538 } 539 540 @Test 541 @ApiTest(apis = { 542 "android.accessibilityservice.BrailleDisplayController#connect", 543 "android.accessibilityservice.BrailleDisplayController" 544 + ".BrailleDisplayCallback#onConnectionFailed", 545 "android.accessibilityservice.BrailleDisplayController" 546 + ".BrailleDisplayCallback#FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND", 547 }) connect_nonBrailleDisplayDescriptor_returnsNotFoundError()548 public void connect_nonBrailleDisplayDescriptor_returnsNotFoundError() 549 throws Exception { 550 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 551 final byte[] nonBrailleDisplayDescriptor = {0x05, 0x01 /* != 0x41 */}; 552 Bundle testBD = getTestBrailleDisplay( 553 sHidrawNode0, nonBrailleDisplayDescriptor, BT_ADDRESS1, true); 554 setTestData(List.of(testBD)); 555 556 int errorCode = expectConnectionFailed(mController, mExecutor, mBluetoothDevice1); 557 558 assertThat(errorCode).isEqualTo(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND); 559 } 560 } 561 562 @Test 563 @ApiTest(apis = { 564 "android.accessibilityservice.BrailleDisplayController#connect", 565 "android.accessibilityservice.BrailleDisplayController" 566 + ".BrailleDisplayCallback#onConnected", 567 }) connect_multipleDevices_returnsCorrectDevice()568 public void connect_multipleDevices_returnsCorrectDevice() throws Exception { 569 try ( 570 OutputStream testHidrawNode0 = createTestHidrawNode(sHidrawNode0); 571 OutputStream testHidrawNode1 = createTestHidrawNode(sHidrawNode1) 572 ) { 573 Bundle testBD1 = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, true); 574 Bundle testBD2 = getTestBrailleDisplay(sHidrawNode1, DESCRIPTOR2, BT_ADDRESS2, true); 575 setTestData(List.of(testBD1, testBD2)); 576 577 byte[] connectedDeviceDescriptor = expectConnectionSuccess(mController, mExecutor, 578 mBluetoothDevice2); 579 580 assertThat(connectedDeviceDescriptor).isEqualTo(DESCRIPTOR2); 581 assertThat(mController.isConnected()).isTrue(); 582 } 583 } 584 585 @Test 586 @ApiTest(apis = { 587 "android.accessibilityservice.BrailleDisplayController#connect", 588 "android.accessibilityservice.BrailleDisplayController" 589 + ".BrailleDisplayCallback#onConnectionFailed", 590 "android.accessibilityservice.BrailleDisplayController" 591 + ".BrailleDisplayCallback#FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND", 592 }) connect_multipleIdenticalDevices_returnsNotFoundError()593 public void connect_multipleIdenticalDevices_returnsNotFoundError() throws Exception { 594 try ( 595 OutputStream testHidrawNode0 = createTestHidrawNode(sHidrawNode0); 596 OutputStream testHidrawNode1 = createTestHidrawNode(sHidrawNode1) 597 ) { 598 Bundle testBD1 = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, true); 599 // BD2 copies all BD1 device properties, but is exposed as a different HIDRAW node path. 600 Bundle testBD2 = testBD1.deepCopy(); 601 testBD2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, 602 sHidrawNode1); 603 setTestData(List.of(testBD1, testBD2)); 604 605 int errorCode = expectConnectionFailed(mController, mExecutor, mBluetoothDevice1); 606 607 assertThat(errorCode).isEqualTo(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND); 608 } 609 } 610 611 @Test 612 @ApiTest(apis = { 613 "android.accessibilityservice.BrailleDisplayController#connect", 614 "android.accessibilityservice.BrailleDisplayController" 615 + ".BrailleDisplayCallback#onConnectionFailed", 616 "android.accessibilityservice.BrailleDisplayController" 617 + ".BrailleDisplayCallback#FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND", 618 }) connect_multipleServicesSameBrailleDisplay_returnsNotFoundError()619 public void connect_multipleServicesSameBrailleDisplay_returnsNotFoundError() throws Exception { 620 InstrumentedAccessibilityService anotherService = 621 InstrumentedAccessibilityService.enableService( 622 InstrumentedAccessibilityService.class); 623 try { 624 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 625 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, 626 true); 627 setTestData(mService, List.of(testBD)); 628 setTestData(anotherService, List.of(testBD)); 629 630 // Connect another service to the Braille display first. 631 expectConnectionSuccess(anotherService.getBrailleDisplayController(), 632 anotherService.getMainExecutor(), mBluetoothDevice1); 633 // Attempt to connect a different service to the same Braille display. 634 int errorCode = expectConnectionFailed(mController, mExecutor, mBluetoothDevice1); 635 636 assertThat(anotherService.getBrailleDisplayController().isConnected()).isTrue(); 637 assertThat(mController.isConnected()).isFalse(); 638 assertThat(errorCode).isEqualTo(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND); 639 } 640 } finally { 641 anotherService.getBrailleDisplayController().disconnect(); 642 anotherService.disableSelfAndRemove(); 643 } 644 } 645 646 @Test 647 @ApiTest(apis = { 648 "android.accessibilityservice.BrailleDisplayController#connect", 649 "android.accessibilityservice.BrailleDisplayController" 650 + ".BrailleDisplayCallback#onConnected", 651 "android.accessibilityservice.BrailleDisplayController" 652 + ".BrailleDisplayCallback#onConnectionFailed", 653 }) connect_canConnectAfterFailedConnection()654 public void connect_canConnectAfterFailedConnection() throws Exception { 655 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 656 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, true); 657 setTestData(List.of(testBD)); 658 659 expectConnectionFailed(mController, mExecutor, mBluetoothDevice2); 660 expectConnectionSuccess(mController, mExecutor, mBluetoothDevice1); 661 } 662 } 663 664 @Test 665 @ApiTest(apis = { 666 "android.accessibilityservice.BrailleDisplayController#connect", 667 "android.accessibilityservice.BrailleDisplayController" 668 + ".BrailleDisplayCallback#onConnectionFailed", 669 "android.accessibilityservice.BrailleDisplayController" 670 + ".BrailleDisplayCallback#FLAG_ERROR_CANNOT_ACCESS", 671 }) connect_unableToGetHidrawNodePaths_returnsCannotAccessError()672 public void connect_unableToGetHidrawNodePaths_returnsCannotAccessError() throws Exception { 673 // BrailleDisplayScanner#getHidrawNodePaths returns null when test data is empty. 674 setTestData(List.of()); 675 676 int errorCode = expectConnectionFailed(mController, mExecutor, mBluetoothDevice1); 677 678 assertThat(errorCode).isEqualTo(FLAG_ERROR_CANNOT_ACCESS); 679 } 680 681 @Test 682 @ApiTest(apis = { 683 "android.accessibilityservice.BrailleDisplayController#connect", 684 "android.accessibilityservice.BrailleDisplayController" 685 + ".BrailleDisplayCallback#onConnectionFailed", 686 "android.accessibilityservice.BrailleDisplayController" 687 + ".BrailleDisplayCallback#FLAG_ERROR_CANNOT_ACCESS", 688 "android.accessibilityservice.BrailleDisplayController" 689 + ".BrailleDisplayCallback#FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND", 690 }) connect_unableToGetReportDescriptor_returnsErrors()691 public void connect_unableToGetReportDescriptor_returnsErrors() throws Exception { 692 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 693 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, /*descriptor=*/null, BT_ADDRESS1, 694 true); 695 setTestData(List.of(testBD)); 696 697 int errorCode = expectConnectionFailed(mController, mExecutor, mBluetoothDevice1); 698 699 assertThat(errorCode).isEqualTo( 700 FLAG_ERROR_CANNOT_ACCESS | FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND); 701 } 702 } 703 704 // TODO: b/316035785 - Change this to a CTS-Verifier test, requiring >=1 connected USB device 705 @Test 706 @ApiTest(apis = { 707 "android.accessibilityservice.BrailleDisplayController#connect", 708 }) connect_realUsbDevice_noPermission_throwsSecurityException()709 public void connect_realUsbDevice_noPermission_throwsSecurityException() { 710 // Unlike BluetoothDevice used throughout other tests, UsbDevice does not have a test 711 // constructor, so we can only act on real USB devices in a test. 712 // 713 // It is unlikely that a CTS test environment will have a real Braille display available, 714 // but we can at least test the security behavior of connect(UsbDevice) by 715 // attempting to connect to any USB device that is not approved for this test app. 716 // 717 // All logic after the initial security check is shared between USB and Bluetooth devices. 718 UsbManager usbManager = sInstrumentation.getContext().getSystemService(UsbManager.class); 719 assumeTrue(usbManager != null); 720 HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList(); 721 assumeFalse(deviceList.isEmpty()); 722 boolean foundUnapprovedDevice = false; 723 for (UsbDevice usbDevice : deviceList.values()) { 724 if (!usbManager.hasPermission(usbDevice)) { 725 foundUnapprovedDevice = true; 726 assertThrows(SecurityException.class, 727 () -> mController.connect(usbDevice, mExecutor, 728 new TestBrailleDisplayCallback())); 729 } 730 } 731 assertThat(foundUnapprovedDevice).isTrue(); 732 } 733 734 @Test 735 @ApiTest(apis = { 736 "android.accessibilityservice.BrailleDisplayController#connect", 737 "android.accessibilityservice.BrailleDisplayController#disconnect", 738 "android.accessibilityservice.BrailleDisplayController#isConnected", 739 }) connect_allowsReconnectionAfterDisconnect()740 public void connect_allowsReconnectionAfterDisconnect() throws Exception { 741 // This test checks that we can reconnect after disconnection, so prepare the 742 // test state by calling another test that already connects & disconnects. 743 disconnect_disconnectsExistingConnection(); 744 TestUtils.waitOn(mDeviceWaitObject, () -> { 745 synchronized (mDeviceWaitObject) { 746 return mDeviceCount == 0 && !(new File(sHidrawNode0).exists()); 747 } 748 }, CALLBACK_TIMEOUT_MS, "Expected " + sHidrawNode0 + " to be removed"); 749 750 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 751 expectConnectionSuccess(mController, mExecutor, mBluetoothDevice1); 752 assertThat(mController.isConnected()).isTrue(); 753 } 754 } 755 756 @Test 757 @ApiTest(apis = { 758 "android.accessibilityservice.BrailleDisplayController#disconnect", 759 "android.accessibilityservice.BrailleDisplayController" 760 + ".BrailleDisplayCallback#onDisconnected", 761 }) disconnect_disconnectsExistingConnection()762 public void disconnect_disconnectsExistingConnection() throws Exception { 763 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 764 AtomicBoolean calledDisconnected = new AtomicBoolean(); 765 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, true); 766 setTestData(List.of(testBD)); 767 expectConnectionSuccess(mController, mExecutor, mBluetoothDevice1, null, 768 () -> calledDisconnected.set(true)); 769 770 mController.disconnect(); 771 772 TestUtils.waitUntil("Expected disconnection", (int) CALLBACK_TIMEOUT_MS / 1000, 773 calledDisconnected::get); 774 assertThat(mController.isConnected()).isFalse(); 775 } 776 } 777 778 @Test 779 @ApiTest(apis = { 780 "android.accessibilityservice.BrailleDisplayController#disconnect", 781 }) disconnect_notConnected_nothingHappens()782 public void disconnect_notConnected_nothingHappens() { 783 mController.disconnect(); 784 785 assertThat(mController.isConnected()).isFalse(); 786 } 787 788 @Test 789 @ApiTest(apis = { 790 "android.accessibilityservice.BrailleDisplayController" 791 + ".BrailleDisplayCallback#onDisconnected", 792 }) deviceIsRemoved_callsOnBrailleDisplayDisconnected()793 public void deviceIsRemoved_callsOnBrailleDisplayDisconnected() throws Exception { 794 AtomicBoolean calledDisconnected = new AtomicBoolean(); 795 try (OutputStream testHidrawNode = createTestHidrawNode(sHidrawNode0)) { 796 Bundle testBD = getTestBrailleDisplay(sHidrawNode0, DESCRIPTOR1, BT_ADDRESS1, true); 797 setTestData(List.of(testBD)); 798 expectConnectionSuccess(mController, mExecutor, mBluetoothDevice1, null, 799 () -> calledDisconnected.set(true)); 800 801 // Closing the OutputStream stops the `hid` command, and stopping the 802 // `hid` command causes the HIDRAW node it created to be removed. 803 testHidrawNode.close(); 804 805 TestUtils.waitUntil("Expected disconnection", (int) CALLBACK_TIMEOUT_MS / 1000, 806 calledDisconnected::get); 807 } 808 } 809 810 @Test 811 @ApiTest(apis = { 812 "android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback#onInput", 813 }) onInput()814 public void onInput() throws Exception { 815 assumeTrue("Test requires debug build", mIsUserdebugOrEng); 816 String input1 = "hello", input2 = "world"; 817 Object waitObject = new Object(); 818 List<byte[]> receivedInputBytes = new ArrayList<>(); 819 String hidraw1 = createFakeHidrawNode("hidraw1"); 820 Bundle testBD = getTestBrailleDisplay(hidraw1, DESCRIPTOR1, BT_ADDRESS1, true); 821 setTestData(List.of(testBD)); 822 expectConnectionSuccess(mController, mExecutor, mBluetoothDevice1, bytes -> { 823 synchronized (waitObject) { 824 receivedInputBytes.add(bytes); 825 waitObject.notifyAll(); 826 } 827 }, null); 828 829 // Fill the Braille display "device" (test file) with two input messages 830 // that arrive one second apart. 831 executeSystemShellCommand("echo -n " + input1 + " >> " + hidraw1 832 + " && sleep 1 && " 833 + "echo -n " + input2 + " >> " + hidraw1); 834 835 // Expect that both are individually received. 836 TestUtils.waitOn(waitObject, () -> receivedInputBytes.size() == 2, 837 CALLBACK_TIMEOUT_MS, 838 "Expected to receive 2 calls to onInput"); 839 assertThat(receivedInputBytes.get(0)).isEqualTo(input1.getBytes()); 840 assertThat(receivedInputBytes.get(1)).isEqualTo(input2.getBytes()); 841 } 842 843 @Test 844 @ApiTest(apis = { 845 "android.accessibilityservice.BrailleDisplayController#write", 846 }) write()847 public void write() throws Exception { 848 assumeTrue("Test requires debug build", mIsUserdebugOrEng); 849 String output1 = "hello", output2 = "world"; 850 String hidraw1 = createFakeHidrawNode("hidraw1"); 851 Bundle testBD = getTestBrailleDisplay(hidraw1, DESCRIPTOR1, BT_ADDRESS1, true); 852 setTestData(List.of(testBD)); 853 expectConnectionSuccess(mController, mExecutor, mBluetoothDevice1); 854 855 mController.write(output1.getBytes()); 856 mController.write(output2.getBytes()); 857 858 expectFileContents(hidraw1, output1 + output2); 859 } 860 861 @Test 862 @ApiTest(apis = { 863 "android.accessibilityservice.BrailleDisplayController#write", 864 }) write_largeOutput_throwsForLargeOutput()865 public void write_largeOutput_throwsForLargeOutput() throws Exception { 866 assumeTrue("Test requires debug build", mIsUserdebugOrEng); 867 AtomicBoolean calledDisconnected = new AtomicBoolean(); 868 String regularOutput = "ABC"; 869 String largeOutput = "A".repeat(IBinder.getSuggestedMaxIpcSizeBytes() * 2); 870 String hidraw1 = createFakeHidrawNode("hidraw1"); 871 Bundle testBD = getTestBrailleDisplay(hidraw1, DESCRIPTOR1, BT_ADDRESS1, true); 872 setTestData(List.of(testBD)); 873 expectConnectionSuccess(mController, mExecutor, mBluetoothDevice1, null, 874 () -> calledDisconnected.set(true)); 875 876 assertThrows(IllegalArgumentException.class, 877 () -> mController.write(largeOutput.getBytes())); 878 mController.write(regularOutput.getBytes()); 879 880 expectFileContents(hidraw1, regularOutput); 881 } 882 883 @Test 884 @ApiTest(apis = { 885 "android.accessibilityservice.BrailleDisplayController#write", 886 }) write_notConnected_throwsIfNotConnected()887 public void write_notConnected_throwsIfNotConnected() { 888 assertThrows(IOException.class, () -> mController.write("hello".getBytes())); 889 // No connected HIDRAW device file, so nothing to assert is empty. 890 } 891 } 892