Home | History | Annotate | Download | only in accessibility
      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 
     66     public AutoclickController(Context context, int userId) {
     67         mContext = context;
     68         mUserId = userId;
     69     }
     70 
     71     @Override
     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
     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
    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
    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 
    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 
    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          */
    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          */
    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
    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 
    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
    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          */
    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. */
    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          */
    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          */
    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          */
    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          */
    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 
    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          */
    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          */
    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
    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