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