297 lines
12 KiB
Java
297 lines
12 KiB
Java
![]() |
/*
|
||
|
* 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.
|
||
|
*
|
||
|
* <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
|
||
|
@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.
|
||
|
*
|
||
|
* <p>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.
|
||
|
*
|
||
|
* <p>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<IAccessibilityServiceConnection>
|
||
|
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}.
|
||
|
*
|
||
|
* <p>Messages from system_server are routed to the {@link BrailleDisplayCallback} callbacks
|
||
|
* implemented by the accessibility service.
|
||
|
*
|
||
|
* <p>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;
|
||
|
}
|
||
|
|
||
|
}
|