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