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