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