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 com.android.server.accessibility;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import static org.junit.Assert.assertThrows;
23 import static org.mockito.ArgumentMatchers.anyInt;
24 import static org.mockito.ArgumentMatchers.eq;
25 import static org.mockito.Mockito.spy;
26 import static org.mockito.Mockito.verify;
27 import static org.mockito.Mockito.when;
28 
29 import android.accessibilityservice.BrailleDisplayController;
30 import android.accessibilityservice.IBrailleDisplayController;
31 import android.content.Context;
32 import android.os.Bundle;
33 import android.os.IBinder;
34 import android.testing.DexmakerShareClassLoaderRule;
35 
36 import androidx.test.platform.app.InstrumentationRegistry;
37 
38 import com.android.internal.util.HexDump;
39 
40 import com.google.common.truth.Expect;
41 
42 import org.junit.Before;
43 import org.junit.Rule;
44 import org.junit.Test;
45 import org.junit.experimental.runners.Enclosed;
46 import org.junit.runner.RunWith;
47 import org.junit.runners.Parameterized;
48 import org.mockito.Mock;
49 import org.mockito.Mockito;
50 import org.mockito.MockitoAnnotations;
51 
52 import java.io.File;
53 import java.nio.file.Path;
54 import java.util.Arrays;
55 import java.util.Collection;
56 import java.util.List;
57 
58 /**
59  * Tests for internal details of {@link BrailleDisplayConnection}.
60  *
61  * <p>Prefer adding new tests in CTS where possible.
62  */
63 @RunWith(Enclosed.class)
64 public class BrailleDisplayConnectionTest {
65 
66     public static class ScannerTest {
67         private static final Path NULL_PATH = Path.of("/dev/null");
68 
69         private BrailleDisplayConnection mBrailleDisplayConnection;
70         @Mock
71         private BrailleDisplayConnection.NativeInterface mNativeInterface;
72         @Mock
73         private AccessibilityServiceConnection mServiceConnection;
74 
75         @Rule
76         public final Expect expect = Expect.create();
77 
78         private Context mContext;
79 
80         // To mock package-private class
81         @Rule
82         public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
83                 new DexmakerShareClassLoaderRule();
84 
85         @Before
setup()86         public void setup() {
87             MockitoAnnotations.initMocks(this);
88             mContext = InstrumentationRegistry.getInstrumentation().getContext();
89             when(mServiceConnection.isConnectedLocked()).thenReturn(true);
90             mBrailleDisplayConnection =
91                     spy(new BrailleDisplayConnection(new Object(), mServiceConnection));
92         }
93 
94         @Test
defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths()95         public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception {
96             File testDir = mContext.getFilesDir();
97             Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0");
98             Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1");
99             Path otherDevice = Path.of(testDir.getPath(), "otherDevice");
100             Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice};
101             try {
102                 for (Path node : nodePaths) {
103                     assertThat(node.toFile().createNewFile()).isTrue();
104                 }
105 
106                 BrailleDisplayConnection.BrailleDisplayScanner scanner =
107                         BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
108 
109                 assertThat(scanner.getHidrawNodePaths(testDir.toPath()))
110                         .containsExactly(hidrawNode0, hidrawNode1);
111             } finally {
112                 for (Path node : nodePaths) {
113                     node.toFile().delete();
114                 }
115             }
116         }
117 
118         @Test
defaultNativeScanner_getReportDescriptor_returnsDescriptor()119         public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() {
120             int descriptorSize = 4;
121             byte[] descriptor = {0xB, 0xE, 0xE, 0xF};
122             when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize);
123             when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(
124                     descriptor);
125 
126             BrailleDisplayConnection.BrailleDisplayScanner scanner =
127                     BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
128 
129             assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor);
130         }
131 
132         @Test
defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull()133         public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() {
134             when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0);
135 
136             BrailleDisplayConnection.BrailleDisplayScanner scanner =
137                     BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
138 
139             assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull();
140         }
141 
142         @Test
defaultNativeScanner_getUniqueId_returnsUniq()143         public void defaultNativeScanner_getUniqueId_returnsUniq() {
144             String macAddress = "12:34:56:78";
145             when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress);
146 
147             BrailleDisplayConnection.BrailleDisplayScanner scanner =
148                     BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
149 
150             assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress);
151         }
152 
153         @Test
defaultNativeScanner_getDeviceBusType_busUsb()154         public void defaultNativeScanner_getDeviceBusType_busUsb() {
155             when(mNativeInterface.getHidrawBusType(anyInt()))
156                     .thenReturn(BrailleDisplayConnection.BUS_USB);
157 
158             BrailleDisplayConnection.BrailleDisplayScanner scanner =
159                     BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
160 
161             assertThat(scanner.getDeviceBusType(NULL_PATH))
162                     .isEqualTo(BrailleDisplayConnection.BUS_USB);
163         }
164 
165         @Test
defaultNativeScanner_getDeviceBusType_busBluetooth()166         public void defaultNativeScanner_getDeviceBusType_busBluetooth() {
167             when(mNativeInterface.getHidrawBusType(anyInt()))
168                     .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH);
169 
170             BrailleDisplayConnection.BrailleDisplayScanner scanner =
171                     BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
172 
173             assertThat(scanner.getDeviceBusType(NULL_PATH))
174                     .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
175         }
176 
177         @Test
defaultNativeScanner_getName_returnsName()178         public void defaultNativeScanner_getName_returnsName() {
179             String name = "My Braille Display";
180             when(mNativeInterface.getHidrawName(anyInt())).thenReturn(name);
181 
182             BrailleDisplayConnection.BrailleDisplayScanner scanner =
183                     BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
184 
185             assertThat(scanner.getName(NULL_PATH)).isEqualTo(name);
186         }
187 
188         @Test
write_bypassesServiceSideCheckWithLargeBuffer_disconnects()189         public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() {
190             Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
191             mBrailleDisplayConnection.write(
192                     new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]);
193 
194             verify(mBrailleDisplayConnection).disconnect();
195         }
196 
197         @Test
write_notConnected_throwsIllegalStateException()198         public void write_notConnected_throwsIllegalStateException() {
199             when(mServiceConnection.isConnectedLocked()).thenReturn(false);
200 
201             assertThrows(IllegalStateException.class,
202                     () -> mBrailleDisplayConnection.write(new byte[1]));
203         }
204 
205         @Test
write_unableToCreateWriteStream_disconnects()206         public void write_unableToCreateWriteStream_disconnects() {
207             Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
208             // mBrailleDisplayConnection#connectLocked was never called so the
209             // connection's mHidrawNode is still null. This will throw an exception
210             // when attempting to create FileOutputStream on the node.
211             mBrailleDisplayConnection.write(new byte[1]);
212 
213             verify(mBrailleDisplayConnection).disconnect();
214         }
215 
216         @Test
connect_unableToGetUniq_usesNameFallback()217         public void connect_unableToGetUniq_usesNameFallback() throws Exception {
218             try {
219                 IBrailleDisplayController controller =
220                         Mockito.mock(IBrailleDisplayController.class);
221                 final Path path = Path.of("/dev/null");
222                 final String macAddress = "00:11:22:33:AA:BB";
223                 final String name = "My Braille Display";
224                 final byte[] descriptor = {0x05, 0x41};
225                 Bundle bd = new Bundle();
226                 bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH,
227                         path.toString());
228                 bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR,
229                         descriptor);
230                 bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME, name);
231                 bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, true);
232                 bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, null);
233                 BrailleDisplayConnection.BrailleDisplayScanner scanner =
234                         mBrailleDisplayConnection.setTestData(List.of(bd));
235                 // Validate that the test data is set up correctly before attempting connection:
236                 assertThat(scanner.getUniqueId(path)).isNull();
237                 assertThat(scanner.getName(path)).isEqualTo(name);
238 
239                 mBrailleDisplayConnection.connectLocked(
240                         macAddress, name, BrailleDisplayConnection.BUS_BLUETOOTH, controller);
241 
242                 verify(controller).onConnected(eq(mBrailleDisplayConnection), eq(descriptor));
243             } finally {
244                 mBrailleDisplayConnection.disconnect();
245             }
246         }
247 
248         // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
249         // test Braille display data, but its own implementation should also be tested
250         // so that issues in this helper don't cause confusing failures in CTS.
251 
252         @Test
setTestData_scannerReturnsTestData()253         public void setTestData_scannerReturnsTestData() {
254             Bundle bd1 = new Bundle(), bd2 = new Bundle();
255 
256             Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2");
257             bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH,
258                     path1.toString());
259             bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH,
260                     path2.toString());
261             byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF};
262             bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1);
263             bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2);
264             String uniq1 = "uniq1", uniq2 = "uniq2";
265             bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
266             bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
267             String name1 = "name1", name2 = "name2";
268             bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME, name1);
269             bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME, name2);
270             int bus1 = BrailleDisplayConnection.BUS_USB, bus2 =
271                     BrailleDisplayConnection.BUS_BLUETOOTH;
272             bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
273                     bus1 == BrailleDisplayConnection.BUS_BLUETOOTH);
274             bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
275                     bus2 == BrailleDisplayConnection.BUS_BLUETOOTH);
276 
277             BrailleDisplayConnection.BrailleDisplayScanner scanner =
278                     mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));
279 
280             expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2);
281             expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
282             expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
283             expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
284             expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
285             expect.that(scanner.getName(path1)).isEqualTo(name1);
286             expect.that(scanner.getName(path2)).isEqualTo(name2);
287             expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
288             expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
289         }
290 
291         @Test
setTestData_emptyTestData_returnsNullNodePaths()292         public void setTestData_emptyTestData_returnsNullNodePaths() {
293             BrailleDisplayConnection.BrailleDisplayScanner scanner =
294                     mBrailleDisplayConnection.setTestData(List.of());
295 
296             expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull();
297         }
298     }
299 
300     @RunWith(Parameterized.class)
301     public static class BrailleDisplayDescriptorTest {
302         @Parameterized.Parameters(name = "{0}")
data()303         public static Collection<Object[]> data() {
304             return Arrays.asList(new Object[][]{
305                     {"match_BdPage", new byte[]{
306                             // Just one item, defines the BD page
307                             0x05, 0x41}},
308                     {"match_BdPageAfterAnotherPage", new byte[]{
309                             // One item defines another page
310                             0x05, 0x01,
311                             // Next item defines BD page
312                             0x05, 0x41}},
313                     {"match_BdPageAfterSizeZeroItem", new byte[]{
314                             // Size-zero item (last 2 bits are 00)
315                             0x00,
316                             // Next item defines BD page
317                             0x05, 0x41}},
318                     {"match_BdPageAfterSizeOneItem", new byte[]{
319                             // Size-one item (last 2 bits are 01)
320                             0x01, 0x7F,
321                             // Next item defines BD page
322                             0x05, 0x41}},
323                     {"match_BdPageAfterSizeTwoItem", new byte[]{
324                             // Size-two item (last 2 bits are 10)
325                             0x02, 0x7F, 0x7F,
326                             0x05, 0x41}},
327                     {"match_BdPageAfterSizeFourItem", new byte[]{
328                             // Size-four item (last 2 bits are 11)
329                             0x03, 0x7F, 0x7F, 0x7F, 0x7F,
330                             0x05, 0x41}},
331                     {"match_BdPageInBetweenOtherPages", new byte[]{
332                             // One item defines another page
333                             0x05, 0x01,
334                             // Next item defines BD page
335                             0x05, 0x41,
336                             // Next item defines another page
337                             0x05, 0x02}},
338                     {"fail_OtherPage", new byte[]{
339                             // Just one item, defines another page
340                             0x05, 0x01}},
341                     {"fail_BdPageBeforeMissingData", new byte[]{
342                             // This item defines BD page
343                             0x05, 0x41,
344                             // Next item specifies size-one item (last 2 bits are 01) but
345                             // that one data byte is missing; this descriptor is malformed.
346                             0x01}},
347                     {"fail_BdPageWithWrongDataSize", new byte[]{
348                             // This item defines a page with two-byte ID 0x41 0x7F, not 0x41.
349                             0x06, 0x41, 0x7F}},
350                     {"fail_LongItem", new byte[]{
351                             // Item has type bits 1111, indicating Long Item.
352                             (byte) 0xF0}},
353             });
354         }
355 
356 
357         @Parameterized.Parameter(0)
358         public String mTestName;
359         @Parameterized.Parameter(1)
360         public byte[] mDescriptor;
361 
362         @Test
isBrailleDisplay()363         public void isBrailleDisplay() {
364             final boolean expectedMatch = mTestName.startsWith("match_");
365             assertWithMessage(
366                     "Expected isBrailleDisplay==" + expectedMatch
367                             + " for descriptor " + HexDump.toHexString(mDescriptor))
368                     .that(BrailleDisplayConnection.isBrailleDisplay(mDescriptor))
369                     .isEqualTo(expectedMatch);
370         }
371     }
372 }
373