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