Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e78750db authored by Daniel Norman's avatar Daniel Norman
Browse files

New AccessibilityService API to talk with HID Braille Displays.

API is implemented behind flag android.view.accessibility.braille_display_hid

Bug: 303522222
Bug: 316035785
Test: CTS: atest BrailleDisplayTest
      Internal: atest BrailleDisplayConnectionTest
                atest AccessibilityServiceConnectionTest
Change-Id: Ia775750ed6c11da1b21983ec70b035039f1df665
parent abf9557e
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
@@ -3322,11 +3322,13 @@ package android.accessibilityservice {
    method @FlaggedApi("android.view.accessibility.a11y_overlay_callbacks") public void attachAccessibilityOverlayToWindow(int, @NonNull android.view.SurfaceControl, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
    method public boolean clearCache();
    method public boolean clearCachedSubtree(@NonNull android.view.accessibility.AccessibilityNodeInfo);
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void clearTestBrailleDisplayController();
    method public final void disableSelf();
    method public final boolean dispatchGesture(@NonNull android.accessibilityservice.GestureDescription, @Nullable android.accessibilityservice.AccessibilityService.GestureResultCallback, @Nullable android.os.Handler);
    method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
    method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController();
    method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(int);
    method @FlaggedApi("android.view.accessibility.braille_display_hid") @NonNull public android.accessibilityservice.BrailleDisplayController getBrailleDisplayController();
    method @NonNull @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public final android.accessibilityservice.FingerprintGestureController getFingerprintGestureController();
    method @Nullable public final android.accessibilityservice.InputMethod getInputMethod();
    method @NonNull public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController();
@@ -3356,6 +3358,7 @@ package android.accessibilityservice {
    method public boolean setCacheEnabled(boolean);
    method public void setGestureDetectionPassthroughRegion(int, @NonNull android.graphics.Region);
    method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo);
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void setTestBrailleDisplayController(@NonNull android.accessibilityservice.BrailleDisplayController);
    method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region);
    method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
    method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
@@ -3560,6 +3563,25 @@ package android.accessibilityservice {
    field public String[] packageNames;
  }
  @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void disconnect();
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public boolean isConnected();
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void write(@NonNull byte[]) throws java.io.IOException;
  }
  @FlaggedApi("android.view.accessibility.braille_display_hid") public static interface BrailleDisplayController.BrailleDisplayCallback {
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnected(@NonNull byte[]);
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnectionFailed(int);
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onDisconnected();
    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onInput(@NonNull byte[]);
    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 2; // 0x2
    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_CANNOT_ACCESS = 1; // 0x1
  }
  public final class FingerprintGestureController {
    method public boolean isGestureDetectionAvailable();
    method public void registerFingerprintGestureCallback(@NonNull android.accessibilityservice.FingerprintGestureController.FingerprintGestureCallback, @Nullable android.os.Handler);
+8 −0
Original line number Diff line number Diff line
@@ -104,6 +104,14 @@ package android.accessibilityservice {
    method @FlaggedApi("android.view.accessibility.motion_event_observing") public void setObservedMotionEventSources(int);
  }

  @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public static void setTestBrailleDisplayData(@NonNull android.accessibilityservice.AccessibilityService, @NonNull java.util.List<android.os.Bundle>);
    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
  }

}

package android.animation {
+55 −0
Original line number Diff line number Diff line
@@ -80,6 +80,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
@@ -850,6 +851,8 @@ public abstract class AccessibilityService extends Service {
    private boolean mInputMethodInitialized = false;
    private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers =
            new SparseArray<>(0);
    private BrailleDisplayController mBrailleDisplayController;
    private BrailleDisplayController mTestBrailleDisplayController;

    private int mGestureStatusCallbackSequence;

@@ -3634,4 +3637,56 @@ public abstract class AccessibilityService extends Service {
                .attachAccessibilityOverlayToWindow(
                        mConnectionId, accessibilityWindowId, sc, executor, callback);
    }

    /**
     * Returns the {@link BrailleDisplayController} which may be used to communicate with
     * refreshable Braille displays that provide USB or Bluetooth Braille display HID support.
     */
    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
    @NonNull
    public BrailleDisplayController getBrailleDisplayController() {
        BrailleDisplayController.checkApiFlagIsEnabled();
        synchronized (mLock) {
            if (mTestBrailleDisplayController != null) {
                return mTestBrailleDisplayController;
            }

            if (mBrailleDisplayController == null) {
                mBrailleDisplayController = new BrailleDisplayControllerImpl(this, mLock);
            }
            return mBrailleDisplayController;
        }
    }

    /**
     * Set the {@link BrailleDisplayController} implementation that will be returned by
     * {@link #getBrailleDisplayController}, to allow this accessibility service to test its
     * interaction with BrailleDisplayController without requiring a real Braille display.
     *
     * <p>For full test fidelity, ensure that this test-only implementation follows the same
     * behavior specified in the documentation for {@link BrailleDisplayController}, including
     * thrown exceptions.
     *
     * @param controller A test-only implementation of {@link BrailleDisplayController}.
     */
    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
    public void setTestBrailleDisplayController(@NonNull BrailleDisplayController controller) {
        BrailleDisplayController.checkApiFlagIsEnabled();
        Objects.requireNonNull(controller);
        synchronized (mLock) {
            mTestBrailleDisplayController = controller;
        }
    }

    /**
     * Clears the {@link BrailleDisplayController} previously set by
     * {@link #setTestBrailleDisplayController}.
     */
    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
    public void clearTestBrailleDisplayController() {
        BrailleDisplayController.checkApiFlagIsEnabled();
        synchronized (mLock) {
            mTestBrailleDisplayController = null;
        }
    }
}
+308 −0
Original line number Diff line number Diff line
/*
 * 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.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.bluetooth.BluetoothDevice;
import android.hardware.usb.UsbDevice;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.accessibility.AccessibilityInteractionClient;
import android.view.accessibility.Flags;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.concurrent.Executor;

/**
 * Used to communicate with a Braille display that supports the Braille display HID standard
 * (usage page 0x41).
 *
 * <p>Only one Braille display may be connected at a time.
 */
// This interface doesn't actually own resources. Its I/O connections are owned, monitored,
// and automatically closed by the system after the accessibility service is disconnected.
@SuppressLint("NotCloseable")
@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
public interface BrailleDisplayController {

    /**
     * Throw {@link IllegalStateException} if this feature's aconfig flag is disabled.
     *
     * @hide
     */
    static void checkApiFlagIsEnabled() {
        if (!Flags.brailleDisplayHid()) {
            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
        }
    }

    /**
     * Interface provided to {@link BrailleDisplayController} connection methods to
     * receive callbacks from the system.
     */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    interface BrailleDisplayCallback {
        /**
         * The system cannot access connected HID devices.
         */
        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
        int FLAG_ERROR_CANNOT_ACCESS = 1 << 0;
        /**
         * A unique Braille display matching the requested properties could not be identified.
         */
        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
        int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 1 << 1;

        /** @hide */
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(flag = true, prefix = "FLAG_ERROR_", value = {
                FLAG_ERROR_CANNOT_ACCESS,
                FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND,
        })
        @interface ErrorCode {
        }

        /**
         * Callback to observe a successful Braille display connection.
         *
         * <p>The provided HID report descriptor should be used to understand the input bytes
         * received from the Braille display via {@link #onInput} and to prepare
         * the output sent to the Braille display via {@link #write}.
         *
         * @param hidDescriptor The HID report descriptor for this Braille display.
         * @see #connect(BluetoothDevice, BrailleDisplayCallback)
         * @see #connect(UsbDevice, BrailleDisplayCallback)
         */
        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
        void onConnected(@NonNull byte[] hidDescriptor);

        /**
         * Callback to observe a failed Braille display connection.
         *
         * @param errorFlags A bitmask of error codes for the connection failure.
         * @see #connect(BluetoothDevice, BrailleDisplayCallback)
         * @see #connect(UsbDevice, BrailleDisplayCallback)
         */
        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
        void onConnectionFailed(@ErrorCode int errorFlags);

        /**
         * Callback to observe input bytes from the currently connected Braille display.
         *
         * @param input The input bytes from the Braille display, formatted according to the HID
         *              report descriptor and the HIDRAW kernel driver.
         */
        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
        void onInput(@NonNull byte[] input);

        /**
         * Callback to observe when the currently connected Braille display is disconnected by the
         * system.
         */
        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
        void onDisconnected();
    }

    /**
     * Connects to the requested bluetooth Braille display using the Braille
     * display HID standard (usage page 0x41).
     *
     * <p>If successful then the HID report descriptor will be provided to
     * {@link BrailleDisplayCallback#onConnected}
     * and the Braille display will start sending incoming input bytes to
     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
     * then the system will disconnect the Braille display.
     *
     * <p>Note that the callbacks will be executed on the main thread using
     * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
     * {@link #connect(BluetoothDevice, Executor, BrailleDisplayCallback)}.
     *
     * @param bluetoothDevice The Braille display device.
     * @param callback        Callbacks used to provide connection results.
     * @see BrailleDisplayCallback#onConnected
     * @see BrailleDisplayCallback#onConnectionFailed
     * @throws IllegalStateException if a Braille display is already connected to this controller.
     */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
    void connect(@NonNull BluetoothDevice bluetoothDevice,
            @NonNull BrailleDisplayCallback callback);

    /**
     * Connects to the requested bluetooth Braille display using the Braille
     * display HID standard (usage page 0x41).
     *
     * <p>If successful then the HID report descriptor will be provided to
     * {@link BrailleDisplayCallback#onConnected}
     * and the Braille display will start sending incoming input bytes to
     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
     * then the system will disconnect the Braille display.
     *
     * @param bluetoothDevice  The Braille display device.
     * @param callbackExecutor Executor for executing the provided callbacks.
     * @param callback         Callbacks used to provide connection results.
     * @see BrailleDisplayCallback#onConnected
     * @see BrailleDisplayCallback#onConnectionFailed
     * @throws IllegalStateException if a Braille display is already connected to this controller.
     */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
    void connect(@NonNull BluetoothDevice bluetoothDevice,
            @NonNull @CallbackExecutor Executor callbackExecutor,
            @NonNull BrailleDisplayCallback callback);

    /**
     * Connects to the requested USB Braille display using the Braille
     * display HID standard (usage page 0x41).
     *
     * <p>If successful then the HID report descriptor will be provided to
     * {@link BrailleDisplayCallback#onConnected}
     * and the Braille display will start sending incoming input bytes to
     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
     * then the system will disconnect the Braille display.
     *
     * <p>The accessibility service app must already have approval to access the USB device
     * from the standard {@link android.hardware.usb.UsbManager} access approval process.
     *
     * <p>Note that the callbacks will be executed on the main thread using
     * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
     * {@link #connect(UsbDevice, Executor, BrailleDisplayCallback)}.
     *
     * @param usbDevice        The Braille display device.
     * @param callback         Callbacks used to provide connection results.
     * @see BrailleDisplayCallback#onConnected
     * @see BrailleDisplayCallback#onConnectionFailed
     * @throws SecurityException if the caller does not have USB device approval.
     * @throws IllegalStateException if a Braille display is already connected to this controller.
     */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    void connect(@NonNull UsbDevice usbDevice,
            @NonNull BrailleDisplayCallback callback);

    /**
     * Connects to the requested USB Braille display using the Braille
     * display HID standard (usage page 0x41).
     *
     * <p>If successful then the HID report descriptor will be provided to
     * {@link BrailleDisplayCallback#onConnected}
     * and the Braille display will start sending incoming input bytes to
     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
     * then the system will disconnect the Braille display.
     *
     * <p>The accessibility service app must already have approval to access the USB device
     * from the standard {@link android.hardware.usb.UsbManager} access approval process.
     *
     * @param usbDevice        The Braille display device.
     * @param callbackExecutor Executor for executing the provided callbacks.
     * @param callback         Callbacks used to provide connection results.
     * @see BrailleDisplayCallback#onConnected
     * @see BrailleDisplayCallback#onConnectionFailed
     * @throws SecurityException if the caller does not have USB device approval.
     * @throws IllegalStateException if a Braille display is already connected to this controller.
     */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    void connect(@NonNull UsbDevice usbDevice,
            @NonNull @CallbackExecutor Executor callbackExecutor,
            @NonNull BrailleDisplayCallback callback);

    /**
     * Returns true if a Braille display is currently connected, otherwise false.
     *
     * @see #connect
     */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    boolean isConnected();

    /**
     * Writes a HID report to the currently connected Braille display.
     *
     * <p>This method returns immediately after dispatching the write request to the system.
     * If the system experiences an error in writing output (e.g. the Braille display is unplugged
     * after the system receives the write request but before writing the bytes to the Braille
     * display) then the system will disconnect the Braille display, which calls
     * {@link BrailleDisplayCallback#onDisconnected()}.
     *
     * @param buffer The bytes to write to the Braille display. These bytes should be formatted
     *               according to the HID report descriptor and the HIDRAW kernel driver.
     * @throws IOException              if there is no currently connected Braille display.
     * @throws IllegalArgumentException if the buffer exceeds the maximum safe payload size for
     *                                  binder transactions of
     *                                  {@link IBinder#getSuggestedMaxIpcSizeBytes()}
     */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    void write(@NonNull byte[] buffer) throws IOException;

    /**
     * Disconnects from the currently connected Braille display.
     *
     * @see #isConnected()
     */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    void disconnect();

    /**
     * Provides test Braille display data to be used for automated CTS tests.
     *
     * <p>See {@code TEST_BRAILLE_DISPLAY_*} bundle keys.
     *
     * @hide
     */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)
    @TestApi
    static void setTestBrailleDisplayData(
            @NonNull AccessibilityService service,
            @NonNull List<Bundle> brailleDisplays) {
        checkApiFlagIsEnabled();
        final IAccessibilityServiceConnection serviceConnection =
                AccessibilityInteractionClient.getConnection(service.getConnectionId());
        if (serviceConnection != null) {
            try {
                serviceConnection.setTestBrailleDisplayData(brailleDisplays);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /** @hide */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    @TestApi
    String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
    /** @hide */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    @TestApi
    String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
    /** @hide */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    @TestApi
    String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
    /** @hide */
    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
    @TestApi
    String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
}
+267 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading