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