• 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.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