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