1 /*
2  * Copyright (C) 2015 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 com.android.server.accessibility;
18 
19 import android.accessibilityservice.AccessibilityTrace;
20 import android.annotation.NonNull;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.database.ContentObserver;
24 import android.net.Uri;
25 import android.os.Handler;
26 import android.os.SystemClock;
27 import android.provider.Settings;
28 import android.view.InputDevice;
29 import android.view.KeyEvent;
30 import android.view.MotionEvent;
31 import android.view.MotionEvent.PointerCoords;
32 import android.view.MotionEvent.PointerProperties;
33 import android.view.accessibility.AccessibilityManager;
34 
35 /**
36  * Implements "Automatically click on mouse stop" feature.
37  *
38  * If enabled, it will observe motion events from mouse source, and send click event sequence
39  * shortly after mouse stops moving. The click will only be performed if mouse movement had been
40  * actually detected.
41  *
42  * Movement detection has tolerance to jitter that may be caused by poor motor control to prevent:
43  * <ul>
44  *   <li>Initiating unwanted clicks with no mouse movement.</li>
45  *   <li>Autoclick never occurring after mouse arriving at target.</li>
46  * </ul>
47  *
48  * Non-mouse motion events, key events (excluding modifiers) and non-movement mouse events cancel
49  * the automatic click.
50  *
51  * It is expected that each instance will receive mouse events from a single mouse device. User of
52  * the class should handle cases where multiple mouse devices are present.
53  *
54  * Each instance is associated to a single user (and it does not handle user switch itself).
55  */
56 public class AutoclickController extends BaseEventStreamTransformation {
57 
58     private static final String LOG_TAG = AutoclickController.class.getSimpleName();
59 
60     private final AccessibilityTraceManager mTrace;
61     private final Context mContext;
62     private final int mUserId;
63 
64     // Lazily created on the first mouse motion event.
65     private ClickScheduler mClickScheduler;
66     private ClickDelayObserver mClickDelayObserver;
67 
AutoclickController(Context context, int userId, AccessibilityTraceManager trace)68     public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) {
69         mTrace = trace;
70         mContext = context;
71         mUserId = userId;
72     }
73 
74     @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)75     public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
76         if (mTrace.isA11yTracingEnabledForTypes(AccessibilityTrace.FLAGS_INPUT_FILTER)) {
77             mTrace.logTrace(LOG_TAG + ".onMotionEvent", AccessibilityTrace.FLAGS_INPUT_FILTER,
78                     "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags);
79         }
80         if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
81             if (mClickScheduler == null) {
82                 Handler handler = new Handler(mContext.getMainLooper());
83                 mClickScheduler =
84                         new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT);
85                 mClickDelayObserver = new ClickDelayObserver(mUserId, handler);
86                 mClickDelayObserver.start(mContext.getContentResolver(), mClickScheduler);
87             }
88 
89             handleMouseMotion(event, policyFlags);
90         } else if (mClickScheduler != null) {
91             mClickScheduler.cancel();
92         }
93 
94         super.onMotionEvent(event, rawEvent, policyFlags);
95     }
96 
97     @Override
onKeyEvent(KeyEvent event, int policyFlags)98     public void onKeyEvent(KeyEvent event, int policyFlags) {
99         if (mTrace.isA11yTracingEnabledForTypes(AccessibilityTrace.FLAGS_INPUT_FILTER)) {
100             mTrace.logTrace(LOG_TAG + ".onKeyEvent", AccessibilityTrace.FLAGS_INPUT_FILTER,
101                     "event=" + event + ";policyFlags=" + policyFlags);
102         }
103         if (mClickScheduler != null) {
104             if (KeyEvent.isModifierKey(event.getKeyCode())) {
105                 mClickScheduler.updateMetaState(event.getMetaState());
106             } else {
107                 mClickScheduler.cancel();
108             }
109         }
110 
111         super.onKeyEvent(event, policyFlags);
112     }
113 
114     @Override
clearEvents(int inputSource)115     public void clearEvents(int inputSource) {
116         if (inputSource == InputDevice.SOURCE_MOUSE && mClickScheduler != null) {
117             mClickScheduler.cancel();
118         }
119 
120         super.clearEvents(inputSource);
121     }
122 
123     @Override
onDestroy()124     public void onDestroy() {
125         if (mClickDelayObserver != null) {
126             mClickDelayObserver.stop();
127             mClickDelayObserver = null;
128         }
129         if (mClickScheduler != null) {
130             mClickScheduler.cancel();
131             mClickScheduler = null;
132         }
133     }
134 
handleMouseMotion(MotionEvent event, int policyFlags)135     private void handleMouseMotion(MotionEvent event, int policyFlags) {
136         switch (event.getActionMasked()) {
137             case MotionEvent.ACTION_HOVER_MOVE: {
138                 if (event.getPointerCount() == 1) {
139                     mClickScheduler.update(event, policyFlags);
140                 } else {
141                     mClickScheduler.cancel();
142                 }
143             } break;
144             // Ignore hover enter and exit.
145             case MotionEvent.ACTION_HOVER_ENTER:
146             case MotionEvent.ACTION_HOVER_EXIT:
147                 break;
148             default:
149                 mClickScheduler.cancel();
150         }
151     }
152 
153     /**
154      * Observes setting value for autoclick delay, and updates ClickScheduler delay whenever the
155      * setting value changes.
156      */
157     final private static class ClickDelayObserver extends ContentObserver {
158         /** URI used to identify the autoclick delay setting with content resolver. */
159         private final Uri mAutoclickDelaySettingUri = Settings.Secure.getUriFor(
160                 Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY);
161 
162         private ContentResolver mContentResolver;
163         private ClickScheduler mClickScheduler;
164         private final int mUserId;
165 
ClickDelayObserver(int userId, Handler handler)166         public ClickDelayObserver(int userId, Handler handler) {
167             super(handler);
168             mUserId = userId;
169         }
170 
171         /**
172          * Starts the observer. And makes sure up-to-date autoclick delay is propagated to
173          * |clickScheduler|.
174          *
175          * @param contentResolver Content resolver that should be observed for setting's value
176          *     changes.
177          * @param clickScheduler ClickScheduler that should be updated when click delay changes.
178          * @throws IllegalStateException If internal state is already setup when the method is
179          *         called.
180          * @throws NullPointerException If any of the arguments is a null pointer.
181          */
start(@onNull ContentResolver contentResolver, @NonNull ClickScheduler clickScheduler)182         public void start(@NonNull ContentResolver contentResolver,
183                 @NonNull ClickScheduler clickScheduler) {
184             if (mContentResolver != null || mClickScheduler != null) {
185                 throw new IllegalStateException("Observer already started.");
186             }
187             if (contentResolver == null) {
188                 throw new NullPointerException("contentResolver not set.");
189             }
190             if (clickScheduler == null) {
191                 throw new NullPointerException("clickScheduler not set.");
192             }
193 
194             mContentResolver = contentResolver;
195             mClickScheduler = clickScheduler;
196             mContentResolver.registerContentObserver(mAutoclickDelaySettingUri, false, this,
197                     mUserId);
198 
199             // Initialize mClickScheduler's initial delay value.
200             onChange(true, mAutoclickDelaySettingUri);
201         }
202 
203         /**
204          * Stops the the observer. Should only be called if the observer has been started.
205          *
206          * @throws IllegalStateException If internal state hasn't yet been initialized by calling
207          *         {@link #start}.
208          */
stop()209         public void stop() {
210             if (mContentResolver == null || mClickScheduler == null) {
211                 throw new IllegalStateException("ClickDelayObserver not started.");
212             }
213 
214             mContentResolver.unregisterContentObserver(this);
215         }
216 
217         @Override
onChange(boolean selfChange, Uri uri)218         public void onChange(boolean selfChange, Uri uri) {
219             if (mAutoclickDelaySettingUri.equals(uri)) {
220                 int delay = Settings.Secure.getIntForUser(
221                         mContentResolver, Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY,
222                         AccessibilityManager.AUTOCLICK_DELAY_DEFAULT, mUserId);
223                 mClickScheduler.updateDelay(delay);
224             }
225         }
226     }
227 
228     /**
229      * Schedules and performs click event sequence that should be initiated when mouse pointer stops
230      * moving. The click is first scheduled when a mouse movement is detected, and then further
231      * delayed on every sufficient mouse movement.
232      */
233     final private class ClickScheduler implements Runnable {
234         /**
235          * Minimal distance pointer has to move relative to anchor in order for movement not to be
236          * discarded as noise. Anchor is the position of the last MOVE event that was not considered
237          * noise.
238          */
239         private static final double MOVEMENT_SLOPE = 20f;
240 
241         /** Whether there is pending click. */
242         private boolean mActive;
243         /** If active, time at which pending click is scheduled. */
244         private long mScheduledClickTime;
245 
246         /** Last observed motion event. null if no events have been observed yet. */
247         private MotionEvent mLastMotionEvent;
248         /** Last observed motion event's policy flags. */
249         private int mEventPolicyFlags;
250         /** Current meta state. This value will be used as meta state for click event sequence. */
251         private int mMetaState;
252 
253         /**
254          * The current anchor's coordinates. Should be ignored if #mLastMotionEvent is null.
255          * Note that these are not necessary coords of #mLastMotionEvent (because last observed
256          * motion event may have been labeled as noise).
257          */
258         private PointerCoords mAnchorCoords;
259 
260         /** Delay that should be used to schedule click. */
261         private int mDelay;
262 
263         /** Handler for scheduling delayed operations. */
264         private Handler mHandler;
265 
266         private PointerProperties mTempPointerProperties[];
267         private PointerCoords mTempPointerCoords[];
268 
ClickScheduler(Handler handler, int delay)269         public ClickScheduler(Handler handler, int delay) {
270             mHandler = handler;
271 
272             mLastMotionEvent = null;
273             resetInternalState();
274             mDelay = delay;
275             mAnchorCoords = new PointerCoords();
276         }
277 
278         @Override
run()279         public void run() {
280             long now = SystemClock.uptimeMillis();
281             // Click was rescheduled after task was posted. Post new run task at updated time.
282             if (now < mScheduledClickTime) {
283                 mHandler.postDelayed(this, mScheduledClickTime - now);
284                 return;
285             }
286 
287             sendClick();
288             resetInternalState();
289         }
290 
291         /**
292          * Updates properties that should be used for click event sequence initiated by this object,
293          * as well as the time at which click will be scheduled.
294          * Should be called whenever new motion event is observed.
295          *
296          * @param event Motion event whose properties should be used as a base for click event
297          *     sequence.
298          * @param policyFlags Policy flags that should be send with click event sequence.
299          */
update(MotionEvent event, int policyFlags)300         public void update(MotionEvent event, int policyFlags) {
301             mMetaState = event.getMetaState();
302 
303             boolean moved = detectMovement(event);
304             cacheLastEvent(event, policyFlags, mLastMotionEvent == null || moved /* useAsAnchor */);
305 
306             if (moved) {
307               rescheduleClick(mDelay);
308             }
309         }
310 
311         /** Cancels any pending clicks and resets the object state. */
cancel()312         public void cancel() {
313             if (!mActive) {
314                 return;
315             }
316             resetInternalState();
317             mHandler.removeCallbacks(this);
318         }
319 
320         /**
321          * Updates the meta state that should be used for click sequence.
322          */
updateMetaState(int state)323         public void updateMetaState(int state) {
324             mMetaState = state;
325         }
326 
327         /**
328          * Updates delay that should be used when scheduling clicks. The delay will be used only for
329          * clicks scheduled after this point (pending click tasks are not affected).
330          * @param delay New delay value.
331          */
updateDelay(int delay)332         public void updateDelay(int delay) {
333             mDelay = delay;
334         }
335 
336         /**
337          * Updates the time at which click sequence should occur.
338          *
339          * @param delay Delay (from now) after which click should occur.
340          */
rescheduleClick(int delay)341         private void rescheduleClick(int delay) {
342             long clickTime = SystemClock.uptimeMillis() + delay;
343             // If there already is a scheduled click at time before the updated time, just update
344             // scheduled time. The click will actually be rescheduled when pending callback is
345             // run.
346             if (mActive && clickTime > mScheduledClickTime) {
347                 mScheduledClickTime = clickTime;
348                 return;
349             }
350 
351             if (mActive) {
352                 mHandler.removeCallbacks(this);
353             }
354 
355             mActive = true;
356             mScheduledClickTime = clickTime;
357 
358             mHandler.postDelayed(this, delay);
359         }
360 
361         /**
362          * Updates last observed motion event.
363          *
364          * @param event The last observed event.
365          * @param policyFlags The policy flags used with the last observed event.
366          * @param useAsAnchor Whether the event coords should be used as a new anchor.
367          */
cacheLastEvent(MotionEvent event, int policyFlags, boolean useAsAnchor)368         private void cacheLastEvent(MotionEvent event, int policyFlags, boolean useAsAnchor) {
369             if (mLastMotionEvent != null) {
370                 mLastMotionEvent.recycle();
371             }
372             mLastMotionEvent = MotionEvent.obtain(event);
373             mEventPolicyFlags = policyFlags;
374 
375             if (useAsAnchor) {
376                 final int pointerIndex = mLastMotionEvent.getActionIndex();
377                 mLastMotionEvent.getPointerCoords(pointerIndex, mAnchorCoords);
378             }
379         }
380 
resetInternalState()381         private void resetInternalState() {
382             mActive = false;
383             if (mLastMotionEvent != null) {
384                 mLastMotionEvent.recycle();
385                 mLastMotionEvent = null;
386             }
387             mScheduledClickTime = -1;
388         }
389 
390         /**
391          * @param event Observed motion event.
392          * @return Whether the event coords are far enough from the anchor for the event not to be
393          *     considered noise.
394          */
detectMovement(MotionEvent event)395         private boolean detectMovement(MotionEvent event) {
396             if (mLastMotionEvent == null) {
397                 return false;
398             }
399             final int pointerIndex = event.getActionIndex();
400             float deltaX = mAnchorCoords.x - event.getX(pointerIndex);
401             float deltaY = mAnchorCoords.y - event.getY(pointerIndex);
402             double delta = Math.hypot(deltaX, deltaY);
403             return delta > MOVEMENT_SLOPE;
404         }
405 
406         /**
407          * Creates and forwards click event sequence.
408          */
sendClick()409         private void sendClick() {
410             if (mLastMotionEvent == null || getNext() == null) {
411                 return;
412             }
413 
414             final int pointerIndex = mLastMotionEvent.getActionIndex();
415 
416             if (mTempPointerProperties == null) {
417                 mTempPointerProperties = new PointerProperties[1];
418                 mTempPointerProperties[0] = new PointerProperties();
419             }
420 
421             mLastMotionEvent.getPointerProperties(pointerIndex, mTempPointerProperties[0]);
422 
423             if (mTempPointerCoords == null) {
424                 mTempPointerCoords = new PointerCoords[1];
425                 mTempPointerCoords[0] = new PointerCoords();
426             }
427             mLastMotionEvent.getPointerCoords(pointerIndex, mTempPointerCoords[0]);
428 
429             final long now = SystemClock.uptimeMillis();
430 
431             MotionEvent downEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1,
432                     mTempPointerProperties, mTempPointerCoords, mMetaState,
433                     MotionEvent.BUTTON_PRIMARY, 1.0f, 1.0f, mLastMotionEvent.getDeviceId(), 0,
434                     mLastMotionEvent.getSource(), mLastMotionEvent.getFlags());
435 
436             MotionEvent pressEvent = MotionEvent.obtain(downEvent);
437             pressEvent.setAction(MotionEvent.ACTION_BUTTON_PRESS);
438             pressEvent.setActionButton(MotionEvent.BUTTON_PRIMARY);
439 
440             MotionEvent releaseEvent = MotionEvent.obtain(downEvent);
441             releaseEvent.setAction(MotionEvent.ACTION_BUTTON_RELEASE);
442             releaseEvent.setActionButton(MotionEvent.BUTTON_PRIMARY);
443             releaseEvent.setButtonState(0);
444 
445             MotionEvent upEvent = MotionEvent.obtain(downEvent);
446             upEvent.setAction(MotionEvent.ACTION_UP);
447             upEvent.setButtonState(0);
448 
449             AutoclickController.super.onMotionEvent(downEvent, downEvent, mEventPolicyFlags);
450             downEvent.recycle();
451 
452             AutoclickController.super.onMotionEvent(pressEvent, pressEvent, mEventPolicyFlags);
453             pressEvent.recycle();
454 
455             AutoclickController.super.onMotionEvent(releaseEvent, releaseEvent, mEventPolicyFlags);
456             releaseEvent.recycle();
457 
458             AutoclickController.super.onMotionEvent(upEvent, upEvent, mEventPolicyFlags);
459             upEvent.recycle();
460         }
461 
462         @Override
toString()463         public String toString() {
464             StringBuilder builder = new StringBuilder();
465             builder.append("ClickScheduler: { active=").append(mActive);
466             builder.append(", delay=").append(mDelay);
467             builder.append(", scheduledClickTime=").append(mScheduledClickTime);
468             builder.append(", anchor={x:").append(mAnchorCoords.x);
469             builder.append(", y:").append(mAnchorCoords.y).append("}");
470             builder.append(", metastate=").append(mMetaState);
471             builder.append(", policyFlags=").append(mEventPolicyFlags);
472             builder.append(", lastMotionEvent=").append(mLastMotionEvent);
473             builder.append(" }");
474             return builder.toString();
475         }
476     }
477 }
478