• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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