1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.accessibilityservice; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.FlaggedApi; 21 import android.annotation.NonNull; 22 import android.annotation.RequiresPermission; 23 import android.bluetooth.BluetoothDevice; 24 import android.hardware.usb.UsbDevice; 25 import android.os.Binder; 26 import android.os.IBinder; 27 import android.os.RemoteException; 28 import android.os.SystemProperties; 29 import android.view.accessibility.AccessibilityInteractionClient; 30 import android.view.accessibility.Flags; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.internal.util.FunctionalUtils; 34 35 import java.io.IOException; 36 import java.util.Objects; 37 import java.util.concurrent.Executor; 38 39 /** 40 * Default implementation of {@link BrailleDisplayController}. 41 * 42 * @hide 43 */ 44 // BrailleDisplayControllerImpl is not an API, but it implements BrailleDisplayController APIs. 45 // This @FlaggedApi annotation tells the linter that this method delegates API checks to its 46 // callers. 47 @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) 48 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 49 public final class BrailleDisplayControllerImpl implements BrailleDisplayController { 50 51 private final AccessibilityService mAccessibilityService; 52 private final Object mLock; 53 private final boolean mIsHidrawSupported; 54 55 private IBrailleDisplayConnection mBrailleDisplayConnection; 56 private Executor mCallbackExecutor; 57 private BrailleDisplayCallback mCallback; 58 59 /** 60 * Read-only property that returns whether HIDRAW access is supported on this device. 61 * 62 * <p>Defaults to true. 63 * 64 * <p>Device manufacturers without HIDRAW kernel support can set this to false in 65 * the device's product makefile. 66 */ 67 private static final boolean IS_HIDRAW_SUPPORTED = SystemProperties.getBoolean( 68 "ro.accessibility.support_hidraw", true); 69 BrailleDisplayControllerImpl(AccessibilityService accessibilityService, Object lock)70 BrailleDisplayControllerImpl(AccessibilityService accessibilityService, 71 Object lock) { 72 this(accessibilityService, lock, IS_HIDRAW_SUPPORTED); 73 } 74 75 @VisibleForTesting BrailleDisplayControllerImpl(AccessibilityService accessibilityService, Object lock, boolean isHidrawSupported)76 public BrailleDisplayControllerImpl(AccessibilityService accessibilityService, 77 Object lock, boolean isHidrawSupported) { 78 mAccessibilityService = accessibilityService; 79 mLock = lock; 80 mIsHidrawSupported = isHidrawSupported; 81 } 82 83 @Override 84 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) connect(@onNull BluetoothDevice bluetoothDevice, @NonNull BrailleDisplayCallback callback)85 public void connect(@NonNull BluetoothDevice bluetoothDevice, 86 @NonNull BrailleDisplayCallback callback) { 87 connect(bluetoothDevice, mAccessibilityService.getMainExecutor(), callback); 88 } 89 90 @Override 91 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) connect(@onNull BluetoothDevice bluetoothDevice, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull BrailleDisplayCallback callback)92 public void connect(@NonNull BluetoothDevice bluetoothDevice, 93 @NonNull @CallbackExecutor Executor callbackExecutor, 94 @NonNull BrailleDisplayCallback callback) { 95 Objects.requireNonNull(bluetoothDevice); 96 Objects.requireNonNull(callbackExecutor); 97 Objects.requireNonNull(callback); 98 connect(serviceConnection -> serviceConnection.connectBluetoothBrailleDisplay( 99 bluetoothDevice.getAddress(), new IBrailleDisplayControllerWrapper()), 100 callbackExecutor, callback); 101 } 102 103 @Override connect(@onNull UsbDevice usbDevice, @NonNull BrailleDisplayCallback callback)104 public void connect(@NonNull UsbDevice usbDevice, 105 @NonNull BrailleDisplayCallback callback) { 106 connect(usbDevice, mAccessibilityService.getMainExecutor(), callback); 107 } 108 109 @Override connect(@onNull UsbDevice usbDevice, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull BrailleDisplayCallback callback)110 public void connect(@NonNull UsbDevice usbDevice, 111 @NonNull @CallbackExecutor Executor callbackExecutor, 112 @NonNull BrailleDisplayCallback callback) { 113 Objects.requireNonNull(usbDevice); 114 Objects.requireNonNull(callbackExecutor); 115 Objects.requireNonNull(callback); 116 connect(serviceConnection -> serviceConnection.connectUsbBrailleDisplay( 117 usbDevice, new IBrailleDisplayControllerWrapper()), 118 callbackExecutor, callback); 119 } 120 121 /** 122 * Shared implementation for the {@code connect()} API methods. 123 * 124 * <p>Performs a blocking call to system_server to create the connection. Success is 125 * returned through {@link BrailleDisplayCallback#onConnected} while normal connection 126 * errors are returned through {@link BrailleDisplayCallback#onConnectionFailed}. This 127 * connection is implemented using cached data from the HIDRAW driver so it returns 128 * quickly without needing to perform any I/O with the Braille display. 129 * 130 * <p>The AIDL call to system_server is blocking (not posted to a handler thread) so 131 * that runtime exceptions signaling abnormal connection errors from API misuse 132 * (e.g. lacking permissions, providing an invalid BluetoothDevice, calling connect 133 * while already connected) are propagated to the API caller. 134 */ connect( FunctionalUtils.RemoteExceptionIgnoringConsumer<IAccessibilityServiceConnection> createConnection, @NonNull Executor callbackExecutor, @NonNull BrailleDisplayCallback callback)135 private void connect( 136 FunctionalUtils.RemoteExceptionIgnoringConsumer<IAccessibilityServiceConnection> 137 createConnection, 138 @NonNull Executor callbackExecutor, @NonNull BrailleDisplayCallback callback) { 139 BrailleDisplayController.checkApiFlagIsEnabled(); 140 if (!mIsHidrawSupported) { 141 callbackExecutor.execute(() -> callback.onConnectionFailed( 142 BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS)); 143 return; 144 } 145 if (isConnected()) { 146 throw new IllegalStateException( 147 "This service already has a connected Braille display"); 148 } 149 final IAccessibilityServiceConnection serviceConnection = 150 AccessibilityInteractionClient.getConnection( 151 mAccessibilityService.getConnectionId()); 152 if (serviceConnection == null) { 153 throw new IllegalStateException("Accessibility service is not connected"); 154 } 155 synchronized (mLock) { 156 mCallbackExecutor = callbackExecutor; 157 mCallback = callback; 158 } 159 try { 160 createConnection.acceptOrThrow(serviceConnection); 161 } catch (RemoteException e) { 162 throw e.rethrowFromSystemServer(); 163 } 164 } 165 166 @Override isConnected()167 public boolean isConnected() { 168 BrailleDisplayController.checkApiFlagIsEnabled(); 169 return mBrailleDisplayConnection != null; 170 } 171 172 @Override write(@onNull byte[] buffer)173 public void write(@NonNull byte[] buffer) throws IOException { 174 BrailleDisplayController.checkApiFlagIsEnabled(); 175 Objects.requireNonNull(buffer); 176 if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) { 177 // This same check must be performed in the system to prevent reflection misuse, 178 // but perform it here too to prevent unnecessary IPCs from non-reflection callers. 179 throw new IllegalArgumentException("Invalid write buffer size " + buffer.length); 180 } 181 synchronized (mLock) { 182 if (mBrailleDisplayConnection == null) { 183 throw new IOException("Braille display is not connected"); 184 } 185 try { 186 mBrailleDisplayConnection.write(buffer); 187 } catch (RemoteException e) { 188 throw e.rethrowFromSystemServer(); 189 } 190 } 191 } 192 193 @Override disconnect()194 public void disconnect() { 195 BrailleDisplayController.checkApiFlagIsEnabled(); 196 synchronized (mLock) { 197 try { 198 if (mBrailleDisplayConnection != null) { 199 mBrailleDisplayConnection.disconnect(); 200 } 201 } catch (RemoteException e) { 202 throw e.rethrowFromSystemServer(); 203 } finally { 204 clearConnectionLocked(); 205 } 206 } 207 } 208 209 /** 210 * Implementation of the {@code IBrailleDisplayController} AIDL interface provided to 211 * system_server, which system_server uses to pass messages back to this 212 * {@code BrailleDisplayController}. 213 * 214 * <p>Messages from system_server are routed to the {@link BrailleDisplayCallback} callbacks 215 * implemented by the accessibility service. 216 * 217 * <p>Note: Per API Guidelines 7.5 the Binder identity must be cleared before invoking the 218 * callback executor so that Binder identity checks in the callbacks are performed using the 219 * app's identity. 220 */ 221 private final class IBrailleDisplayControllerWrapper extends IBrailleDisplayController.Stub { 222 /** 223 * Called when the system successfully connects to a Braille display. 224 */ 225 @Override onConnected(IBrailleDisplayConnection connection, byte[] hidDescriptor)226 public void onConnected(IBrailleDisplayConnection connection, byte[] hidDescriptor) { 227 BrailleDisplayController.checkApiFlagIsEnabled(); 228 final long identity = Binder.clearCallingIdentity(); 229 try { 230 synchronized (mLock) { 231 mBrailleDisplayConnection = connection; 232 mCallbackExecutor.execute(() -> mCallback.onConnected(hidDescriptor)); 233 } 234 } finally { 235 Binder.restoreCallingIdentity(identity); 236 } 237 } 238 239 /** 240 * Called when the system is unable to connect to a Braille display. 241 */ 242 @Override onConnectionFailed(@railleDisplayCallback.ErrorCode int errorCode)243 public void onConnectionFailed(@BrailleDisplayCallback.ErrorCode int errorCode) { 244 BrailleDisplayController.checkApiFlagIsEnabled(); 245 final long identity = Binder.clearCallingIdentity(); 246 try { 247 synchronized (mLock) { 248 mCallbackExecutor.execute(() -> mCallback.onConnectionFailed(errorCode)); 249 } 250 } finally { 251 Binder.restoreCallingIdentity(identity); 252 } 253 } 254 255 /** 256 * Called when input is received from the currently connected Braille display. 257 */ 258 @Override onInput(byte[] input)259 public void onInput(byte[] input) { 260 BrailleDisplayController.checkApiFlagIsEnabled(); 261 final long identity = Binder.clearCallingIdentity(); 262 try { 263 synchronized (mLock) { 264 // Ignore input that arrives after disconnection. 265 if (mBrailleDisplayConnection != null) { 266 mCallbackExecutor.execute(() -> mCallback.onInput(input)); 267 } 268 } 269 } finally { 270 Binder.restoreCallingIdentity(identity); 271 } 272 } 273 274 /** 275 * Called when the currently connected Braille display is disconnected. 276 */ 277 @Override onDisconnected()278 public void onDisconnected() { 279 BrailleDisplayController.checkApiFlagIsEnabled(); 280 final long identity = Binder.clearCallingIdentity(); 281 try { 282 synchronized (mLock) { 283 mCallbackExecutor.execute(mCallback::onDisconnected); 284 clearConnectionLocked(); 285 } 286 } finally { 287 Binder.restoreCallingIdentity(identity); 288 } 289 } 290 } 291 clearConnectionLocked()292 private void clearConnectionLocked() { 293 mBrailleDisplayConnection = null; 294 } 295 296 } 297