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