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

Commit 77287ab7 authored by Phil Weaver's avatar Phil Weaver
Browse files

Block a11y-changing key events from a11y services

If the volume keys are being pressed to enable or disable
an accessibility service, the key events should not be
dispatched to accessibility services.

Bug: 62653966
Test: A11y CTS and unit tests. Adding a set of unit tests for
the expanded KeyboardInterceptor. Also verified that the
accessibility shortcut now works as designed with TalkBack.

Change-Id: Iaea58a5bfe6748d4b9a033f5b957e78298881c40
parent 6f76e7f9
Loading
Loading
Loading
Loading
+5 −1
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.server.accessibility;
package com.android.server.accessibility;


import android.content.Context;
import android.content.Context;
import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManager;
import android.util.Pools.SimplePool;
import android.util.Pools.SimplePool;
import android.util.Slog;
import android.util.Slog;
@@ -30,6 +31,8 @@ import android.view.MotionEvent;
import android.view.WindowManagerPolicy;
import android.view.WindowManagerPolicy;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityEvent;


import com.android.server.LocalServices;

/**
/**
 * This class is an input filter for implementing accessibility features such
 * This class is an input filter for implementing accessibility features such
 * as display magnification and explore by touch.
 * as display magnification and explore by touch.
@@ -425,7 +428,8 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
        }
        }


        if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) {
        if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) {
            mKeyboardInterceptor = new KeyboardInterceptor(mAms);
            mKeyboardInterceptor = new KeyboardInterceptor(mAms,
                    LocalServices.getService(WindowManagerPolicy.class));
            addFirstEventHandler(mKeyboardInterceptor);
            addFirstEventHandler(mKeyboardInterceptor);
        }
        }
    }
    }
+3 −1
Original line number Original line Diff line number Diff line
@@ -102,6 +102,7 @@ import android.view.accessibility.IAccessibilityManagerClient;


import com.android.internal.R;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.SomeArgs;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.DumpUtils;
@@ -903,7 +904,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
        }
        }
    }
    }


    boolean notifyKeyEvent(KeyEvent event, int policyFlags) {
    @VisibleForTesting
    public boolean notifyKeyEvent(KeyEvent event, int policyFlags) {
        synchronized (mLock) {
        synchronized (mLock) {
            List<Service> boundServices = getCurrentUserStateLocked().mBoundServices;
            List<Service> boundServices = getCurrentUserStateLocked().mBoundServices;
            if (boundServices.isEmpty()) {
            if (boundServices.isEmpty()) {
+149 −3
Original line number Original line Diff line number Diff line
@@ -16,19 +16,52 @@


package com.android.server.accessibility;
package com.android.server.accessibility;


import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Pools;
import android.util.Slog;
import android.view.KeyEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.WindowManagerPolicy;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityEvent;


/**
/**
 * Intercepts key events and forwards them to accessibility manager service.
 * Intercepts key events and forwards them to accessibility manager service.
 */
 */
public class KeyboardInterceptor implements EventStreamTransformation {
public class KeyboardInterceptor implements EventStreamTransformation, Handler.Callback {
    private static final int MESSAGE_PROCESS_QUEUED_EVENTS = 1;
    private static final String LOG_TAG = "KeyboardInterceptor";

    private final AccessibilityManagerService mAms;
    private final WindowManagerPolicy mPolicy;
    private final Handler mHandler;

    private EventStreamTransformation mNext;
    private EventStreamTransformation mNext;
    private AccessibilityManagerService mAms;
    private KeyEventHolder mEventQueueStart;
    private KeyEventHolder mEventQueueEnd;


    public KeyboardInterceptor(AccessibilityManagerService service) {
    /**
     * @param service The service to notify of key events
     * @param policy The policy to check for keys that may affect a11y
     */
    public KeyboardInterceptor(AccessibilityManagerService service, WindowManagerPolicy policy) {
        mAms = service;
        mAms = service;
        mPolicy = policy;
        mHandler = new Handler(this);
    }

    /**
     * @param service The service to notify of key events
     * @param policy The policy to check for keys that may affect a11y
     * @param handler The handler to use. Only used for testing.
     */
    public KeyboardInterceptor(AccessibilityManagerService service, WindowManagerPolicy policy,
            Handler handler) {
        // Can't combine the constructors without making at least mHandler non-final.
        mAms = service;
        mPolicy = policy;
        mHandler = handler;
    }
    }


    @Override
    @Override
@@ -40,6 +73,19 @@ public class KeyboardInterceptor implements EventStreamTransformation {


    @Override
    @Override
    public void onKeyEvent(KeyEvent event, int policyFlags) {
    public void onKeyEvent(KeyEvent event, int policyFlags) {
        /*
         * Certain keys have system-level behavior that affects accessibility services.
         * Let that behavior settle before handling the keys
         */
        long eventDelay = getEventDelay(event, policyFlags);
        if (eventDelay < 0) {
            return;
        }
        if ((eventDelay > 0) || (mEventQueueStart != null))  {
            addEventToQueue(event, policyFlags, eventDelay);
            return;
        }

        mAms.notifyKeyEvent(event, policyFlags);
        mAms.notifyKeyEvent(event, policyFlags);
    }
    }


@@ -65,4 +111,104 @@ public class KeyboardInterceptor implements EventStreamTransformation {
    @Override
    @Override
    public void onDestroy() {
    public void onDestroy() {
    }
    }

    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what != MESSAGE_PROCESS_QUEUED_EVENTS) {
            Slog.e(LOG_TAG, "Unexpected message type");
            return false;
        }
        processQueuedEvents();
        if (mEventQueueStart != null) {
            scheduleProcessQueuedEvents();
        }
        return true;
    }

    private void addEventToQueue(KeyEvent event, int policyFlags, long delay) {
        long dispatchTime = SystemClock.uptimeMillis() + delay;
        if (mEventQueueStart == null) {
            mEventQueueEnd = mEventQueueStart =
                    KeyEventHolder.obtain(event, policyFlags, dispatchTime);
            scheduleProcessQueuedEvents();
            return;
        }
        final KeyEventHolder holder = KeyEventHolder.obtain(event, policyFlags, dispatchTime);
        holder.next = mEventQueueStart;
        mEventQueueStart.previous = holder;
        mEventQueueStart = holder;
    }

    private void scheduleProcessQueuedEvents() {
        if (!mHandler.sendEmptyMessageAtTime(
                MESSAGE_PROCESS_QUEUED_EVENTS, mEventQueueEnd.dispatchTime)) {
            Slog.e(LOG_TAG, "Failed to schedule key event");
        };
    }

    private void processQueuedEvents() {
        final long currentTime = SystemClock.uptimeMillis();
        while ((mEventQueueEnd != null) && (mEventQueueEnd.dispatchTime <= currentTime)) {
            final long eventDelay = getEventDelay(mEventQueueEnd.event, mEventQueueEnd.policyFlags);
            if (eventDelay > 0) {
                // Reschedule the event
                mEventQueueEnd.dispatchTime = currentTime + eventDelay;
                return;
            }
            // We'll either send or drop the event
            if (eventDelay == 0) {
                mAms.notifyKeyEvent(mEventQueueEnd.event, mEventQueueEnd.policyFlags);
            }
            final KeyEventHolder eventToBeRecycled = mEventQueueEnd;
            mEventQueueEnd = mEventQueueEnd.previous;
            if (mEventQueueEnd != null) {
                mEventQueueEnd.next = null;
            }
            eventToBeRecycled.recycle();
            if (mEventQueueEnd == null) {
                mEventQueueStart = null;
            }
        }
    }

    private long getEventDelay(KeyEvent event, int policyFlags) {
        int keyCode = event.getKeyCode();
        if ((keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) || (keyCode == KeyEvent.KEYCODE_VOLUME_UP)) {
            return mPolicy.interceptKeyBeforeDispatching(null, event, policyFlags);
        }
        return 0;
    }

    private static class KeyEventHolder {
        private static final int MAX_POOL_SIZE = 32;
        private static final Pools.SimplePool<KeyEventHolder> sPool =
                new Pools.SimplePool<>(MAX_POOL_SIZE);

        public int policyFlags;
        public long dispatchTime;
        public KeyEvent event;
        public KeyEventHolder next;
        public KeyEventHolder previous;

        public static KeyEventHolder obtain(KeyEvent event, int policyFlags, long dispatchTime) {
            KeyEventHolder holder = sPool.acquire();
            if (holder == null) {
                holder = new KeyEventHolder();
            }
            holder.event = KeyEvent.obtain(event);
            holder.policyFlags = policyFlags;
            holder.dispatchTime = dispatchTime;
            return holder;
        }

        public void recycle() {
            event.recycle();
            event = null;
            policyFlags = 0;
            dispatchTime = 0;
            next = null;
            previous = null;
            sPool.release(this);
        }
    }
}
}
+218 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2017 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 static junit.framework.Assert.assertTrue;

import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.hamcrest.MockitoHamcrest.argThat;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import android.os.Looper;
import android.support.test.runner.AndroidJUnit4;
import android.view.KeyEvent;
import android.view.WindowManagerPolicy;
import android.view.WindowManagerPolicy.WindowState;

/**
 * Tests for KeyboardInterceptor
 */
@RunWith(AndroidJUnit4.class)
public class KeyboardInterceptorTest {
    private KeyboardInterceptor mInterceptor;
    private MessageCapturingHandler mHandler = new MessageCapturingHandler(
            msg -> mInterceptor.handleMessage(msg));
    @Mock AccessibilityManagerService mMockAms;
    @Mock WindowManagerPolicy mMockPolicy;

    @BeforeClass
    public static void oneTimeInitialization() {
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mInterceptor = new KeyboardInterceptor(mMockAms, mMockPolicy, mHandler);
    }

    @Test
    public void whenNonspecialKeyArrives_withNothingInQueue_eventGoesToAms() {
        KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_0);
        mInterceptor.onKeyEvent(event, 0);
        verify(mMockAms).notifyKeyEvent(argThat(matchesKeyEvent(event)), eq(0));
    }

    @Test
    public void whenVolumeKeyArrives_andPolicySaysUseIt_eventGoesToAms() {
        KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN);
        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(event)), eq(0))).thenReturn(0L);
        mInterceptor.onKeyEvent(event, 0);
        verify(mMockAms).notifyKeyEvent(argThat(matchesKeyEvent(event)), eq(0));
    }

    @Test
    public void whenVolumeKeyArrives_andPolicySaysDropIt_eventDropped() {
        KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP);
        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(event)), eq(0))).thenReturn(-1L);
        mInterceptor.onKeyEvent(event, 0);
        verify(mMockAms, times(0)).notifyKeyEvent(anyObject(), anyInt());
        assertFalse(mHandler.hasMessages());
    }

    @Test
    public void whenVolumeKeyArrives_andPolicySaysDelayThenUse_eventQueuedThenSentToAms() {
        KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP);
        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(event)), eq(0))).thenReturn(150L);
        mInterceptor.onKeyEvent(event, 0);

        assertTrue(mHandler.hasMessages());
        verify(mMockAms, times(0)).notifyKeyEvent(anyObject(), anyInt());

        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(event)), eq(0))).thenReturn(0L);
        mHandler.sendAllMessages();

        verify(mMockAms).notifyKeyEvent(argThat(matchesKeyEvent(event)), eq(0));
    }

    @Test
    public void whenVolumeKeyArrives_andPolicySaysDelayThenDrop_eventQueuedThenDropped() {
        KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN);
        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(event)), eq(0))).thenReturn(150L);
        mInterceptor.onKeyEvent(event, 0);

        assertTrue(mHandler.hasMessages());
        verify(mMockAms, times(0)).notifyKeyEvent(anyObject(), anyInt());

        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(event)), eq(0))).thenReturn(-1L);
        mHandler.sendAllMessages();

        verify(mMockAms, times(0)).notifyKeyEvent(anyObject(), anyInt());
    }

    @Test
    public void whenSomeEventsGetDelayed_allEventsStillInOrder() {
        KeyEvent[] events = {new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_0),
                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A),
                new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP),
                new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_0)};

        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(events[1])), eq(0))).thenReturn(150L);
        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(events[3])), eq(0))).thenReturn(75L);

        for (KeyEvent event : events) {
            mInterceptor.onKeyEvent(event, 0);
        }

        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(events[1])), eq(0))).thenReturn(0L);
        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(events[3])), eq(0))).thenReturn(0L);

        mHandler.sendAllMessages();

        InOrder inOrder = Mockito.inOrder(mMockAms);
        for (KeyEvent event : events) {
            inOrder.verify(mMockAms).notifyKeyEvent(argThat(matchesKeyEvent(event)), eq(0));
        }
    }

    @Test
    public void whenSomeEventsGetDropped_otherEventsStillInOrder() {
        KeyEvent[] events = {new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_0),
                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A),
                new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP),
                new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_0)};

        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(events[1])), eq(0))).thenReturn(150L);
        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(events[3])), eq(0))).thenReturn(75L);

        for (KeyEvent event : events) {
            mInterceptor.onKeyEvent(event, 0);
        }

        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(events[1])), eq(0))).thenReturn(-1L);
        when(mMockPolicy.interceptKeyBeforeDispatching((WindowState) argThat(nullValue()),
                argThat(matchesKeyEvent(events[3])), eq(0))).thenReturn(-1L);

        mHandler.sendAllMessages();

        InOrder inOrder = Mockito.inOrder(mMockAms);
        for (KeyEvent event : events) {
            if ((event == events[1]) || (event == events[3])) continue;
            inOrder.verify(mMockAms).notifyKeyEvent(argThat(matchesKeyEvent(event)), eq(0));
        }
    }

    private static KeyEventMatcher matchesKeyEvent(KeyEvent event) {
        return new KeyEventMatcher(event);
    }

    private static class KeyEventMatcher extends TypeSafeMatcher<KeyEvent> {
        private KeyEvent mEventToMatch;

        public KeyEventMatcher(KeyEvent eventToMatch) {
            mEventToMatch = eventToMatch;
        }

        @Override
        protected boolean matchesSafely(KeyEvent item) {
            return (mEventToMatch.getKeyCode() == item.getKeyCode())
                    && (mEventToMatch.getAction() == item.getAction());
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("Matches key event");
        }
    }
}
 No newline at end of file