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