diff --git a/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java index cac1dc4e04a9b918def14b3eefbce9e2a7c85d72..f1df33657279709b4bc17b7a1e0ad37a325e8bab 100644 --- a/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java +++ b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java @@ -25,9 +25,11 @@ import android.hardware.usb.UsbDevice; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.os.SystemProperties; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.Flags; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FunctionalUtils; import java.io.IOException; @@ -36,24 +38,46 @@ import java.util.concurrent.Executor; /** * Default implementation of {@link BrailleDisplayController}. + * + * @hide */ // BrailleDisplayControllerImpl is not an API, but it implements BrailleDisplayController APIs. // This @FlaggedApi annotation tells the linter that this method delegates API checks to its // callers. @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) -final class BrailleDisplayControllerImpl implements BrailleDisplayController { +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public final class BrailleDisplayControllerImpl implements BrailleDisplayController { private final AccessibilityService mAccessibilityService; private final Object mLock; + private final boolean mIsHidrawSupported; private IBrailleDisplayConnection mBrailleDisplayConnection; private Executor mCallbackExecutor; private BrailleDisplayCallback mCallback; + /** + * Read-only property that returns whether HIDRAW access is supported on this device. + * + * <p>Defaults to true. + * + * <p>Device manufacturers without HIDRAW kernel support can set this to false in + * the device's product makefile. + */ + private static final boolean IS_HIDRAW_SUPPORTED = SystemProperties.getBoolean( + "ro.accessibility.support_hidraw", true); + BrailleDisplayControllerImpl(AccessibilityService accessibilityService, Object lock) { + this(accessibilityService, lock, IS_HIDRAW_SUPPORTED); + } + + @VisibleForTesting + public BrailleDisplayControllerImpl(AccessibilityService accessibilityService, + Object lock, boolean isHidrawSupported) { mAccessibilityService = accessibilityService; mLock = lock; + mIsHidrawSupported = isHidrawSupported; } @Override @@ -113,6 +137,11 @@ final class BrailleDisplayControllerImpl implements BrailleDisplayController { createConnection, @NonNull Executor callbackExecutor, @NonNull BrailleDisplayCallback callback) { BrailleDisplayController.checkApiFlagIsEnabled(); + if (!mIsHidrawSupported) { + callbackExecutor.execute(() -> callback.onConnectionFailed( + BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS)); + return; + } if (isConnected()) { throw new IllegalStateException( "This service already has a connected Braille display"); diff --git a/core/tests/coretests/src/android/accessibilityservice/BrailleDisplayControllerImplTest.java b/core/tests/coretests/src/android/accessibilityservice/BrailleDisplayControllerImplTest.java index aaa199d4c814e40aa088cdb9716a3a134d1d91d8..e8b295bd5fdb4188729456d1b234840da7c1206a 100644 --- a/core/tests/coretests/src/android/accessibilityservice/BrailleDisplayControllerImplTest.java +++ b/core/tests/coretests/src/android/accessibilityservice/BrailleDisplayControllerImplTest.java @@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import android.hardware.usb.UsbDevice; import android.platform.test.annotations.RequiresFlagsEnabled; @@ -58,6 +59,7 @@ public class BrailleDisplayControllerImplTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private AccessibilityService mAccessibilityService; private BrailleDisplayController mBrailleDisplayController; @Mock @@ -76,12 +78,13 @@ public class BrailleDisplayControllerImplTest { @Before public void test() { MockitoAnnotations.initMocks(this); - AccessibilityService accessibilityService = spy(new TestAccessibilityService()); - doReturn((Executor) Runnable::run).when(accessibilityService).getMainExecutor(); - doReturn(TEST_SERVICE_CONNECTION_ID).when(accessibilityService).getConnectionId(); + mAccessibilityService = spy(new TestAccessibilityService()); + doReturn((Executor) Runnable::run).when(mAccessibilityService).getMainExecutor(); + doReturn(TEST_SERVICE_CONNECTION_ID).when(mAccessibilityService).getConnectionId(); AccessibilityInteractionClient.addConnection(TEST_SERVICE_CONNECTION_ID, mAccessibilityServiceConnection, /*initializeCache=*/false); - mBrailleDisplayController = accessibilityService.getBrailleDisplayController(); + mBrailleDisplayController = new BrailleDisplayControllerImpl( + mAccessibilityService, new Object(), /*isHidrawSupported=*/true); } // Automated CTS tests only use the BluetoothDevice version of BrailleDisplayController#connect @@ -104,4 +107,17 @@ public class BrailleDisplayControllerImplTest { assertThrows(IllegalStateException.class, () -> mBrailleDisplayController.connect(usbDevice, mBrailleDisplayCallback)); } + + @Test + public void connect_HidrawNotSupported_callsOnConnectionFailed() { + BrailleDisplayController controller = new BrailleDisplayControllerImpl( + mAccessibilityService, new Object(), /*isHidrawSupported=*/false); + UsbDevice usbDevice = Mockito.mock(UsbDevice.class); + + controller.connect(usbDevice, mBrailleDisplayCallback); + + verify(mBrailleDisplayCallback).onConnectionFailed( + BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS); + verifyZeroInteractions(mAccessibilityServiceConnection); + } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 43c018cfeea3455cdad3abe29b1ea152e5d277a0..cbb66dc18f2899b36aef38234c6ee7e75bd3a4b5 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -4659,8 +4659,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { - new AccessibilityShellCommand(this, mSystemActionPerformer).exec(this, in, out, err, args, - callback, resultReceiver); + new AccessibilityShellCommand(mContext, this, mSystemActionPerformer) + .exec(this, in, out, err, args, callback, resultReceiver); } private final class InteractionBridge { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java index 8cf5547b05eca523a56b54014aff17556d9c99a0..490803228337b9583c4fbbe9810b785618e42e55 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java @@ -16,8 +16,10 @@ package com.android.server.accessibility; +import android.Manifest; import android.annotation.NonNull; import android.app.ActivityManager; +import android.content.Context; import android.os.Binder; import android.os.Process; import android.os.ShellCommand; @@ -26,18 +28,27 @@ import android.os.UserHandle; import com.android.server.LocalServices; import com.android.server.wm.WindowManagerInternal; +import java.io.File; +import java.io.IOException; import java.io.PrintWriter; /** * Shell command implementation for the accessibility manager service */ final class AccessibilityShellCommand extends ShellCommand { - final @NonNull AccessibilityManagerService mService; - final @NonNull SystemActionPerformer mSystemActionPerformer; - final @NonNull WindowManagerInternal mWindowManagerService; + @NonNull + final Context mContext; + @NonNull + final AccessibilityManagerService mService; + @NonNull + final SystemActionPerformer mSystemActionPerformer; + @NonNull + final WindowManagerInternal mWindowManagerService; - AccessibilityShellCommand(@NonNull AccessibilityManagerService service, + AccessibilityShellCommand(@NonNull Context context, + @NonNull AccessibilityManagerService service, @NonNull SystemActionPerformer systemActionPerformer) { + mContext = context; mService = service; mSystemActionPerformer = systemActionPerformer; mWindowManagerService = LocalServices.getService(WindowManagerInternal.class); @@ -61,6 +72,8 @@ final class AccessibilityShellCommand extends ShellCommand { case "start-trace": case "stop-trace": return mService.getTraceManager().onShellCommand(cmd, this); + case "check-hidraw": + return checkHidraw(); } return -1; } @@ -106,6 +119,67 @@ final class AccessibilityShellCommand extends ShellCommand { return -1; } + private int checkHidraw() { + mContext.enforceCallingPermission(Manifest.permission.MANAGE_ACCESSIBILITY, + "Missing MANAGE_ACCESSIBILITY permission"); + String subcommand = getNextArgRequired(); + File hidrawNode = new File(getNextArgRequired()); + switch (subcommand) { + case "read" -> { + return checkHidrawRead(hidrawNode); + } + case "write" -> { + return checkHidrawWrite(hidrawNode); + } + case "descriptor" -> { + return checkHidrawDescriptor(hidrawNode); + } + default -> { + getErrPrintWriter().print("Unknown subcommand " + subcommand); + return -1; + } + } + } + + private int checkHidrawRead(File hidrawNode) { + if (!hidrawNode.canRead()) { + getErrPrintWriter().println("Unable to read from " + hidrawNode); + return -1; + } + // Tests executing this command using UiAutomation#executeShellCommand will not receive + // the command's exit value, so print the path to stdout to indicate success. + getOutPrintWriter().print(hidrawNode.getAbsolutePath()); + return 0; + } + + private int checkHidrawWrite(File hidrawNode) { + if (!hidrawNode.canWrite()) { + getErrPrintWriter().println("Unable to write to " + hidrawNode); + return -1; + } + // Tests executing this command using UiAutomation#executeShellCommand will not receive + // the command's exit value, so print the path to stdout to indicate success. + getOutPrintWriter().print(hidrawNode.getAbsolutePath()); + return 0; + } + + private int checkHidrawDescriptor(File hidrawNode) { + BrailleDisplayConnection.BrailleDisplayScanner scanner = + BrailleDisplayConnection.createScannerForShell(); + byte[] descriptor = scanner.getDeviceReportDescriptor(hidrawNode.toPath()); + if (descriptor == null) { + getErrPrintWriter().println("Unable to read descriptor for " + hidrawNode); + return -1; + } + try { + // Print the descriptor bytes to stdout. + getRawOutputStream().write(descriptor); + return 0; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private Integer parseUserId() { final String option = getNextOption(); if (option != null) { @@ -131,6 +205,8 @@ final class AccessibilityShellCommand extends ShellCommand { pw.println(" Get whether binding to services provided by instant apps is allowed."); pw.println(" call-system-action <ACTION_ID>"); pw.println(" Calls the system action with the given action id."); + pw.println(" check-hidraw [read|write|descriptor] <HIDRAW_NODE_PATH>"); + pw.println(" Checks if the system can perform various actions on the HIDRAW node."); mService.getTraceManager().onHelp(pw); } } diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java index 40b6ff01965e6539a900f9bb97699ba0b065ec59..8b41873636a9f018e7dea42dcd7c069287031c88 100644 --- a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java @@ -115,6 +115,13 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub { this.mServiceConnection = Objects.requireNonNull(serviceConnection); } + /** + * Used for `cmd accessibility` to check hidraw access. + */ + static BrailleDisplayScanner createScannerForShell() { + return getDefaultNativeScanner(new DefaultNativeInterface()); + } + /** * Interface to scan for properties of connected Braille displays. * @@ -125,7 +132,6 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub { * @see #getDefaultNativeScanner * @see #setTestData */ - @VisibleForTesting interface BrailleDisplayScanner { Collection<Path> getHidrawNodePaths(@NonNull Path directory); @@ -441,7 +447,7 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub { * from HIDRAW nodes and perform ioctls using the provided {@link NativeInterface}. */ @VisibleForTesting - BrailleDisplayScanner getDefaultNativeScanner(@NonNull NativeInterface nativeInterface) { + static BrailleDisplayScanner getDefaultNativeScanner(@NonNull NativeInterface nativeInterface) { Objects.requireNonNull(nativeInterface); return new BrailleDisplayScanner() { private static final String HIDRAW_DEVICE_GLOB = "hidraw*"; @@ -576,7 +582,7 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub { } /** Native interface that actually calls native HIDRAW ioctls. */ - private class DefaultNativeInterface implements NativeInterface { + private static class DefaultNativeInterface implements NativeInterface { @Override public int getHidrawDescSize(int fd) { return nativeGetHidrawDescSize(fd); @@ -598,11 +604,11 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub { } } - private native int nativeGetHidrawDescSize(int fd); + private static native int nativeGetHidrawDescSize(int fd); - private native byte[] nativeGetHidrawDesc(int fd, int descSize); + private static native byte[] nativeGetHidrawDesc(int fd, int descSize); - private native String nativeGetHidrawUniq(int fd); + private static native String nativeGetHidrawUniq(int fd); - private native int nativeGetHidrawBusType(int fd); + private static native int nativeGetHidrawBusType(int fd); } diff --git a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp index 9a509a71cf9204c8fae78f3d77b32f09e9f26005..c3375236098a768e678871f17696ec6ef36ad370 100644 --- a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp +++ b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp @@ -40,7 +40,7 @@ constexpr int UNIQ_SIZE_MAX = 64; } // anonymous namespace static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize( - JNIEnv* env, jobject thiz, int fd) { + JNIEnv* env, jclass /*clazz*/, int fd) { int size = 0; if (ioctl(fd, HIDIOCGRDESCSIZE, &size) < 0) { return -1; @@ -49,7 +49,7 @@ static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawD } static jbyteArray com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc( - JNIEnv* env, jobject thiz, int fd, int descSize) { + JNIEnv* env, jclass /*clazz*/, int fd, int descSize) { struct hidraw_report_descriptor desc; desc.size = descSize; if (ioctl(fd, HIDIOCGRDESC, &desc) < 0) { @@ -63,9 +63,8 @@ static jbyteArray com_android_server_accessibility_BrailleDisplayConnection_getH return result; } -static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq(JNIEnv* env, - jobject thiz, - int fd) { +static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq( + JNIEnv* env, jclass /*clazz*/, int fd) { char buf[UNIQ_SIZE_MAX]; if (ioctl(fd, HIDIOCGRAWUNIQ(UNIQ_SIZE_MAX), buf) < 0) { return nullptr; @@ -74,9 +73,8 @@ static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidr return env->NewStringUTF(buf); } -static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType(JNIEnv* env, - jobject thiz, - int fd) { +static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType( + JNIEnv* env, jclass /*clazz*/, int fd) { struct hidraw_devinfo info; if (ioctl(fd, HIDIOCGRAWINFO, &info) < 0) { return -1; 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 aec3f451fac69f8a46abb5b0ce95f3a9fb5453dc..344e2c21f0a557a7afa7e1dd03fc68a76ff8fae1 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java @@ -103,7 +103,7 @@ public class BrailleDisplayConnectionTest { } BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); assertThat(scanner.getHidrawNodePaths(testDir.toPath())) .containsExactly(hidrawNode0, hidrawNode1); @@ -123,7 +123,7 @@ public class BrailleDisplayConnectionTest { descriptor); BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor); } @@ -133,7 +133,7 @@ public class BrailleDisplayConnectionTest { when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0); BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull(); } @@ -144,7 +144,7 @@ public class BrailleDisplayConnectionTest { when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress); BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress); } @@ -155,7 +155,7 @@ public class BrailleDisplayConnectionTest { .thenReturn(BrailleDisplayConnection.BUS_USB); BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); assertThat(scanner.getDeviceBusType(NULL_PATH)) .isEqualTo(BrailleDisplayConnection.BUS_USB); @@ -167,7 +167,7 @@ public class BrailleDisplayConnectionTest { .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH); BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); assertThat(scanner.getDeviceBusType(NULL_PATH)) .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);