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