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