/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.accessibilityservice; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.bluetooth.BluetoothDevice; 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; import java.util.Objects; 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) @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. * *

Defaults to true. * *

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 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull BluetoothDevice bluetoothDevice, @NonNull BrailleDisplayCallback callback) { connect(bluetoothDevice, mAccessibilityService.getMainExecutor(), callback); } @Override @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull BluetoothDevice bluetoothDevice, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull BrailleDisplayCallback callback) { Objects.requireNonNull(bluetoothDevice); Objects.requireNonNull(callbackExecutor); Objects.requireNonNull(callback); connect(serviceConnection -> serviceConnection.connectBluetoothBrailleDisplay( bluetoothDevice.getAddress(), new IBrailleDisplayControllerWrapper()), callbackExecutor, callback); } @Override public void connect(@NonNull UsbDevice usbDevice, @NonNull BrailleDisplayCallback callback) { connect(usbDevice, mAccessibilityService.getMainExecutor(), callback); } @Override public void connect(@NonNull UsbDevice usbDevice, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull BrailleDisplayCallback callback) { Objects.requireNonNull(usbDevice); Objects.requireNonNull(callbackExecutor); Objects.requireNonNull(callback); connect(serviceConnection -> serviceConnection.connectUsbBrailleDisplay( usbDevice, new IBrailleDisplayControllerWrapper()), callbackExecutor, callback); } /** * Shared implementation for the {@code connect()} API methods. * *

Performs a blocking call to system_server to create the connection. Success is * returned through {@link BrailleDisplayCallback#onConnected} while normal connection * errors are returned through {@link BrailleDisplayCallback#onConnectionFailed}. This * connection is implemented using cached data from the HIDRAW driver so it returns * quickly without needing to perform any I/O with the Braille display. * *

The AIDL call to system_server is blocking (not posted to a handler thread) so * that runtime exceptions signaling abnormal connection errors from API misuse * (e.g. lacking permissions, providing an invalid BluetoothDevice, calling connect * while already connected) are propagated to the API caller. */ private void connect( FunctionalUtils.RemoteExceptionIgnoringConsumer 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"); } final IAccessibilityServiceConnection serviceConnection = AccessibilityInteractionClient.getConnection( mAccessibilityService.getConnectionId()); if (serviceConnection == null) { throw new IllegalStateException("Accessibility service is not connected"); } synchronized (mLock) { mCallbackExecutor = callbackExecutor; mCallback = callback; } try { createConnection.acceptOrThrow(serviceConnection); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } @Override public boolean isConnected() { BrailleDisplayController.checkApiFlagIsEnabled(); return mBrailleDisplayConnection != null; } @Override public void write(@NonNull byte[] buffer) throws IOException { BrailleDisplayController.checkApiFlagIsEnabled(); Objects.requireNonNull(buffer); if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) { // This same check must be performed in the system to prevent reflection misuse, // but perform it here too to prevent unnecessary IPCs from non-reflection callers. throw new IllegalArgumentException("Invalid write buffer size " + buffer.length); } synchronized (mLock) { if (mBrailleDisplayConnection == null) { throw new IOException("Braille display is not connected"); } try { mBrailleDisplayConnection.write(buffer); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } @Override public void disconnect() { BrailleDisplayController.checkApiFlagIsEnabled(); synchronized (mLock) { try { if (mBrailleDisplayConnection != null) { mBrailleDisplayConnection.disconnect(); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } finally { clearConnectionLocked(); } } } /** * Implementation of the {@code IBrailleDisplayController} AIDL interface provided to * system_server, which system_server uses to pass messages back to this * {@code BrailleDisplayController}. * *

Messages from system_server are routed to the {@link BrailleDisplayCallback} callbacks * implemented by the accessibility service. * *

Note: Per API Guidelines 7.5 the Binder identity must be cleared before invoking the * callback executor so that Binder identity checks in the callbacks are performed using the * app's identity. */ private final class IBrailleDisplayControllerWrapper extends IBrailleDisplayController.Stub { /** * Called when the system successfully connects to a Braille display. */ @Override public void onConnected(IBrailleDisplayConnection connection, byte[] hidDescriptor) { BrailleDisplayController.checkApiFlagIsEnabled(); final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { mBrailleDisplayConnection = connection; mCallbackExecutor.execute(() -> mCallback.onConnected(hidDescriptor)); } } finally { Binder.restoreCallingIdentity(identity); } } /** * Called when the system is unable to connect to a Braille display. */ @Override public void onConnectionFailed(@BrailleDisplayCallback.ErrorCode int errorCode) { BrailleDisplayController.checkApiFlagIsEnabled(); final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { mCallbackExecutor.execute(() -> mCallback.onConnectionFailed(errorCode)); } } finally { Binder.restoreCallingIdentity(identity); } } /** * Called when input is received from the currently connected Braille display. */ @Override public void onInput(byte[] input) { BrailleDisplayController.checkApiFlagIsEnabled(); final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { // Ignore input that arrives after disconnection. if (mBrailleDisplayConnection != null) { mCallbackExecutor.execute(() -> mCallback.onInput(input)); } } } finally { Binder.restoreCallingIdentity(identity); } } /** * Called when the currently connected Braille display is disconnected. */ @Override public void onDisconnected() { BrailleDisplayController.checkApiFlagIsEnabled(); final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { mCallbackExecutor.execute(mCallback::onDisconnected); clearConnectionLocked(); } } finally { Binder.restoreCallingIdentity(identity); } } } private void clearConnectionLocked() { mBrailleDisplayConnection = null; } }