Loading services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java +57 −3 Original line number Diff line number Diff line Loading @@ -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; } /** Loading services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java +235 −146 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; /** Loading @@ -51,7 +59,10 @@ import java.util.List; * * <p>Prefer adding new tests in CTS where possible. */ @RunWith(Enclosed.class) public class BrailleDisplayConnectionTest { public static class ScannerTest { private static final Path NULL_PATH = Path.of("/dev/null"); private BrailleDisplayConnection mBrailleDisplayConnection; Loading Loading @@ -108,7 +119,8 @@ public class BrailleDisplayConnectionTest { int descriptorSize = 4; byte[] descriptor = {0xB, 0xE, 0xE, 0xF}; when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize); when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor); when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn( descriptor); BrailleDisplayConnection.BrailleDisplayScanner scanner = mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); Loading Loading @@ -198,15 +210,18 @@ public class BrailleDisplayConnectionTest { 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()); 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; 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, Loading @@ -232,3 +247,77 @@ public class BrailleDisplayConnectionTest { expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull(); } } @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}}, }); } @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); } } } Loading
services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java +57 −3 Original line number Diff line number Diff line Loading @@ -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; } /** Loading
services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java +235 −146 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; /** Loading @@ -51,7 +59,10 @@ import java.util.List; * * <p>Prefer adding new tests in CTS where possible. */ @RunWith(Enclosed.class) public class BrailleDisplayConnectionTest { public static class ScannerTest { private static final Path NULL_PATH = Path.of("/dev/null"); private BrailleDisplayConnection mBrailleDisplayConnection; Loading Loading @@ -108,7 +119,8 @@ public class BrailleDisplayConnectionTest { int descriptorSize = 4; byte[] descriptor = {0xB, 0xE, 0xE, 0xF}; when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize); when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor); when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn( descriptor); BrailleDisplayConnection.BrailleDisplayScanner scanner = mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); Loading Loading @@ -198,15 +210,18 @@ public class BrailleDisplayConnectionTest { 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()); 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; 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, Loading @@ -232,3 +247,77 @@ public class BrailleDisplayConnectionTest { expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull(); } } @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}}, }); } @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); } } }