/* * 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 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;
}
}