1 /* 2 * Copyright (C) 2021 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.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.os.RemoteException; 23 import android.util.ArrayMap; 24 import android.view.MotionEvent; 25 import android.view.accessibility.AccessibilityInteractionClient; 26 27 import java.util.LinkedList; 28 import java.util.Queue; 29 import java.util.concurrent.Executor; 30 31 /** 32 * This class allows a service to handle touch exploration and the detection of specialized 33 * accessibility gestures. The service receives motion events and can match those motion events 34 * against the gestures it supports. The service can also request the framework enter three other 35 * states of operation for the duration of this interaction. Upon entering any of these states the 36 * framework will take over and the service will not receive motion events until the start of a new 37 * interaction. The states are as follows: 38 * 39 * <ul> 40 * <li>The service can tell the framework that this interaction is touch exploration. The user is 41 * trying to explore the screen rather than manipulate it. The framework will then convert the 42 * motion events to hover events to support touch exploration. 43 * <li>The service can tell the framework that this interaction is a dragging interaction where 44 * two fingers are used to execute a one-finger gesture such as scrolling the screen. The 45 * service must specify which of the two fingers should be passed through to rest of the input 46 * pipeline. 47 * <li>Finally, the service can request that the framework delegate this interaction, meaning pass 48 * it through to the rest of the input pipeline as-is. 49 * </ul> 50 * 51 * When {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE } is enabled, this 52 * controller will receive all motion events received by the framework for the specified display 53 * when not touch-exploring or delegating. If the service classifies this interaction as touch 54 * exploration or delegating the framework will stop sending motion events to the service for the 55 * duration of this interaction. If the service classifies this interaction as a dragging 56 * interaction the framework will send motion events to the service to allow the service to 57 * determine if the interaction still qualifies as dragging or if it has become a delegating 58 * interaction. If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE } is disabled 59 * this controller will not receive any motion events because touch interactions are being passed 60 * through to the input pipeline unaltered. 61 * Note that {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE } 62 * requires setting {@link android.R.attr#canRequestTouchExplorationMode} as well. 63 */ 64 public final class TouchInteractionController { 65 /** The state where the user is not touching the screen. */ 66 public static final int STATE_CLEAR = 0; 67 /** 68 * The state where the user is touching the screen and the service is receiving motion events. 69 */ 70 public static final int STATE_TOUCH_INTERACTING = 1; 71 /** 72 * The state where the user is explicitly exploring the screen. The service is not receiving 73 * motion events. 74 */ 75 public static final int STATE_TOUCH_EXPLORING = 2; 76 /** 77 * The state where the user is dragging with two fingers. The service is not receiving motion 78 * events. The selected finger is being dispatched to the rest of the input pipeline to execute 79 * the drag. 80 */ 81 public static final int STATE_DRAGGING = 3; 82 /** 83 * The user is performing a gesture which is being passed through to the input pipeline as-is. 84 * The service is not receiving motion events. 85 */ 86 public static final int STATE_DELEGATING = 4; 87 88 @IntDef({ 89 STATE_CLEAR, 90 STATE_TOUCH_INTERACTING, 91 STATE_TOUCH_EXPLORING, 92 STATE_DRAGGING, 93 STATE_DELEGATING 94 }) 95 private @interface State {} 96 97 // The maximum number of pointers that can be touching the screen at once. (See MAX_POINTER_ID 98 // in frameworks/native/include/input/Input.h) 99 private static final int MAX_POINTER_COUNT = 32; 100 101 private final AccessibilityService mService; 102 private final Object mLock; 103 private final int mDisplayId; 104 private boolean mServiceDetectsGestures; 105 /** Map of callbacks to executors. Lazily created when adding the first callback. */ 106 private ArrayMap<Callback, Executor> mCallbacks; 107 // A list of motion events that should be queued until a pending transition has taken place. 108 private Queue<MotionEvent> mQueuedMotionEvents = new LinkedList<>(); 109 // Whether this controller is waiting for a state transition. 110 // Motion events will be queued and sent to listeners after the transition has taken place. 111 private boolean mStateChangeRequested = false; 112 113 // The current state of the display. 114 private int mState = STATE_CLEAR; 115 TouchInteractionController( @onNull AccessibilityService service, @NonNull Object lock, int displayId)116 TouchInteractionController( 117 @NonNull AccessibilityService service, @NonNull Object lock, int displayId) { 118 mDisplayId = displayId; 119 mLock = lock; 120 mService = service; 121 } 122 123 /** 124 * Adds the specified callback to the list of callbacks. The callback will 125 * run using on the specified {@link Executor}', or on the service's main thread if the 126 * Executor is {@code null}. 127 * @param callback the callback to add, must be non-null 128 * @param executor the executor for this callback, or {@code null} to execute on the service's 129 * main thread 130 */ registerCallback(@ullable Executor executor, @NonNull Callback callback)131 public void registerCallback(@Nullable Executor executor, @NonNull Callback callback) { 132 synchronized (mLock) { 133 if (mCallbacks == null) { 134 mCallbacks = new ArrayMap<>(); 135 } 136 mCallbacks.put(callback, executor); 137 if (mCallbacks.size() == 1) { 138 setServiceDetectsGestures(true); 139 } 140 } 141 } 142 143 /** 144 * Unregisters the specified callback. 145 * 146 * @param callback the callback to remove, must be non-null 147 * @return {@code true} if the callback was removed, {@code false} otherwise 148 */ unregisterCallback(@onNull Callback callback)149 public boolean unregisterCallback(@NonNull Callback callback) { 150 if (mCallbacks == null) { 151 return false; 152 } 153 synchronized (mLock) { 154 boolean result = mCallbacks.remove(callback) != null; 155 if (result && mCallbacks.size() == 0) { 156 setServiceDetectsGestures(false); 157 } 158 return result; 159 } 160 } 161 162 /** 163 * Removes all callbacks and returns control of touch interactions to the framework. 164 */ unregisterAllCallbacks()165 public void unregisterAllCallbacks() { 166 if (mCallbacks != null) { 167 synchronized (mLock) { 168 mCallbacks.clear(); 169 setServiceDetectsGestures(false); 170 } 171 } 172 } 173 174 /** 175 * Dispatches motion events to any registered callbacks. This should be called on the service's 176 * main thread. 177 */ onMotionEvent(MotionEvent event)178 void onMotionEvent(MotionEvent event) { 179 if (mStateChangeRequested) { 180 mQueuedMotionEvents.add(event); 181 } else { 182 sendEventToAllListeners(event); 183 } 184 } 185 sendEventToAllListeners(MotionEvent event)186 private void sendEventToAllListeners(MotionEvent event) { 187 final ArrayMap<Callback, Executor> entries; 188 synchronized (mLock) { 189 // callbacks may remove themselves. Perform a shallow copy to avoid concurrent 190 // modification. 191 entries = new ArrayMap<>(mCallbacks); 192 } 193 for (int i = 0, count = entries.size(); i < count; i++) { 194 final Callback callback = entries.keyAt(i); 195 final Executor executor = entries.valueAt(i); 196 if (executor != null) { 197 executor.execute(() -> callback.onMotionEvent(event)); 198 } else { 199 // We're already on the main thread, just run the callback. 200 callback.onMotionEvent(event); 201 } 202 } 203 } 204 205 /** 206 * Dispatches motion events to any registered callbacks. This should be called on the service's 207 * main thread. 208 */ onStateChanged(@tate int state)209 void onStateChanged(@State int state) { 210 mState = state; 211 final ArrayMap<Callback, Executor> entries; 212 synchronized (mLock) { 213 // callbacks may remove themselves. Perform a shallow copy to avoid concurrent 214 // modification. 215 entries = new ArrayMap<>(mCallbacks); 216 } 217 for (int i = 0, count = entries.size(); i < count; i++) { 218 final Callback callback = entries.keyAt(i); 219 final Executor executor = entries.valueAt(i); 220 if (executor != null) { 221 executor.execute(() -> callback.onStateChanged(state)); 222 } else { 223 // We're already on the main thread, just run the callback. 224 callback.onStateChanged(state); 225 } 226 } 227 mStateChangeRequested = false; 228 while (mQueuedMotionEvents.size() > 0) { 229 sendEventToAllListeners(mQueuedMotionEvents.poll()); 230 } 231 } 232 233 /** 234 * When {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled, this 235 * controller will receive all motion events received by the framework for the specified display 236 * when not touch-exploring, delegating, or dragging. This allows the service to detect its own 237 * gestures, and use its own logic to judge when the framework should start touch-exploring, 238 * delegating, or dragging. If {@link 239 * AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE } is disabled this flag has no 240 * effect. 241 * 242 * @see AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE 243 */ setServiceDetectsGestures(boolean mode)244 private void setServiceDetectsGestures(boolean mode) { 245 final IAccessibilityServiceConnection connection = 246 AccessibilityInteractionClient.getInstance() 247 .getConnection(mService.getConnectionId()); 248 if (connection != null) { 249 try { 250 connection.setServiceDetectsGesturesEnabled(mDisplayId, mode); 251 mServiceDetectsGestures = mode; 252 } catch (RemoteException re) { 253 throw new RuntimeException(re); 254 } 255 } 256 } 257 258 /** 259 * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at 260 * least one callback has been added for this display this function tells the framework to 261 * initiate touch exploration. Touch exploration will continue for the duration of this 262 * interaction. 263 */ requestTouchExploration()264 public void requestTouchExploration() { 265 validateTransitionRequest(); 266 final IAccessibilityServiceConnection connection = 267 AccessibilityInteractionClient.getInstance() 268 .getConnection(mService.getConnectionId()); 269 if (connection != null) { 270 try { 271 connection.requestTouchExploration(mDisplayId); 272 } catch (RemoteException re) { 273 throw new RuntimeException(re); 274 } 275 mStateChangeRequested = true; 276 } 277 } 278 279 /** 280 * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} and {@link If 281 * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at least 282 * one callback has been added, this function tells the framework to initiate a dragging 283 * interaction using the specified pointer. The pointer's movements will be passed through to 284 * the rest of the input pipeline. Dragging is often used to perform two-finger scrolling. 285 * 286 * @param pointerId the pointer to be passed through to the rest of the input pipeline. If the 287 * pointer id is valid but not actually present on the screen it will be ignored. 288 * @throws IllegalArgumentException if the pointer id is outside of the allowed range. 289 */ requestDragging(int pointerId)290 public void requestDragging(int pointerId) { 291 validateTransitionRequest(); 292 if (pointerId < 0 || pointerId > MAX_POINTER_COUNT) { 293 throw new IllegalArgumentException("Invalid pointer id: " + pointerId); 294 } 295 final IAccessibilityServiceConnection connection = 296 AccessibilityInteractionClient.getInstance() 297 .getConnection(mService.getConnectionId()); 298 if (connection != null) { 299 try { 300 connection.requestDragging(mDisplayId, pointerId); 301 } catch (RemoteException re) { 302 throw new RuntimeException(re); 303 } 304 mStateChangeRequested = true; 305 } 306 } 307 308 /** 309 * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} and {@link If 310 * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at least 311 * one callback has been added, this function tells the framework to initiate a delegating 312 * interaction. Motion events will be passed through as-is to the rest of the input pipeline for 313 * the duration of this interaction. 314 */ requestDelegating()315 public void requestDelegating() { 316 validateTransitionRequest(); 317 final IAccessibilityServiceConnection connection = 318 AccessibilityInteractionClient.getInstance() 319 .getConnection(mService.getConnectionId()); 320 if (connection != null) { 321 try { 322 connection.requestDelegating(mDisplayId); 323 } catch (RemoteException re) { 324 throw new RuntimeException(re); 325 } 326 mStateChangeRequested = true; 327 } 328 } 329 330 /** 331 * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} and {@link If 332 * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at least 333 * one callback has been added, this function tells the framework to perform a click. 334 * The framework will first try to perform 335 * {@link AccessibilityNodeInfo.AccessibilityAction#ACTION_CLICK} on the item with 336 * accessibility focus. If that fails, the framework will simulate a click using motion events 337 * on the last location to have accessibility focus. 338 */ performClick()339 public void performClick() { 340 final IAccessibilityServiceConnection connection = 341 AccessibilityInteractionClient.getInstance() 342 .getConnection(mService.getConnectionId()); 343 if (connection != null) { 344 try { 345 connection.onDoubleTap(mDisplayId); 346 } catch (RemoteException re) { 347 throw new RuntimeException(re); 348 } 349 } 350 } 351 352 /** 353 * If {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} and {@link If 354 * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled and at least 355 * one callback has been added, this function tells the framework to perform a long click. 356 * The framework will simulate a long click using motion events on the last location with 357 * accessibility focus and will delegate any movements to the rest of the input pipeline. This 358 * allows a user to double-tap and hold to trigger a drag and then execute that drag by moving 359 * their finger. 360 */ performLongClickAndStartDrag()361 public void performLongClickAndStartDrag() { 362 final IAccessibilityServiceConnection connection = 363 AccessibilityInteractionClient.getInstance() 364 .getConnection(mService.getConnectionId()); 365 if (connection != null) { 366 try { 367 connection.onDoubleTapAndHold(mDisplayId); 368 } catch (RemoteException re) { 369 throw new RuntimeException(re); 370 } 371 } 372 } 373 validateTransitionRequest()374 private void validateTransitionRequest() { 375 if (!mServiceDetectsGestures || mCallbacks.size() == 0) { 376 throw new IllegalStateException( 377 "State transitions are not allowed without first adding a callback."); 378 } 379 if ((mState == STATE_DELEGATING || mState == STATE_TOUCH_EXPLORING)) { 380 throw new IllegalStateException( 381 "State transition requests are not allowed in " + stateToString(mState)); 382 } 383 } 384 385 /** @return the maximum number of pointers that this display will accept. */ getMaxPointerCount()386 public int getMaxPointerCount() { 387 return MAX_POINTER_COUNT; 388 } 389 390 /** @return the display id associated with this controller. */ getDisplayId()391 public int getDisplayId() { 392 return mDisplayId; 393 } 394 395 /** 396 * @return the current state of this controller. 397 * @see TouchInteractionController#STATE_CLEAR 398 * @see TouchInteractionController#STATE_DELEGATING 399 * @see TouchInteractionController#STATE_DRAGGING 400 * @see TouchInteractionController#STATE_TOUCH_EXPLORING 401 */ getState()402 public int getState() { 403 synchronized (mLock) { 404 return mState; 405 } 406 } 407 408 /** Returns a string representation of the specified state. */ 409 @NonNull stateToString(int state)410 public static String stateToString(int state) { 411 switch (state) { 412 case STATE_CLEAR: 413 return "STATE_CLEAR"; 414 case STATE_TOUCH_INTERACTING: 415 return "STATE_TOUCH_INTERACTING"; 416 case STATE_TOUCH_EXPLORING: 417 return "STATE_TOUCH_EXPLORING"; 418 case STATE_DRAGGING: 419 return "STATE_DRAGGING"; 420 case STATE_DELEGATING: 421 return "STATE_DELEGATING"; 422 default: 423 return "Unknown state: " + state; 424 } 425 } 426 427 /** callbacks allow services to receive motion events and state change updates. */ 428 public interface Callback { 429 /** 430 * Called when the framework has sent a motion event to the service. 431 * 432 * @param event the event being passed to the service. 433 */ onMotionEvent(@onNull MotionEvent event)434 void onMotionEvent(@NonNull MotionEvent event); 435 436 /** 437 * Called when the state of motion event dispatch for this display has changed. 438 * 439 * @param state the new state of motion event dispatch. 440 * @see TouchInteractionController#STATE_CLEAR 441 * @see TouchInteractionController#STATE_DELEGATING 442 * @see TouchInteractionController#STATE_DRAGGING 443 * @see TouchInteractionController#STATE_TOUCH_EXPLORING 444 */ onStateChanged(@tate int state)445 void onStateChanged(@State int state); 446 } 447 } 448