Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 976724e8 authored by Toni Barzic's avatar Toni Barzic
Browse files

Feed mouse and keyboard events to EventStreamTransformation

Updates AccessibilityInputFilter to pass keyboard and mouse events
through enabed EventStreamTransformations.

This is done in preparation for implementing Autoclick on mouse stop
feature, which depends on those events.

Existing EventStreamTransformation implementstions are modified to
non touchscreen motion events and key board events.

Adds stub EventStreamTransformation (AutoclickController) that will
be used to implement autoclick feature.

Implements key event filtering as a separate EventStreamTransformation
(instead of doing it directly in AccessibilityInputFilter).

Introduces private EventStreamState classes to AccessibilityInputFilter,
which keep track of whether event sequences for different input sources
have started, and help reduce code duplication when determining whether
an event should be fed to registered EventStreamTransformers.

BUG=23113412

Change-Id: If115799bbe4debce48689370ff5ea9fa6dab9639
parent 55a309f8
Loading
Loading
Loading
Loading
+304 −85
Original line number Diff line number Diff line
@@ -63,6 +63,13 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
     */
    static final int FLAG_FEATURE_FILTER_KEY_EVENTS = 0x00000004;

    /**
     * Flag for enabling "Automatically click on mouse stop" feature.
     *
     * @see #setEnabledFeatures(int)
     */
    static final int FLAG_FEATURE_AUTOCLICK = 0x00000008;

    private final Runnable mProcessBatchedEventsRunnable = new Runnable() {
        @Override
        public void run() {
@@ -88,8 +95,6 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo

    private final Choreographer mChoreographer;

    private int mCurrentTouchDeviceId;

    private boolean mInstalled;

    private int mEnabledFeatures;
@@ -98,17 +103,19 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo

    private ScreenMagnifier mScreenMagnifier;

    private AutoclickController mAutoclickController;

    private KeyboardInterceptor mKeyboardInterceptor;

    private EventStreamTransformation mEventHandler;

    private MotionEventHolder mEventQueue;

    private boolean mMotionEventSequenceStarted;
    private EventStreamState mMouseStreamState;

    private boolean mHoverEventSequenceStarted;
    private EventStreamState mTouchScreenStreamState;

    private boolean mKeyEventSequenceStarted;

    private boolean mFilterKeyEvents;
    private EventStreamState mKeyboardStreamState;

    AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
        super(context.getMainLooper());
@@ -145,86 +152,92 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
            Slog.d(TAG, "Received event: " + event + ", policyFlags=0x"
                    + Integer.toHexString(policyFlags));
        }
        if (event instanceof MotionEvent
                && event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
            MotionEvent motionEvent = (MotionEvent) event;
            onMotionEvent(motionEvent, policyFlags);
        } else if (event instanceof KeyEvent
                && event.isFromSource(InputDevice.SOURCE_KEYBOARD)) {
            KeyEvent keyEvent = (KeyEvent) event;
            onKeyEvent(keyEvent, policyFlags);
        } else {

        if (mEventHandler == null) {
            super.onInputEvent(event, policyFlags);
        }
            return;
        }

    private void onMotionEvent(MotionEvent event, int policyFlags) {
        if (mEventHandler == null) {
        EventStreamState state = getEventStreamState(event);
        if (state == null) {
            super.onInputEvent(event, policyFlags);
            return;
        }

        int eventSource = event.getSource();
        if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
            mMotionEventSequenceStarted = false;
            mHoverEventSequenceStarted = false;
            mEventHandler.clear();
            state.reset();
            mEventHandler.clearEvents(eventSource);
            super.onInputEvent(event, policyFlags);
            return;
        }
        final int deviceId = event.getDeviceId();
        if (mCurrentTouchDeviceId != deviceId) {
            mCurrentTouchDeviceId = deviceId;
            mMotionEventSequenceStarted = false;
            mHoverEventSequenceStarted = false;
            mEventHandler.clear();

        if (state.updateDeviceId(event.getDeviceId())) {
            mEventHandler.clearEvents(eventSource);
        }
        if (mCurrentTouchDeviceId < 0) {

        if (!state.deviceIdValid()) {
            super.onInputEvent(event, policyFlags);
            return;
        }
        // We do not handle scroll events.
        if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
            super.onInputEvent(event, policyFlags);
            return;

        if (event instanceof MotionEvent) {
            MotionEvent motionEvent = (MotionEvent) event;
            processMotionEvent(state, motionEvent, policyFlags);
        } else if (event instanceof KeyEvent) {
            KeyEvent keyEvent = (KeyEvent) event;
            processKeyEvent(state, keyEvent, policyFlags);
        }
        // Wait for a down touch event to start processing.
        if (event.isTouchEvent()) {
            if (!mMotionEventSequenceStarted) {
                if (event.getActionMasked() != MotionEvent.ACTION_DOWN) {
                    return;
    }
                mMotionEventSequenceStarted = true;

    /**
     * Gets current event stream state associated with an input event.
     * @return The event stream state that should be used for the event. Null if the event should
     *     not be handled by #AccessibilityInputFilter.
     */
    private EventStreamState getEventStreamState(InputEvent event) {
        if (event instanceof MotionEvent) {
          if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
              if (mTouchScreenStreamState == null) {
                  mTouchScreenStreamState = new TouchScreenEventStreamState();
              }
        } else {
            // Wait for an enter hover event to start processing.
            if (!mHoverEventSequenceStarted) {
                if (event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER) {
                    return;
              return mTouchScreenStreamState;
          }
          if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
              if (mMouseStreamState == null) {
                  mMouseStreamState = new MouseEventStreamState();
              }
                mHoverEventSequenceStarted = true;
              return mMouseStreamState;
          }
        } else if (event instanceof KeyEvent) {
          if (event.isFromSource(InputDevice.SOURCE_KEYBOARD)) {
              if (mKeyboardStreamState == null) {
                  mKeyboardStreamState = new KeyboardEventStreamState();
              }
        batchMotionEvent((MotionEvent) event, policyFlags);
              return mKeyboardStreamState;
          }
        }
        return null;
    }

    private void onKeyEvent(KeyEvent event, int policyFlags) {
        if (!mFilterKeyEvents) {
    private void processMotionEvent(EventStreamState state, MotionEvent event, int policyFlags) {
        if (!state.shouldProcessScroll() && event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
            super.onInputEvent(event, policyFlags);
            return;
        }
        if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
            mKeyEventSequenceStarted = false;
            super.onInputEvent(event, policyFlags);

        if (!state.shouldProcessMotionEvent(event)) {
            return;
        }
        // Wait for a down key event to start processing.
        if (!mKeyEventSequenceStarted) {
            if (event.getAction() != KeyEvent.ACTION_DOWN) {
                super.onInputEvent(event, policyFlags);
                return;

        batchMotionEvent(event, policyFlags);
    }
            mKeyEventSequenceStarted = true;

    private void processKeyEvent(EventStreamState state, KeyEvent event, int policyFlags) {
        if (!state.shouldProcessKeyEvent(event)) {
            return;
        }
        mAms.notifyKeyEvent(event, policyFlags);
        mEventHandler.onKeyEvent(event, policyFlags);
    }

    private void scheduleProcessBatchedEvents() {
@@ -292,6 +305,11 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
        sendInputEvent(transformedEvent, policyFlags);
    }

    @Override
    public void onKeyEvent(KeyEvent event, int policyFlags) {
        sendInputEvent(event, policyFlags);
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // TODO Implement this to inject the accessibility event
@@ -305,7 +323,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
    }

    @Override
    public void clear() {
    public void clearEvents(int inputSource) {
        /* do nothing */
    }

@@ -329,43 +347,77 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
    }

    private void enableFeatures() {
        mMotionEventSequenceStarted = false;
        mHoverEventSequenceStarted = false;
        if ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0) {
            mEventHandler = mScreenMagnifier = new ScreenMagnifier(mContext,
                    Display.DEFAULT_DISPLAY, mAms);
            mEventHandler.setNext(this);
        resetStreamState();

        if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) {
            mAutoclickController = new AutoclickController(mContext);
            addFirstEventHandler(mAutoclickController);
        }

        if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
            mTouchExplorer = new TouchExplorer(mContext, mAms);
            mTouchExplorer.setNext(this);
            if (mEventHandler != null) {
                mEventHandler.setNext(mTouchExplorer);
            } else {
                mEventHandler = mTouchExplorer;
            addFirstEventHandler(mTouchExplorer);
        }

        if ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0) {
            mScreenMagnifier = new ScreenMagnifier(mContext,
                    Display.DEFAULT_DISPLAY, mAms);
            addFirstEventHandler(mScreenMagnifier);
        }

        if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) {
            mFilterKeyEvents = true;
           mKeyboardInterceptor = new KeyboardInterceptor(mAms);
           addFirstEventHandler(mKeyboardInterceptor);
        }
    }

    /**
     * Adds an event handler to the event handler chain. The handler is added at the beginning of
     * the chain.
     *
     * @param handler The handler to be added to the event handlers list.
     */
    private void addFirstEventHandler(EventStreamTransformation handler) {
        if (mEventHandler != null) {
           handler.setNext(mEventHandler);
        } else {
            handler.setNext(this);
        }
        mEventHandler = handler;
    }

    void disableFeatures() {
        if (mAutoclickController != null) {
            mAutoclickController.onDestroy();
            mAutoclickController = null;
        }
        if (mTouchExplorer != null) {
            mTouchExplorer.clear();
            mTouchExplorer.onDestroy();
            mTouchExplorer = null;
        }
        if (mScreenMagnifier != null) {
            mScreenMagnifier.clear();
            mScreenMagnifier.onDestroy();
            mScreenMagnifier = null;
        }
        if (mKeyboardInterceptor != null) {
            mKeyboardInterceptor.onDestroy();
            mKeyboardInterceptor = null;
        }

        mEventHandler = null;
        mKeyEventSequenceStarted = false;
        mMotionEventSequenceStarted = false;
        mHoverEventSequenceStarted = false;
        mFilterKeyEvents = false;
        resetStreamState();
    }

    void resetStreamState() {
        if (mTouchScreenStreamState != null) {
            mTouchScreenStreamState.reset();
        }
        if (mMouseStreamState != null) {
            mMouseStreamState.reset();
        }
        if (mKeyboardStreamState != null) {
            mKeyboardStreamState.reset();
        }
    }

    @Override
@@ -402,4 +454,171 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
            sPool.release(this);
        }
    }

    /**
     * Keeps state of event streams observed for an input device with a certain source.
     * Provides information about whether motion and key events should be processed by accessibility
     * #EventStreamTransformations. Base implementation describes behaviour for event sources that
     * whose events should not be handled by a11y event stream transformations.
     */
    private static class EventStreamState {
        private int mDeviceId;

        EventStreamState() {
            mDeviceId = -1;
        }

        /**
         * Updates the ID of the device associated with the state. If the ID changes, resets
         * internal state.
         *
         * @param deviceId Updated input device ID.
         * @return Whether the device ID has changed.
         */
        public boolean updateDeviceId(int deviceId) {
            if (mDeviceId == deviceId) {
                return false;
            }
            // Reset clears internal state, so make sure it's called before |mDeviceId| is updated.
            reset();
            mDeviceId = deviceId;
            return true;
        }

        /**
         * @return Whether device ID is valid.
         */
        public boolean deviceIdValid() {
            return mDeviceId >= 0;
        }

        /**
         * Resets the event stream state.
         */
        public void reset() {
            mDeviceId = -1;
        }

        /**
         * @return Whether scroll events for device should be handled by event transformations.
         */
        public boolean shouldProcessScroll() {
            return false;
        }

        /**
         * @param event An observed motion event.
         * @return Whether the event should be handled by event transformations.
         */
        public boolean shouldProcessMotionEvent(MotionEvent event) {
            return false;
        }

        /**
         * @param event An observed key event.
         * @return Whether the event should be handled by event transformations.
         */
        public boolean shouldProcessKeyEvent(KeyEvent event) {
            return false;
        }
    }

    /**
     * Keeps state of stream of events from a mouse device.
     */
    private static class MouseEventStreamState extends EventStreamState {
        private boolean mMotionSequenceStarted;

        public MouseEventStreamState() {
            reset();
        }

        @Override
        final public void reset() {
            super.reset();
            mMotionSequenceStarted = false;
        }

        @Override
        final public boolean shouldProcessScroll() {
            return true;
        }

        @Override
        final public boolean shouldProcessMotionEvent(MotionEvent event) {
            if (mMotionSequenceStarted) {
                return true;
            }
            // Wait for down or move event to start processing mouse events.
            int action = event.getActionMasked();
            mMotionSequenceStarted =
                    action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_HOVER_MOVE;
            return mMotionSequenceStarted;
        }
    }

    /**
     * Keeps state of stream of events from a touch screen device.
     */
    private static class TouchScreenEventStreamState extends EventStreamState {
        private boolean mTouchSequenceStarted;
        private boolean mHoverSequenceStarted;

        public TouchScreenEventStreamState() {
            reset();
        }

        @Override
        final public void reset() {
            super.reset();
            mTouchSequenceStarted = false;
            mHoverSequenceStarted = false;
        }

        @Override
        final public boolean shouldProcessMotionEvent(MotionEvent event) {
            // Wait for a down touch event to start processing.
            if (event.isTouchEvent()) {
                if (mTouchSequenceStarted) {
                    return true;
                }
                mTouchSequenceStarted = event.getActionMasked() == MotionEvent.ACTION_DOWN;
                return mTouchSequenceStarted;
            }

            // Wait for an enter hover event to start processing.
            if (mHoverSequenceStarted) {
                return true;
            }
            mHoverSequenceStarted = event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER;
            return mHoverSequenceStarted;
        }
    }

    /**
     * Keeps state of stream of events from a keyboard device.
     */
    private static class KeyboardEventStreamState extends EventStreamState {
        private boolean mEventSequenceStarted;

        public KeyboardEventStreamState() {
            reset();
        }

        @Override
        final public void reset() {
            super.reset();
            mEventSequenceStarted = false;
        }

        @Override
        final public boolean shouldProcessKeyEvent(KeyEvent event) {
            // Wait for a down key event to start processing.
            if (mEventSequenceStarted) {
                return true;
            }
            mEventSequenceStarted = event.getAction() == KeyEvent.ACTION_DOWN;
            return mEventSequenceStarted;
        }
    }
}
+83 −0
Original line number Diff line number Diff line
/*
 * 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.content.Context;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;

/**
 * Implements "Automatically click on mouse stop" feature.
 *
 * If enabled, it will observe motion events from mouse source, and send click event sequence short
 * while 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.
 */
public class AutoclickController implements EventStreamTransformation {
    private EventStreamTransformation mNext;

    public AutoclickController(Context context) {}

    @Override
    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        // TODO: Implement this.
        if (mNext != null) {
            mNext.onMotionEvent(event, rawEvent, policyFlags);
        }
    }

    @Override
    public void onKeyEvent(KeyEvent event, int policyFlags) {
        // TODO: Implement this.
        if (mNext != null) {
          mNext.onKeyEvent(event, policyFlags);
        }
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (mNext != null) {
            mNext.onAccessibilityEvent(event);
        }
    }

    @Override
    public void setNext(EventStreamTransformation next) {
        mNext = next;
    }

    @Override
    public void clearEvents(int inputSource) {
        if (mNext != null) {
            mNext.clearEvents(inputSource);
        }
    }

    @Override
    public void onDestroy() {
    }
}
+12 −2
Original line number Diff line number Diff line
@@ -67,6 +67,14 @@ interface EventStreamTransformation {
     */
    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags);

    /**
     * Receives a key event.
     *
     * @param event The key event.
     * @param policyFlags Policy flags for the event.
     */
    public void onKeyEvent(KeyEvent event, int policyFlags);

    /**
     * Receives an accessibility event.
     *
@@ -82,9 +90,11 @@ interface EventStreamTransformation {
    public void setNext(EventStreamTransformation next);

    /**
     * Clears the internal state of this transformation.
     * Clears internal state associated with events from specific input source.
     *
     * @param inputSource The input source class for which transformation state should be cleared.
     */
    public void clear();
    public void clearEvents(int inputSource);

    /**
     * Destroys this transformation.
+68 −0
Original line number Diff line number Diff line
/*
 * 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.view.KeyEvent;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;

/**
 * Intercepts key events and forwards them to accessibility manager service.
 */
public class KeyboardInterceptor implements EventStreamTransformation {
    private EventStreamTransformation mNext;
    private AccessibilityManagerService mAms;

    public KeyboardInterceptor(AccessibilityManagerService service) {
        mAms = service;
    }

    @Override
    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        if (mNext != null) {
            mNext.onMotionEvent(event, rawEvent, policyFlags);
        }
    }

    @Override
    public void onKeyEvent(KeyEvent event, int policyFlags) {
        mAms.notifyKeyEvent(event, policyFlags);
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (mNext != null) {
            mNext.onAccessibilityEvent(event);
        }
    }

    @Override
    public void setNext(EventStreamTransformation next) {
        mNext = next;
    }

    @Override
    public void clearEvents(int inputSource) {
        if (mNext != null) {
            mNext.clearEvents(inputSource);
        }
    }

    @Override
    public void onDestroy() {
    }
}
+29 −6
Original line number Diff line number Diff line
@@ -37,6 +37,8 @@ import android.util.Slog;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MagnificationSpec;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
@@ -325,6 +327,12 @@ public final class ScreenMagnifier implements WindowManagerInternal.Magnificatio
    @Override
    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent,
            int policyFlags) {
        if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
            if (mNext != null) {
                mNext.onMotionEvent(event, rawEvent, policyFlags);
            }
            return;
        }
        mMagnifiedContentInteractonStateHandler.onMotionEvent(event);
        switch (mCurrentState) {
            case STATE_DELEGATING: {
@@ -347,6 +355,13 @@ public final class ScreenMagnifier implements WindowManagerInternal.Magnificatio
        }
    }

    @Override
    public void onKeyEvent(KeyEvent event, int policyFlags) {
        if (mNext != null) {
          mNext.onKeyEvent(event, policyFlags);
        }
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (mNext != null) {
@@ -360,22 +375,30 @@ public final class ScreenMagnifier implements WindowManagerInternal.Magnificatio
    }

    @Override
    public void clear() {
        mCurrentState = STATE_DETECTING;
        mDetectingStateHandler.clear();
        mStateViewportDraggingHandler.clear();
        mMagnifiedContentInteractonStateHandler.clear();
    public void clearEvents(int inputSource) {
        if (inputSource == InputDevice.SOURCE_TOUCHSCREEN) {
            clear();
        }

        if (mNext != null) {
            mNext.clear();
            mNext.clearEvents(inputSource);
        }
    }

    @Override
    public void onDestroy() {
        clear();
        mScreenStateObserver.destroy();
        mWindowManager.setMagnificationCallbacks(null);
    }

    private void clear() {
        mCurrentState = STATE_DETECTING;
        mDetectingStateHandler.clear();
        mStateViewportDraggingHandler.clear();
        mMagnifiedContentInteractonStateHandler.clear();
    }

    private void handleMotionEventStateDelegating(MotionEvent event,
            MotionEvent rawEvent, int policyFlags) {
        switch (event.getActionMasked()) {
Loading