| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.accessibility; |
| |
| import android.annotation.NonNull; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.ContentObserver; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.SystemClock; |
| import android.provider.Settings; |
| import android.view.InputDevice; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.MotionEvent.PointerCoords; |
| import android.view.MotionEvent.PointerProperties; |
| import android.view.accessibility.AccessibilityManager; |
| |
| /** |
| * Implements "Automatically click on mouse stop" feature. |
| * |
| * If enabled, it will observe motion events from mouse source, and send click event sequence |
| * shortly after mouse stops moving. The click will only be performed if mouse movement had been |
| * actually detected. |
| * |
| * Movement detection has tolerance to jitter that may be caused by poor motor control to prevent: |
| * <ul> |
| * <li>Initiating unwanted clicks with no mouse movement.</li> |
| * <li>Autoclick never occurring after mouse arriving at target.</li> |
| * </ul> |
| * |
| * Non-mouse motion events, key events (excluding modifiers) and non-movement mouse events cancel |
| * the automatic click. |
| * |
| * It is expected that each instance will receive mouse events from a single mouse device. User of |
| * the class should handle cases where multiple mouse devices are present. |
| * |
| * Each instance is associated to a single user (and it does not handle user switch itself). |
| */ |
| public class AutoclickController extends BaseEventStreamTransformation { |
| |
| private static final String LOG_TAG = AutoclickController.class.getSimpleName(); |
| |
| private final Context mContext; |
| private final int mUserId; |
| |
| // Lazily created on the first mouse motion event. |
| private ClickScheduler mClickScheduler; |
| private ClickDelayObserver mClickDelayObserver; |
| |
| public AutoclickController(Context context, int userId) { |
| mContext = context; |
| mUserId = userId; |
| } |
| |
| @Override |
| public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { |
| if (mClickScheduler == null) { |
| Handler handler = new Handler(mContext.getMainLooper()); |
| mClickScheduler = |
| new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT); |
| mClickDelayObserver = new ClickDelayObserver(mUserId, handler); |
| mClickDelayObserver.start(mContext.getContentResolver(), mClickScheduler); |
| } |
| |
| handleMouseMotion(event, policyFlags); |
| } else if (mClickScheduler != null) { |
| mClickScheduler.cancel(); |
| } |
| |
| super.onMotionEvent(event, rawEvent, policyFlags); |
| } |
| |
| @Override |
| public void onKeyEvent(KeyEvent event, int policyFlags) { |
| if (mClickScheduler != null) { |
| if (KeyEvent.isModifierKey(event.getKeyCode())) { |
| mClickScheduler.updateMetaState(event.getMetaState()); |
| } else { |
| mClickScheduler.cancel(); |
| } |
| } |
| |
| super.onKeyEvent(event, policyFlags); |
| } |
| |
| @Override |
| public void clearEvents(int inputSource) { |
| if (inputSource == InputDevice.SOURCE_MOUSE && mClickScheduler != null) { |
| mClickScheduler.cancel(); |
| } |
| |
| super.clearEvents(inputSource); |
| } |
| |
| @Override |
| public void onDestroy() { |
| if (mClickDelayObserver != null) { |
| mClickDelayObserver.stop(); |
| mClickDelayObserver = null; |
| } |
| if (mClickScheduler != null) { |
| mClickScheduler.cancel(); |
| mClickScheduler = null; |
| } |
| } |
| |
| private void handleMouseMotion(MotionEvent event, int policyFlags) { |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_HOVER_MOVE: { |
| if (event.getPointerCount() == 1) { |
| mClickScheduler.update(event, policyFlags); |
| } else { |
| mClickScheduler.cancel(); |
| } |
| } break; |
| // Ignore hover enter and exit. |
| case MotionEvent.ACTION_HOVER_ENTER: |
| case MotionEvent.ACTION_HOVER_EXIT: |
| break; |
| default: |
| mClickScheduler.cancel(); |
| } |
| } |
| |
| /** |
| * Observes setting value for autoclick delay, and updates ClickScheduler delay whenever the |
| * setting value changes. |
| */ |
| final private static class ClickDelayObserver extends ContentObserver { |
| /** URI used to identify the autoclick delay setting with content resolver. */ |
| private final Uri mAutoclickDelaySettingUri = Settings.Secure.getUriFor( |
| Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY); |
| |
| private ContentResolver mContentResolver; |
| private ClickScheduler mClickScheduler; |
| private final int mUserId; |
| |
| public ClickDelayObserver(int userId, Handler handler) { |
| super(handler); |
| mUserId = userId; |
| } |
| |
| /** |
| * Starts the observer. And makes sure up-to-date autoclick delay is propagated to |
| * |clickScheduler|. |
| * |
| * @param contentResolver Content resolver that should be observed for setting's value |
| * changes. |
| * @param clickScheduler ClickScheduler that should be updated when click delay changes. |
| * @throws IllegalStateException If internal state is already setup when the method is |
| * called. |
| * @throws NullPointerException If any of the arguments is a null pointer. |
| */ |
| public void start(@NonNull ContentResolver contentResolver, |
| @NonNull ClickScheduler clickScheduler) { |
| if (mContentResolver != null || mClickScheduler != null) { |
| throw new IllegalStateException("Observer already started."); |
| } |
| if (contentResolver == null) { |
| throw new NullPointerException("contentResolver not set."); |
| } |
| if (clickScheduler == null) { |
| throw new NullPointerException("clickScheduler not set."); |
| } |
| |
| mContentResolver = contentResolver; |
| mClickScheduler = clickScheduler; |
| mContentResolver.registerContentObserver(mAutoclickDelaySettingUri, false, this, |
| mUserId); |
| |
| // Initialize mClickScheduler's initial delay value. |
| onChange(true, mAutoclickDelaySettingUri); |
| } |
| |
| /** |
| * Stops the the observer. Should only be called if the observer has been started. |
| * |
| * @throws IllegalStateException If internal state hasn't yet been initialized by calling |
| * {@link #start}. |
| */ |
| public void stop() { |
| if (mContentResolver == null || mClickScheduler == null) { |
| throw new IllegalStateException("ClickDelayObserver not started."); |
| } |
| |
| mContentResolver.unregisterContentObserver(this); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange, Uri uri) { |
| if (mAutoclickDelaySettingUri.equals(uri)) { |
| int delay = Settings.Secure.getIntForUser( |
| mContentResolver, Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY, |
| AccessibilityManager.AUTOCLICK_DELAY_DEFAULT, mUserId); |
| mClickScheduler.updateDelay(delay); |
| } |
| } |
| } |
| |
| /** |
| * Schedules and performs click event sequence that should be initiated when mouse pointer stops |
| * moving. The click is first scheduled when a mouse movement is detected, and then further |
| * delayed on every sufficient mouse movement. |
| */ |
| final private class ClickScheduler implements Runnable { |
| /** |
| * Minimal distance pointer has to move relative to anchor in order for movement not to be |
| * discarded as noise. Anchor is the position of the last MOVE event that was not considered |
| * noise. |
| */ |
| private static final double MOVEMENT_SLOPE = 20f; |
| |
| /** Whether there is pending click. */ |
| private boolean mActive; |
| /** If active, time at which pending click is scheduled. */ |
| private long mScheduledClickTime; |
| |
| /** Last observed motion event. null if no events have been observed yet. */ |
| private MotionEvent mLastMotionEvent; |
| /** Last observed motion event's policy flags. */ |
| private int mEventPolicyFlags; |
| /** Current meta state. This value will be used as meta state for click event sequence. */ |
| private int mMetaState; |
| |
| /** |
| * The current anchor's coordinates. Should be ignored if #mLastMotionEvent is null. |
| * Note that these are not necessary coords of #mLastMotionEvent (because last observed |
| * motion event may have been labeled as noise). |
| */ |
| private PointerCoords mAnchorCoords; |
| |
| /** Delay that should be used to schedule click. */ |
| private int mDelay; |
| |
| /** Handler for scheduling delayed operations. */ |
| private Handler mHandler; |
| |
| private PointerProperties mTempPointerProperties[]; |
| private PointerCoords mTempPointerCoords[]; |
| |
| public ClickScheduler(Handler handler, int delay) { |
| mHandler = handler; |
| |
| mLastMotionEvent = null; |
| resetInternalState(); |
| mDelay = delay; |
| mAnchorCoords = new PointerCoords(); |
| } |
| |
| @Override |
| public void run() { |
| long now = SystemClock.uptimeMillis(); |
| // Click was rescheduled after task was posted. Post new run task at updated time. |
| if (now < mScheduledClickTime) { |
| mHandler.postDelayed(this, mScheduledClickTime - now); |
| return; |
| } |
| |
| sendClick(); |
| resetInternalState(); |
| } |
| |
| /** |
| * Updates properties that should be used for click event sequence initiated by this object, |
| * as well as the time at which click will be scheduled. |
| * Should be called whenever new motion event is observed. |
| * |
| * @param event Motion event whose properties should be used as a base for click event |
| * sequence. |
| * @param policyFlags Policy flags that should be send with click event sequence. |
| */ |
| public void update(MotionEvent event, int policyFlags) { |
| mMetaState = event.getMetaState(); |
| |
| boolean moved = detectMovement(event); |
| cacheLastEvent(event, policyFlags, mLastMotionEvent == null || moved /* useAsAnchor */); |
| |
| if (moved) { |
| rescheduleClick(mDelay); |
| } |
| } |
| |
| /** Cancels any pending clicks and resets the object state. */ |
| public void cancel() { |
| if (!mActive) { |
| return; |
| } |
| resetInternalState(); |
| mHandler.removeCallbacks(this); |
| } |
| |
| /** |
| * Updates the meta state that should be used for click sequence. |
| */ |
| public void updateMetaState(int state) { |
| mMetaState = state; |
| } |
| |
| /** |
| * Updates delay that should be used when scheduling clicks. The delay will be used only for |
| * clicks scheduled after this point (pending click tasks are not affected). |
| * @param delay New delay value. |
| */ |
| public void updateDelay(int delay) { |
| mDelay = delay; |
| } |
| |
| /** |
| * Updates the time at which click sequence should occur. |
| * |
| * @param delay Delay (from now) after which click should occur. |
| */ |
| private void rescheduleClick(int delay) { |
| long clickTime = SystemClock.uptimeMillis() + delay; |
| // If there already is a scheduled click at time before the updated time, just update |
| // scheduled time. The click will actually be rescheduled when pending callback is |
| // run. |
| if (mActive && clickTime > mScheduledClickTime) { |
| mScheduledClickTime = clickTime; |
| return; |
| } |
| |
| if (mActive) { |
| mHandler.removeCallbacks(this); |
| } |
| |
| mActive = true; |
| mScheduledClickTime = clickTime; |
| |
| mHandler.postDelayed(this, delay); |
| } |
| |
| /** |
| * Updates last observed motion event. |
| * |
| * @param event The last observed event. |
| * @param policyFlags The policy flags used with the last observed event. |
| * @param useAsAnchor Whether the event coords should be used as a new anchor. |
| */ |
| private void cacheLastEvent(MotionEvent event, int policyFlags, boolean useAsAnchor) { |
| if (mLastMotionEvent != null) { |
| mLastMotionEvent.recycle(); |
| } |
| mLastMotionEvent = MotionEvent.obtain(event); |
| mEventPolicyFlags = policyFlags; |
| |
| if (useAsAnchor) { |
| final int pointerIndex = mLastMotionEvent.getActionIndex(); |
| mLastMotionEvent.getPointerCoords(pointerIndex, mAnchorCoords); |
| } |
| } |
| |
| private void resetInternalState() { |
| mActive = false; |
| if (mLastMotionEvent != null) { |
| mLastMotionEvent.recycle(); |
| mLastMotionEvent = null; |
| } |
| mScheduledClickTime = -1; |
| } |
| |
| /** |
| * @param event Observed motion event. |
| * @return Whether the event coords are far enough from the anchor for the event not to be |
| * considered noise. |
| */ |
| private boolean detectMovement(MotionEvent event) { |
| if (mLastMotionEvent == null) { |
| return false; |
| } |
| final int pointerIndex = event.getActionIndex(); |
| float deltaX = mAnchorCoords.x - event.getX(pointerIndex); |
| float deltaY = mAnchorCoords.y - event.getY(pointerIndex); |
| double delta = Math.hypot(deltaX, deltaY); |
| return delta > MOVEMENT_SLOPE; |
| } |
| |
| /** |
| * Creates and forwards click event sequence. |
| */ |
| private void sendClick() { |
| if (mLastMotionEvent == null || getNext() == null) { |
| return; |
| } |
| |
| final int pointerIndex = mLastMotionEvent.getActionIndex(); |
| |
| if (mTempPointerProperties == null) { |
| mTempPointerProperties = new PointerProperties[1]; |
| mTempPointerProperties[0] = new PointerProperties(); |
| } |
| |
| mLastMotionEvent.getPointerProperties(pointerIndex, mTempPointerProperties[0]); |
| |
| if (mTempPointerCoords == null) { |
| mTempPointerCoords = new PointerCoords[1]; |
| mTempPointerCoords[0] = new PointerCoords(); |
| } |
| mLastMotionEvent.getPointerCoords(pointerIndex, mTempPointerCoords[0]); |
| |
| final long now = SystemClock.uptimeMillis(); |
| |
| MotionEvent downEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1, |
| mTempPointerProperties, mTempPointerCoords, mMetaState, |
| MotionEvent.BUTTON_PRIMARY, 1.0f, 1.0f, mLastMotionEvent.getDeviceId(), 0, |
| mLastMotionEvent.getSource(), mLastMotionEvent.getFlags()); |
| |
| // The only real difference between these two events is the action flag. |
| MotionEvent upEvent = MotionEvent.obtain(downEvent); |
| upEvent.setAction(MotionEvent.ACTION_UP); |
| |
| AutoclickController.super.onMotionEvent(downEvent, downEvent, mEventPolicyFlags); |
| downEvent.recycle(); |
| |
| AutoclickController.super.onMotionEvent(upEvent, upEvent, mEventPolicyFlags); |
| upEvent.recycle(); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| builder.append("ClickScheduler: { active=").append(mActive); |
| builder.append(", delay=").append(mDelay); |
| builder.append(", scheduledClickTime=").append(mScheduledClickTime); |
| builder.append(", anchor={x:").append(mAnchorCoords.x); |
| builder.append(", y:").append(mAnchorCoords.y).append("}"); |
| builder.append(", metastate=").append(mMetaState); |
| builder.append(", policyFlags=").append(mEventPolicyFlags); |
| builder.append(", lastMotionEvent=").append(mLastMotionEvent); |
| builder.append(" }"); |
| return builder.toString(); |
| } |
| } |
| } |