AutoclickController.java revision c9547607048a7f875784a36cd3370332f9c8befd
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
17package com.android.server.accessibility;
18
19import android.content.Context;
20import android.os.Handler;
21import android.os.SystemClock;
22import android.view.InputDevice;
23import android.view.KeyEvent;
24import android.view.MotionEvent;
25import android.view.MotionEvent.PointerCoords;
26import android.view.MotionEvent.PointerProperties;
27import android.view.accessibility.AccessibilityEvent;
28
29/**
30 * Implements "Automatically click on mouse stop" feature.
31 *
32 * If enabled, it will observe motion events from mouse source, and send click event sequence
33 * shortly after mouse stops moving. The click will only be performed if mouse movement had been
34 * actually detected.
35 *
36 * Movement detection has tolerance to jitter that may be caused by poor motor control to prevent:
37 * <ul>
38 *   <li>Initiating unwanted clicks with no mouse movement.</li>
39 *   <li>Autoclick never occurring after mouse arriving at target.</li>
40 * </ul>
41 *
42 * Non-mouse motion events, key events (excluding modifiers) and non-movement mouse events cancel
43 * the automatic click.
44 *
45 * It is expected that each instance will receive mouse events from a single mouse device. User of
46 * the class should handle cases where multiple mouse devices are present.
47 */
48public class AutoclickController implements EventStreamTransformation {
49    private static final String LOG_TAG = AutoclickController.class.getSimpleName();
50
51    // TODO: Control click delay via settings.
52    private static final int CLICK_DELAY_MS = 600;
53
54    private EventStreamTransformation mNext;
55    private Context mContext;
56
57    // Lazily created on the first mouse motion event.
58    private ClickScheduler mClickScheduler;
59
60    public AutoclickController(Context context) {
61        mContext = context;
62    }
63
64    @Override
65    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
66        if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
67            if (mClickScheduler == null) {
68                Handler handler = new Handler(mContext.getMainLooper());
69                mClickScheduler = new ClickScheduler(handler, CLICK_DELAY_MS);
70            }
71
72            handleMouseMotion(event, policyFlags);
73        } else if (mClickScheduler != null) {
74            mClickScheduler.cancel();
75        }
76
77        if (mNext != null) {
78            mNext.onMotionEvent(event, rawEvent, policyFlags);
79        }
80    }
81
82    @Override
83    public void onKeyEvent(KeyEvent event, int policyFlags) {
84        if (mClickScheduler != null) {
85            if (KeyEvent.isModifierKey(event.getKeyCode())) {
86                mClickScheduler.updateMetaState(event.getMetaState());
87            } else {
88                mClickScheduler.cancel();
89            }
90        }
91
92        if (mNext != null) {
93          mNext.onKeyEvent(event, policyFlags);
94        }
95    }
96
97    @Override
98    public void onAccessibilityEvent(AccessibilityEvent event) {
99        if (mNext != null) {
100            mNext.onAccessibilityEvent(event);
101        }
102    }
103
104    @Override
105    public void setNext(EventStreamTransformation next) {
106        mNext = next;
107    }
108
109    @Override
110    public void clearEvents(int inputSource) {
111        if (inputSource == InputDevice.SOURCE_MOUSE && mClickScheduler != null) {
112            mClickScheduler.cancel();
113        }
114
115        if (mNext != null) {
116            mNext.clearEvents(inputSource);
117        }
118    }
119
120    @Override
121    public void onDestroy() {
122        if (mClickScheduler != null) {
123            mClickScheduler.cancel();
124        }
125    }
126
127    private void handleMouseMotion(MotionEvent event, int policyFlags) {
128        switch (event.getActionMasked()) {
129            case MotionEvent.ACTION_HOVER_MOVE: {
130                if (event.getPointerCount() == 1) {
131                    mClickScheduler.update(event, policyFlags);
132                } else {
133                    mClickScheduler.cancel();
134                }
135            } break;
136            // Ignore hover enter and exit.
137            case MotionEvent.ACTION_HOVER_ENTER:
138            case MotionEvent.ACTION_HOVER_EXIT:
139                break;
140            default:
141                mClickScheduler.cancel();
142        }
143    }
144
145    /**
146     * Schedules and performs click event sequence that should be initiated when mouse pointer stops
147     * moving. The click is first scheduled when a mouse movement is detected, and then further
148     * delayed on every sufficient mouse movement.
149     */
150    final private class ClickScheduler implements Runnable {
151        /**
152         * Minimal distance pointer has to move relative to anchor in order for movement not to be
153         * discarded as noise. Anchor is the position of the last MOVE event that was not considered
154         * noise.
155         */
156        private static final double MOVEMENT_SLOPE = 20f;
157
158        /** Whether there is pending click. */
159        private boolean mActive;
160        /** If active, time at which pending click is scheduled. */
161        private long mScheduledClickTime;
162
163        /** Last observed motion event. null if no events have been observed yet. */
164        private MotionEvent mLastMotionEvent;
165        /** Last observed motion event's policy flags. */
166        private int mEventPolicyFlags;
167        /** Current meta state. This value will be used as meta state for click event sequence. */
168        private int mMetaState;
169
170        /**
171         * The current anchor's coordinates. Should be ignored if #mLastMotionEvent is null.
172         * Note that these are not necessary coords of #mLastMotionEvent (because last observed
173         * motion event may have been labeled as noise).
174         */
175        private PointerCoords mAnchorCoords;
176
177        /** Delay that should be used to schedule click. */
178        private int mDelay;
179
180        /** Handler for scheduling delayed operations. */
181        private Handler mHandler;
182
183        private PointerProperties mTempPointerProperties[];
184        private PointerCoords mTempPointerCoords[];
185
186        public ClickScheduler(Handler handler, int delay) {
187            mHandler = handler;
188
189            mLastMotionEvent = null;
190            resetInternalState();
191            mDelay = delay;
192            mAnchorCoords = new PointerCoords();
193        }
194
195        @Override
196        public void run() {
197            long now = SystemClock.uptimeMillis();
198            // Click was rescheduled after task was posted. Post new run task at updated time.
199            if (now < mScheduledClickTime) {
200                mHandler.postDelayed(this, mScheduledClickTime - now);
201                return;
202            }
203
204            sendClick();
205            resetInternalState();
206        }
207
208        /**
209         * Updates properties that should be used for click event sequence initiated by this object,
210         * as well as the time at which click will be scheduled.
211         * Should be called whenever new motion event is observed.
212         *
213         * @param event Motion event whose properties should be used as a base for click event
214         *     sequence.
215         * @param policyFlags Policy flags that should be send with click event sequence.
216         */
217        public void update(MotionEvent event, int policyFlags) {
218            mMetaState = event.getMetaState();
219
220            boolean moved = detectMovement(event);
221            cacheLastEvent(event, policyFlags, mLastMotionEvent == null || moved /* useAsAnchor */);
222
223            if (moved) {
224              rescheduleClick(mDelay);
225            }
226        }
227
228        /** Cancels any pending clicks and resets the object state. */
229        public void cancel() {
230            if (!mActive) {
231                return;
232            }
233            resetInternalState();
234            mHandler.removeCallbacks(this);
235        }
236
237        /**
238         * Updates the meta state that should be used for click sequence.
239         */
240        public void updateMetaState(int state) {
241            mMetaState = state;
242        }
243
244        /**
245         * Updates the time at which click sequence should occur.
246         *
247         * @param delay Delay (from now) after which click should occur.
248         */
249        private void rescheduleClick(int delay) {
250            long clickTime = SystemClock.uptimeMillis() + delay;
251            // If there already is a scheduled click at time before the updated time, just update
252            // scheduled time. The click will actually be rescheduled when pending callback is
253            // run.
254            if (mActive && clickTime > mScheduledClickTime) {
255                mScheduledClickTime = clickTime;
256                return;
257            }
258
259            if (mActive) {
260                mHandler.removeCallbacks(this);
261            }
262
263            mActive = true;
264            mScheduledClickTime = clickTime;
265
266            mHandler.postDelayed(this, delay);
267        }
268
269        /**
270         * Updates last observed motion event.
271         *
272         * @param event The last observed event.
273         * @param policyFlags The policy flags used with the last observed event.
274         * @param useAsAnchor Whether the event coords should be used as a new anchor.
275         */
276        private void cacheLastEvent(MotionEvent event, int policyFlags, boolean useAsAnchor) {
277            if (mLastMotionEvent != null) {
278                mLastMotionEvent.recycle();
279            }
280            mLastMotionEvent = MotionEvent.obtain(event);
281            mEventPolicyFlags = policyFlags;
282
283            if (useAsAnchor) {
284                final int pointerIndex = mLastMotionEvent.getActionIndex();
285                mLastMotionEvent.getPointerCoords(pointerIndex, mAnchorCoords);
286            }
287        }
288
289        private void resetInternalState() {
290            mActive = false;
291            if (mLastMotionEvent != null) {
292                mLastMotionEvent.recycle();
293                mLastMotionEvent = null;
294            }
295            mScheduledClickTime = -1;
296        }
297
298        /**
299         * @param event Observed motion event.
300         * @return Whether the event coords are far enough from the anchor for the event not to be
301         *     considered noise.
302         */
303        private boolean detectMovement(MotionEvent event) {
304            if (mLastMotionEvent == null) {
305                return false;
306            }
307            final int pointerIndex = event.getActionIndex();
308            float deltaX = mAnchorCoords.x - event.getX(pointerIndex);
309            float deltaY = mAnchorCoords.y - event.getY(pointerIndex);
310            double delta = Math.hypot(deltaX, deltaY);
311            return delta > MOVEMENT_SLOPE;
312        }
313
314        /**
315         * Creates and forwards click event sequence.
316         */
317        private void sendClick() {
318            if (mLastMotionEvent == null || mNext == null) {
319                return;
320            }
321
322            final int pointerIndex = mLastMotionEvent.getActionIndex();
323
324            if (mTempPointerProperties == null) {
325                mTempPointerProperties = new PointerProperties[1];
326                mTempPointerProperties[0] = new PointerProperties();
327            }
328
329            mLastMotionEvent.getPointerProperties(pointerIndex, mTempPointerProperties[0]);
330
331            if (mTempPointerCoords == null) {
332                mTempPointerCoords = new PointerCoords[1];
333                mTempPointerCoords[0] = new PointerCoords();
334            }
335            mLastMotionEvent.getPointerCoords(pointerIndex, mTempPointerCoords[0]);
336
337            final long now = SystemClock.uptimeMillis();
338
339            MotionEvent downEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1,
340                    mTempPointerProperties, mTempPointerCoords, mMetaState,
341                    MotionEvent.BUTTON_PRIMARY, 1.0f, 1.0f, mLastMotionEvent.getDeviceId(), 0,
342                    mLastMotionEvent.getSource(), mLastMotionEvent.getFlags());
343
344            // The only real difference between these two events is the action flag.
345            MotionEvent upEvent = MotionEvent.obtain(downEvent);
346            upEvent.setAction(MotionEvent.ACTION_UP);
347
348            mNext.onMotionEvent(downEvent, downEvent, mEventPolicyFlags);
349            downEvent.recycle();
350
351            mNext.onMotionEvent(upEvent, upEvent, mEventPolicyFlags);
352            upEvent.recycle();
353        }
354
355        @Override
356        public String toString() {
357            StringBuilder builder = new StringBuilder();
358            builder.append("ClickScheduler: { active=").append(mActive);
359            builder.append(", delay=").append(mDelay);
360            builder.append(", scheduledClickTime=").append(mScheduledClickTime);
361            builder.append(", anchor={x:").append(mAnchorCoords.x);
362            builder.append(", y:").append(mAnchorCoords.y).append("}");
363            builder.append(", metastate=").append(mMetaState);
364            builder.append(", policyFlags=").append(mEventPolicyFlags);
365            builder.append(", lastMotionEvent=").append(mLastMotionEvent);
366            builder.append(" }");
367            return builder.toString();
368        }
369    }
370}
371