/* * 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 com.android.server.accessibility; import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND; import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS; import android.accessibilityservice.BrailleDisplayController; import android.accessibilityservice.IBrailleDisplayConnection; import android.accessibilityservice.IBrailleDisplayController; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.PermissionManuallyEnforced; import android.annotation.RequiresNoPermission; import android.bluetooth.BluetoothDevice; import android.hardware.usb.UsbDevice; import android.os.Bundle; import android.os.HandlerThread; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; /** * This class represents the connection between {@code system_server} and a connected * Braille Display using the Braille Display HID standard (usage page 0x41). */ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub { private static final String LOG_TAG = "BrailleDisplayConnection"; /** * Represents the connection type of a Braille display. * *
The integer values must match the kernel's bus type values because this bus type is
* used to locate the correct HIDRAW node using data from the kernel. These values come
* from the UAPI header file bionic/libc/kernel/uapi/linux/input.h, which is guaranteed
* to stay constant.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, prefix = {"BUS_"}, value = {
BUS_UNKNOWN,
BUS_USB,
BUS_BLUETOOTH
})
@interface BusType {
}
static final int BUS_UNKNOWN = -1;
static final int BUS_USB = 0x03;
static final int BUS_BLUETOOTH = 0x05;
// Access to this static object must be guarded by a lock that is shared for all instances
// of this class: the singular Accessibility system_server lock (mLock).
private static final Set Helps simplify testing Braille Display APIs using test data without requiring
* a real Braille display to be connected to the device, by using a test implementation
* of this interface.
*
* @see #getDefaultNativeScanner
* @see #setTestData
*/
interface BrailleDisplayScanner {
Collection If found, saves instance state for this connection and starts a thread to
* read from the Braille display.
*
* @param expectedUniqueId The expected unique ID of the device to connect, from
* {@link UsbDevice#getSerialNumber()} or
* {@link BluetoothDevice#getAddress()}.
* @param expectedName The expected name of the device to connect, from
* {@link BluetoothDevice#getName()} or
* {@link UsbDevice#getProductName()}.
* @param expectedBusType The expected bus type from {@link BusType}.
* @param controller Interface containing oneway callbacks used to communicate with the
* {@link android.accessibilityservice.BrailleDisplayController}.
*/
void connectLocked(
@NonNull String expectedUniqueId,
@Nullable String expectedName,
@BusType int expectedBusType,
@NonNull IBrailleDisplayController controller) {
Objects.requireNonNull(expectedUniqueId);
this.mController = Objects.requireNonNull(controller);
final Path devicePath = Path.of("/dev");
final List Writes are posted to a background thread handler.
*
* @param buffer The bytes to write to the Braille display. These bytes should be formatted
* according to the report descriptor.
*/
@Override
@PermissionManuallyEnforced // by assertServiceIsConnectedLocked()
public void write(@NonNull byte[] buffer) {
Objects.requireNonNull(buffer);
if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
Slog.e(LOG_TAG, "Requested write of size " + buffer.length
+ " which is larger than maximum " + IBinder.getSuggestedMaxIpcSizeBytes());
// The caller only got here by bypassing the AccessibilityService-side check with
// reflection, so disconnect this connection to prevent further attempts.
disconnect();
return;
}
synchronized (mLock) {
assertServiceIsConnectedLocked();
if (mOutputThread == null) {
try {
mOutputStream = new FileOutputStream(mHidrawNode);
} catch (Exception e) {
Slog.e(LOG_TAG, "Unable to create write stream", e);
disconnect();
return;
}
mOutputThread = new HandlerThread("BrailleDisplayConnection output thread",
Process.THREAD_PRIORITY_BACKGROUND);
mOutputThread.setDaemon(true);
mOutputThread.start();
}
// TODO: b/316035785 - Proactively disconnect a misbehaving Braille display by calling
// disconnect() if the mOutputThread handler queue grows too large.
mOutputThread.getThreadHandler().post(() -> {
try {
mOutputStream.write(buffer);
} catch (IOException e) {
Slog.d(LOG_TAG, "Error writing to connected Braille display", e);
disconnect();
}
});
}
}
/**
* Starts reading HID bytes from this Braille display.
*
* Reads are performed on a background thread.
*/
private void startReadingLocked() {
mInputThread = new Thread(() -> {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try (InputStream inputStream = new FileInputStream(mHidrawNode)) {
final byte[] buffer = new byte[IBinder.getSuggestedMaxIpcSizeBytes()];
int readSize;
while (!Thread.interrupted()) {
if (!mHidrawNode.exists()) {
disconnect();
break;
}
// Reading from the HIDRAW character device node will block
// until bytes are available.
readSize = inputStream.read(buffer);
if (readSize > 0) {
try {
// Send the input to the AccessibilityService.
mController.onInput(Arrays.copyOfRange(buffer, 0, readSize));
} catch (RemoteException e) {
// Error communicating with the AccessibilityService.
Slog.e(LOG_TAG, "Error calling onInput", e);
disconnect();
break;
}
}
}
} catch (IOException e) {
Slog.d(LOG_TAG, "Error reading from connected Braille display", e);
disconnect();
}
}, "BrailleDisplayConnection input thread");
mInputThread.setDaemon(true);
mInputThread.start();
}
/** Stop the Input thread. */
private void closeInputLocked() {
if (mInputThread != null) {
mInputThread.interrupt();
}
mInputThread = null;
}
/** Stop the Output thread and close the Output stream. */
private void closeOutputLocked() {
if (mOutputThread != null) {
mOutputThread.quit();
}
mOutputThread = null;
if (mOutputStream != null) {
try {
mOutputStream.close();
} catch (IOException e) {
Slog.e(LOG_TAG, "Unable to close output stream", e);
}
}
mOutputStream = null;
}
/**
* Returns a {@link BrailleDisplayScanner} that opens {@link FileInputStream}s to read
* from HIDRAW nodes and perform ioctls using the provided {@link NativeInterface}.
*/
@VisibleForTesting
static BrailleDisplayScanner getDefaultNativeScanner(@NonNull NativeInterface nativeInterface) {
Objects.requireNonNull(nativeInterface);
return new BrailleDisplayScanner() {
private static final String HIDRAW_DEVICE_GLOB = "hidraw*";
@Override
public Collection Replaces the default {@link BrailleDisplayScanner} object for this connection,
* and also returns it to allow unit testing this test-only implementation.
*
* @see BrailleDisplayController#setTestBrailleDisplayData
*/
BrailleDisplayScanner setTestData(@NonNull List