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