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
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
package com.android.server.accessibility;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.PowerManager;
|
||||
import android.util.Pools.SimplePool;
|
||||
import android.util.Slog;
|
||||
@@ -30,6 +31,8 @@ import android.view.MotionEvent;
|
||||
import android.view.WindowManagerPolicy;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import com.android.server.LocalServices;
|
||||
|
||||
/**
|
||||
* This class is an input filter for implementing accessibility features such
|
||||
* 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) {
|
||||
mKeyboardInterceptor = new KeyboardInterceptor(mAms);
|
||||
mKeyboardInterceptor = new KeyboardInterceptor(mAms,
|
||||
LocalServices.getService(WindowManagerPolicy.class));
|
||||
addFirstEventHandler(mKeyboardInterceptor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ import android.view.accessibility.IAccessibilityManagerClient;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.content.PackageMonitor;
|
||||
import com.android.internal.os.SomeArgs;
|
||||
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) {
|
||||
List<Service> boundServices = getCurrentUserStateLocked().mBoundServices;
|
||||
if (boundServices.isEmpty()) {
|
||||
|
||||
@@ -16,19 +16,52 @@
|
||||
|
||||
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.MotionEvent;
|
||||
import android.view.WindowManagerPolicy;
|
||||
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 class KeyboardInterceptor implements EventStreamTransformation, Handler.Callback {
|
||||
private static final int MESSAGE_PROCESS_QUEUED_EVENTS = 1;
|
||||
private static final String LOG_TAG = "KeyboardInterceptor";
|
||||
|
||||
public KeyboardInterceptor(AccessibilityManagerService service) {
|
||||
private final AccessibilityManagerService mAms;
|
||||
private final WindowManagerPolicy mPolicy;
|
||||
private final Handler mHandler;
|
||||
|
||||
private EventStreamTransformation mNext;
|
||||
private KeyEventHolder mEventQueueStart;
|
||||
private KeyEventHolder mEventQueueEnd;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
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
|
||||
@@ -40,6 +73,19 @@ public class KeyboardInterceptor implements EventStreamTransformation {
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -65,4 +111,104 @@ public class KeyboardInterceptor implements EventStreamTransformation {
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user