diff options
2 files changed, 292 insertions, 149 deletions
diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java index 9b27dd347caf..40b6ff01965e 100644 --- a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java @@ -232,9 +232,63 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub { } /** Returns true if this descriptor includes usages for the Braille display usage page 0x41. */ - private static boolean isBrailleDisplay(byte[] descriptor) { - // TODO: b/316036493 - Check that descriptor includes 0x41 reports. - return true; + @VisibleForTesting + static boolean isBrailleDisplay(byte[] descriptor) { + boolean foundMatch = false; + for (int i = 0; i < descriptor.length; i++) { + // HID Spec "6.2.2.2 Short Items" defines that the report descriptor is a collection of + // items: each item is a collection of bytes where the first byte defines info about + // the type of item and the following 0, 1, 2, or 4 bytes are data bytes for that item. + // All items in the HID descriptor are expected to be Short Items. + final byte itemInfo = descriptor[i]; + if (!isHidItemShort(itemInfo)) { + Slog.w(LOG_TAG, "Item " + itemInfo + " declares unsupported long type"); + return false; + } + final int dataSize = getHidItemDataSize(itemInfo); + if (i + dataSize >= descriptor.length) { + Slog.w(LOG_TAG, "Item " + itemInfo + " specifies size past the remaining bytes"); + return false; + } + // The item we're looking for (usage page declaration) should have size 1. + if (dataSize == 1) { + final byte itemData = descriptor[i + 1]; + if (isHidItemBrailleDisplayUsagePage(itemInfo, itemData)) { + foundMatch = true; + } + } + // Move to the next item by skipping past all data bytes in this item. + i += dataSize; + } + return foundMatch; + } + + private static boolean isHidItemShort(byte itemInfo) { + // Info bits 7-4 describe the item type, and HID Spec "6.2.2.3 Long Items" says that long + // items always have type bits 1111. Otherwise, the item is a short item. + return (itemInfo & 0b1111_0000) != 0b1111_0000; + } + + private static int getHidItemDataSize(byte itemInfo) { + // HID Spec "6.2.2.2 Short Items" says that info bits 0-1 specify the optional data size: + // 0, 1, 2, or 4 bytes. + return switch (itemInfo & 0b0000_0011) { + case 0b00 -> 0; + case 0b01 -> 1; + case 0b10 -> 2; + default -> 4; + }; + } + + private static boolean isHidItemBrailleDisplayUsagePage(byte itemInfo, byte itemData) { + // From HID Spec "6.2.2.7 Global Items" + final byte usagePageType = 0b0000_0100; + // From HID Usage Tables version 1.2. + final byte brailleDisplayUsagePage = 0x41; + // HID Spec "6.2.2.2 Short Items" says item info bits 2-7 describe the type and + // function of the item. + final byte itemType = (byte) (itemInfo & 0b1111_1100); + return itemType == usagePageType && itemData == brailleDisplayUsagePage; } /** diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java index b322dd709c2d..aec3f451fac6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java @@ -17,6 +17,7 @@ package com.android.server.accessibility; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.anyInt; @@ -33,17 +34,24 @@ import android.testing.DexmakerShareClassLoaderRule; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.internal.util.HexDump; + import com.google.common.truth.Expect; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.io.File; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; import java.util.List; /** @@ -51,184 +59,265 @@ import java.util.List; * * <p>Prefer adding new tests in CTS where possible. */ +@RunWith(Enclosed.class) public class BrailleDisplayConnectionTest { - private static final Path NULL_PATH = Path.of("/dev/null"); - - private BrailleDisplayConnection mBrailleDisplayConnection; - @Mock - private BrailleDisplayConnection.NativeInterface mNativeInterface; - @Mock - private AccessibilityServiceConnection mServiceConnection; - - @Rule - public final Expect expect = Expect.create(); - - private Context mContext; - - // To mock package-private class - @Rule - public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = - new DexmakerShareClassLoaderRule(); - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mContext = InstrumentationRegistry.getInstrumentation().getContext(); - when(mServiceConnection.isConnectedLocked()).thenReturn(true); - mBrailleDisplayConnection = - spy(new BrailleDisplayConnection(new Object(), mServiceConnection)); - } - @Test - public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception { - File testDir = mContext.getFilesDir(); - Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0"); - Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1"); - Path otherDevice = Path.of(testDir.getPath(), "otherDevice"); - Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice}; - try { - for (Path node : nodePaths) { - assertThat(node.toFile().createNewFile()).isTrue(); + public static class ScannerTest { + private static final Path NULL_PATH = Path.of("/dev/null"); + + private BrailleDisplayConnection mBrailleDisplayConnection; + @Mock + private BrailleDisplayConnection.NativeInterface mNativeInterface; + @Mock + private AccessibilityServiceConnection mServiceConnection; + + @Rule + public final Expect expect = Expect.create(); + + private Context mContext; + + // To mock package-private class + @Rule + public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = + new DexmakerShareClassLoaderRule(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + when(mServiceConnection.isConnectedLocked()).thenReturn(true); + mBrailleDisplayConnection = + spy(new BrailleDisplayConnection(new Object(), mServiceConnection)); + } + + @Test + public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception { + File testDir = mContext.getFilesDir(); + Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0"); + Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1"); + Path otherDevice = Path.of(testDir.getPath(), "otherDevice"); + Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice}; + try { + for (Path node : nodePaths) { + assertThat(node.toFile().createNewFile()).isTrue(); + } + + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + + assertThat(scanner.getHidrawNodePaths(testDir.toPath())) + .containsExactly(hidrawNode0, hidrawNode1); + } finally { + for (Path node : nodePaths) { + node.toFile().delete(); + } } + } + + @Test + public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() { + int descriptorSize = 4; + byte[] descriptor = {0xB, 0xE, 0xE, 0xF}; + when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize); + when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn( + descriptor); BrailleDisplayConnection.BrailleDisplayScanner scanner = mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - assertThat(scanner.getHidrawNodePaths(testDir.toPath())) - .containsExactly(hidrawNode0, hidrawNode1); - } finally { - for (Path node : nodePaths) { - node.toFile().delete(); - } + assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor); } - } - @Test - public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() { - int descriptorSize = 4; - byte[] descriptor = {0xB, 0xE, 0xE, 0xF}; - when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize); - when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor); + @Test + public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() { + when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0); - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor); - } + assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull(); + } - @Test - public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() { - when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0); + @Test + public void defaultNativeScanner_getUniqueId_returnsUniq() { + String macAddress = "12:34:56:78"; + when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress); - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull(); - } + assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress); + } + + @Test + public void defaultNativeScanner_getDeviceBusType_busUsb() { + when(mNativeInterface.getHidrawBusType(anyInt())) + .thenReturn(BrailleDisplayConnection.BUS_USB); - @Test - public void defaultNativeScanner_getUniqueId_returnsUniq() { - String macAddress = "12:34:56:78"; - when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress); + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + assertThat(scanner.getDeviceBusType(NULL_PATH)) + .isEqualTo(BrailleDisplayConnection.BUS_USB); + } - assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress); - } + @Test + public void defaultNativeScanner_getDeviceBusType_busBluetooth() { + when(mNativeInterface.getHidrawBusType(anyInt())) + .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH); - @Test - public void defaultNativeScanner_getDeviceBusType_busUsb() { - when(mNativeInterface.getHidrawBusType(anyInt())) - .thenReturn(BrailleDisplayConnection.BUS_USB); + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + assertThat(scanner.getDeviceBusType(NULL_PATH)) + .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH); + } - assertThat(scanner.getDeviceBusType(NULL_PATH)) - .isEqualTo(BrailleDisplayConnection.BUS_USB); - } + @Test + public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() { + Mockito.doNothing().when(mBrailleDisplayConnection).disconnect(); + mBrailleDisplayConnection.write( + new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]); - @Test - public void defaultNativeScanner_getDeviceBusType_busBluetooth() { - when(mNativeInterface.getHidrawBusType(anyInt())) - .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH); + verify(mBrailleDisplayConnection).disconnect(); + } - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + @Test + public void write_notConnected_throwsIllegalStateException() { + when(mServiceConnection.isConnectedLocked()).thenReturn(false); - assertThat(scanner.getDeviceBusType(NULL_PATH)) - .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH); - } + assertThrows(IllegalStateException.class, + () -> mBrailleDisplayConnection.write(new byte[1])); + } - @Test - public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() { - Mockito.doNothing().when(mBrailleDisplayConnection).disconnect(); - mBrailleDisplayConnection.write( - new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]); + @Test + public void write_unableToCreateWriteStream_disconnects() { + Mockito.doNothing().when(mBrailleDisplayConnection).disconnect(); + // mBrailleDisplayConnection#connectLocked was never called so the + // connection's mHidrawNode is still null. This will throw an exception + // when attempting to create FileOutputStream on the node. + mBrailleDisplayConnection.write(new byte[1]); - verify(mBrailleDisplayConnection).disconnect(); - } + verify(mBrailleDisplayConnection).disconnect(); + } - @Test - public void write_notConnected_throwsIllegalStateException() { - when(mServiceConnection.isConnectedLocked()).thenReturn(false); + // BrailleDisplayConnection#setTestData() is used to enable CTS testing with + // test Braille display data, but its own implementation should also be tested + // so that issues in this helper don't cause confusing failures in CTS. + + @Test + public void setTestData_scannerReturnsTestData() { + Bundle bd1 = new Bundle(), bd2 = new Bundle(); + + Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2"); + bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, + path1.toString()); + bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, + path2.toString()); + byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF}; + bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1); + bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2); + String uniq1 = "uniq1", uniq2 = "uniq2"; + bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1); + bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2); + int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = + BrailleDisplayConnection.BUS_BLUETOOTH; + bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, + bus1 == BrailleDisplayConnection.BUS_BLUETOOTH); + bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, + bus2 == BrailleDisplayConnection.BUS_BLUETOOTH); - assertThrows(IllegalStateException.class, - () -> mBrailleDisplayConnection.write(new byte[1])); - } + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.setTestData(List.of(bd1, bd2)); + + expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2); + expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1); + expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2); + expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1); + expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2); + expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1); + expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2); + } - @Test - public void write_unableToCreateWriteStream_disconnects() { - Mockito.doNothing().when(mBrailleDisplayConnection).disconnect(); - // mBrailleDisplayConnection#connectLocked was never called so the - // connection's mHidrawNode is still null. This will throw an exception - // when attempting to create FileOutputStream on the node. - mBrailleDisplayConnection.write(new byte[1]); + @Test + public void setTestData_emptyTestData_returnsNullNodePaths() { + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.setTestData(List.of()); - verify(mBrailleDisplayConnection).disconnect(); + expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull(); + } } - // BrailleDisplayConnection#setTestData() is used to enable CTS testing with - // test Braille display data, but its own implementation should also be tested - // so that issues in this helper don't cause confusing failures in CTS. - - @Test - public void setTestData_scannerReturnsTestData() { - Bundle bd1 = new Bundle(), bd2 = new Bundle(); - - Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2"); - bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path1.toString()); - bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path2.toString()); - byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF}; - bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1); - bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2); - String uniq1 = "uniq1", uniq2 = "uniq2"; - bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1); - bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2); - int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = BrailleDisplayConnection.BUS_BLUETOOTH; - bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, - bus1 == BrailleDisplayConnection.BUS_BLUETOOTH); - bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, - bus2 == BrailleDisplayConnection.BUS_BLUETOOTH); - - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.setTestData(List.of(bd1, bd2)); - - expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2); - expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1); - expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2); - expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1); - expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2); - expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1); - expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2); - } + @RunWith(Parameterized.class) + public static class BrailleDisplayDescriptorTest { + @Parameterized.Parameters(name = "{0}") + public static Collection<Object[]> data() { + return Arrays.asList(new Object[][]{ + {"match_BdPage", new byte[]{ + // Just one item, defines the BD page + 0x05, 0x41}}, + {"match_BdPageAfterAnotherPage", new byte[]{ + // One item defines another page + 0x05, 0x01, + // Next item defines BD page + 0x05, 0x41}}, + {"match_BdPageAfterSizeZeroItem", new byte[]{ + // Size-zero item (last 2 bits are 00) + 0x00, + // Next item defines BD page + 0x05, 0x41}}, + {"match_BdPageAfterSizeOneItem", new byte[]{ + // Size-one item (last 2 bits are 01) + 0x01, 0x7F, + // Next item defines BD page + 0x05, 0x41}}, + {"match_BdPageAfterSizeTwoItem", new byte[]{ + // Size-two item (last 2 bits are 10) + 0x02, 0x7F, 0x7F, + 0x05, 0x41}}, + {"match_BdPageAfterSizeFourItem", new byte[]{ + // Size-four item (last 2 bits are 11) + 0x03, 0x7F, 0x7F, 0x7F, 0x7F, + 0x05, 0x41}}, + {"match_BdPageInBetweenOtherPages", new byte[]{ + // One item defines another page + 0x05, 0x01, + // Next item defines BD page + 0x05, 0x41, + // Next item defines another page + 0x05, 0x02}}, + {"fail_OtherPage", new byte[]{ + // Just one item, defines another page + 0x05, 0x01}}, + {"fail_BdPageBeforeMissingData", new byte[]{ + // This item defines BD page + 0x05, 0x41, + // Next item specifies size-one item (last 2 bits are 01) but + // that one data byte is missing; this descriptor is malformed. + 0x01}}, + {"fail_BdPageWithWrongDataSize", new byte[]{ + // This item defines a page with two-byte ID 0x41 0x7F, not 0x41. + 0x06, 0x41, 0x7F}}, + {"fail_LongItem", new byte[]{ + // Item has type bits 1111, indicating Long Item. + (byte) 0xF0}}, + }); + } - @Test - public void setTestData_emptyTestData_returnsNullNodePaths() { - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.setTestData(List.of()); - expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull(); + @Parameterized.Parameter(0) + public String mTestName; + @Parameterized.Parameter(1) + public byte[] mDescriptor; + + @Test + public void isBrailleDisplay() { + final boolean expectedMatch = mTestName.startsWith("match_"); + assertWithMessage( + "Expected isBrailleDisplay==" + expectedMatch + + " for descriptor " + HexDump.toHexString(mDescriptor)) + .that(BrailleDisplayConnection.isBrailleDisplay(mDescriptor)) + .isEqualTo(expectedMatch); + } } } |