blob: abcd8e2a2d7e13a0d04a96f1e9dcc7bec95b1c59 [file] [log] [blame]
/*
* Copyright (C) 2011 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 android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY;
import android.accessibilityservice.AccessibilityTrace;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Region;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.Display;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.InputFilter;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
import com.android.server.LocalServices;
import com.android.server.accessibility.gestures.TouchExplorer;
import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler;
import com.android.server.accessibility.magnification.FullScreenMagnificationVibrationHelper;
import com.android.server.accessibility.magnification.MagnificationGestureHandler;
import com.android.server.accessibility.magnification.WindowMagnificationGestureHandler;
import com.android.server.accessibility.magnification.WindowMagnificationPromptController;
import com.android.server.policy.WindowManagerPolicy;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.StringJoiner;
/**
* This class is an input filter for implementing accessibility features such
* as display magnification and explore by touch.
*
* NOTE: This class has to be created and poked only from the main thread.
*/
@SuppressWarnings("MissingPermissionAnnotation")
class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation {
private static final String TAG = AccessibilityInputFilter.class.getSimpleName();
private static final boolean DEBUG = false;
/**
* Flag for enabling the screen magnification feature.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_FEATURE_MAGNIFICATION_SINGLE_FINGER_TRIPLE_TAP = 0x00000001;
/**
* Flag for enabling the touch exploration feature.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_FEATURE_TOUCH_EXPLORATION = 0x00000002;
/**
* Flag for enabling the filtering key events feature.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_FEATURE_FILTER_KEY_EVENTS = 0x00000004;
/**
* Flag for enabling "Automatically click on mouse stop" feature.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_FEATURE_AUTOCLICK = 0x00000008;
/**
* Flag for enabling motion event injection.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_FEATURE_INJECT_MOTION_EVENTS = 0x00000010;
/**
* Flag for enabling the feature to control the screen magnifier. If
* {@link #FLAG_FEATURE_MAGNIFICATION_SINGLE_FINGER_TRIPLE_TAP} is set this flag is ignored
* as the screen magnifier feature performs a super set of the work
* performed by this feature.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER = 0x00000020;
/**
* Flag for enabling the feature to trigger the screen magnifier
* from another on-device interaction.
*/
static final int FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER = 0x00000040;
/**
* Flag for dispatching double tap and double tap and hold to the service.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_SERVICE_HANDLES_DOUBLE_TAP = 0x00000080;
/**
* Flag for enabling multi-finger gestures.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_REQUEST_MULTI_FINGER_GESTURES = 0x00000100;
/**
* Flag for enabling two-finger passthrough when multi-finger gestures are enabled.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_REQUEST_2_FINGER_PASSTHROUGH = 0x00000200;
/**
* Flag for including motion events when dispatching a gesture.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_SEND_MOTION_EVENTS = 0x00000400;
/** Flag for intercepting generic motion events. */
static final int FLAG_FEATURE_INTERCEPT_GENERIC_MOTION_EVENTS = 0x00000800;
/**
* Flag for enabling the two-finger triple-tap magnification feature.
*
* @see #setUserAndEnabledFeatures(int, int)
*/
static final int FLAG_FEATURE_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP = 0x00001000;
static final int FEATURES_AFFECTING_MOTION_EVENTS =
FLAG_FEATURE_INJECT_MOTION_EVENTS
| FLAG_FEATURE_AUTOCLICK
| FLAG_FEATURE_TOUCH_EXPLORATION
| FLAG_FEATURE_MAGNIFICATION_SINGLE_FINGER_TRIPLE_TAP
| FLAG_FEATURE_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP
| FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER
| FLAG_SERVICE_HANDLES_DOUBLE_TAP
| FLAG_REQUEST_MULTI_FINGER_GESTURES
| FLAG_REQUEST_2_FINGER_PASSTHROUGH
| FLAG_FEATURE_INTERCEPT_GENERIC_MOTION_EVENTS;
private final Context mContext;
private final PowerManager mPm;
private final AccessibilityManagerService mAms;
private final SparseArray<EventStreamTransformation> mEventHandler;
private final SparseArray<TouchExplorer> mTouchExplorer = new SparseArray<>(0);
private final SparseArray<MagnificationGestureHandler> mMagnificationGestureHandler =
new SparseArray<>(0);
private final SparseArray<MotionEventInjector> mMotionEventInjectors = new SparseArray<>(0);
private AutoclickController mAutoclickController;
private KeyboardInterceptor mKeyboardInterceptor;
private boolean mInstalled;
private int mUserId;
private int mEnabledFeatures;
// Display-specific features
private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>();
private final SparseArray<EventStreamState> mMouseStreamStates = new SparseArray<>(0);
private final SparseArray<EventStreamState> mTouchScreenStreamStates = new SparseArray<>(0);
// State tracking for generic MotionEvents is display-agnostic so we only need one.
private GenericMotionEventStreamState mGenericMotionEventStreamState;
private int mCombinedGenericMotionEventSources = 0;
private int mCombinedMotionEventObservedSources = 0;
private EventStreamState mKeyboardStreamState;
AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
this(context, service, new SparseArray<>(0));
}
AccessibilityInputFilter(Context context, AccessibilityManagerService service,
SparseArray<EventStreamTransformation> eventHandler) {
super(context.getMainLooper());
mContext = context;
mAms = service;
mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mEventHandler = eventHandler;
}
@Override
public void onInstalled() {
if (DEBUG) {
Slog.d(TAG, "Accessibility input filter installed.");
}
mInstalled = true;
disableFeatures();
enableFeatures();
mAms.onInputFilterInstalled(true);
super.onInstalled();
}
@Override
public void onUninstalled() {
if (DEBUG) {
Slog.d(TAG, "Accessibility input filter uninstalled.");
}
mInstalled = false;
disableFeatures();
mAms.onInputFilterInstalled(false);
super.onUninstalled();
}
void onDisplayAdded(@NonNull Display display) {
enableFeaturesForDisplayIfInstalled(display);
}
void onDisplayRemoved(int displayId) {
disableFeaturesForDisplayIfInstalled(displayId);
}
@Override
public void onInputEvent(InputEvent event, int policyFlags) {
if (DEBUG) {
Slog.d(TAG, "Received event: " + event + ", policyFlags=0x"
+ Integer.toHexString(policyFlags));
}
if (mAms.getTraceManager().isA11yTracingEnabledForTypes(
AccessibilityTrace.FLAGS_INPUT_FILTER)) {
mAms.getTraceManager().logTrace(TAG + ".onInputEvent",
AccessibilityTrace.FLAGS_INPUT_FILTER,
"event=" + event + ";policyFlags=" + policyFlags);
}
if (mEventHandler.size() == 0) {
if (DEBUG) Slog.d(TAG, "No mEventHandler for event " + event);
super.onInputEvent(event, policyFlags);
return;
}
EventStreamState state = getEventStreamState(event);
if (state == null) {
super.onInputEvent(event, policyFlags);
return;
}
final int eventSource = event.getSource();
final int displayId = event.getDisplayId();
if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
state.reset();
clearEventStreamHandler(displayId, eventSource);
super.onInputEvent(event, policyFlags);
return;
}
if (state.updateInputSource(event.getSource())) {
clearEventStreamHandler(displayId, eventSource);
}
if (!state.inputSourceValid()) {
super.onInputEvent(event, policyFlags);
return;
}
if (event instanceof MotionEvent) {
if ((mEnabledFeatures & FEATURES_AFFECTING_MOTION_EVENTS) != 0) {
MotionEvent motionEvent = (MotionEvent) event;
processMotionEvent(state, motionEvent, policyFlags);
return;
} else {
super.onInputEvent(event, policyFlags);
}
} else if (event instanceof KeyEvent) {
KeyEvent keyEvent = (KeyEvent) event;
processKeyEvent(state, keyEvent, policyFlags);
}
}
/**
* 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) {
final int displayId = event.getDisplayId();
if (mGenericMotionEventStreamState == null) {
mGenericMotionEventStreamState = new GenericMotionEventStreamState();
}
if (mGenericMotionEventStreamState.shouldProcessMotionEvent((MotionEvent) event)) {
return mGenericMotionEventStreamState;
}
if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
EventStreamState touchScreenStreamState = mTouchScreenStreamStates.get(displayId);
if (touchScreenStreamState == null) {
touchScreenStreamState = new TouchScreenEventStreamState();
mTouchScreenStreamStates.put(displayId, touchScreenStreamState);
}
return touchScreenStreamState;
}
if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
EventStreamState mouseStreamState = mMouseStreamStates.get(displayId);
if (mouseStreamState == null) {
mouseStreamState = new MouseEventStreamState();
mMouseStreamStates.put(displayId, mouseStreamState);
}
return mouseStreamState;
}
} else if (event instanceof KeyEvent) {
if (event.isFromSource(InputDevice.SOURCE_KEYBOARD)) {
if (mKeyboardStreamState == null) {
mKeyboardStreamState = new KeyboardEventStreamState();
}
return mKeyboardStreamState;
}
}
return null;
}
private void clearEventStreamHandler(int displayId, int eventSource) {
final EventStreamTransformation eventHandler = mEventHandler.get(displayId);
if (eventHandler != null) {
eventHandler.clearEvents(eventSource);
}
}
private void processMotionEvent(EventStreamState state, MotionEvent event, int policyFlags) {
if (!state.shouldProcessScroll() && event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
super.onInputEvent(event, policyFlags);
return;
}
if (!state.shouldProcessMotionEvent(event)) {
return;
}
handleMotionEvent(event, policyFlags);
}
private void processKeyEvent(EventStreamState state, KeyEvent event, int policyFlags) {
if (!state.shouldProcessKeyEvent(event)) {
super.onInputEvent(event, policyFlags);
return;
}
// Since the display id of KeyEvent always would be -1 and there is only one
// KeyboardInterceptor for all display, pass KeyEvent to the mEventHandler of
// DEFAULT_DISPLAY to handle.
mEventHandler.get(Display.DEFAULT_DISPLAY).onKeyEvent(event, policyFlags);
}
private void handleMotionEvent(MotionEvent event, int policyFlags) {
if (DEBUG) {
Slog.i(TAG, "Handling motion event: " + event + ", policyFlags: " + policyFlags);
}
mPm.userActivity(event.getEventTime(), false);
MotionEvent transformedEvent = MotionEvent.obtain(event);
final int displayId = event.getDisplayId();
EventStreamTransformation eventStreamTransformation = mEventHandler.get(
isDisplayIdValid(displayId) ? displayId : Display.DEFAULT_DISPLAY);
if (eventStreamTransformation != null) {
eventStreamTransformation.onMotionEvent(transformedEvent, event, policyFlags);
}
transformedEvent.recycle();
}
private boolean isDisplayIdValid(int displayId) {
return mEventHandler.get(displayId) != null;
}
@Override
public void onMotionEvent(MotionEvent transformedEvent, MotionEvent rawEvent,
int policyFlags) {
if (!mInstalled) {
Slog.w(TAG, "onMotionEvent called before input filter installed!");
return;
}
sendInputEvent(transformedEvent, policyFlags);
}
@Override
public void onKeyEvent(KeyEvent event, int policyFlags) {
if (!mInstalled) {
Slog.w(TAG, "onKeyEvent called before input filter installed!");
return;
}
sendInputEvent(event, policyFlags);
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// TODO Implement this to inject the accessibility event
// into the accessibility manager service similarly
// to how this is done for input events.
}
@Override
public void setNext(EventStreamTransformation sink) {
/* do nothing */
}
@Override
public EventStreamTransformation getNext() {
return null;
}
@Override
public void clearEvents(int inputSource) {
/* do nothing */
}
void setUserAndEnabledFeatures(int userId, int enabledFeatures) {
if (DEBUG) {
Slog.i(TAG, "setUserAndEnabledFeatures(userId = " + userId + ", enabledFeatures = 0x"
+ Integer.toHexString(enabledFeatures) + ")");
}
if (mEnabledFeatures == enabledFeatures && mUserId == userId) {
return;
}
if (mInstalled) {
disableFeatures();
}
mUserId = userId;
mEnabledFeatures = enabledFeatures;
if (mInstalled) {
enableFeatures();
}
}
void notifyAccessibilityEvent(AccessibilityEvent event) {
for (int i = 0; i < mEventHandler.size(); i++) {
final EventStreamTransformation eventHandler = mEventHandler.valueAt(i);
if (eventHandler != null) {
eventHandler.onAccessibilityEvent(event);
}
}
}
void notifyAccessibilityButtonClicked(int displayId) {
if (mMagnificationGestureHandler.size() != 0) {
final MagnificationGestureHandler handler = mMagnificationGestureHandler.get(displayId);
if (handler != null) {
handler.notifyShortcutTriggered();
}
}
}
private void enableFeatures() {
if (DEBUG) Slog.i(TAG, "enableFeatures()");
resetAllStreamState();
final ArrayList<Display> displaysList = mAms.getValidDisplayList();
for (int i = displaysList.size() - 1; i >= 0; i--) {
enableFeaturesForDisplay(displaysList.get(i));
}
enableDisplayIndependentFeatures();
}
private void enableFeaturesForDisplay(Display display) {
if (DEBUG) {
Slog.i(TAG, "enableFeaturesForDisplay() : display Id = " + display.getDisplayId());
}
final Context displayContext = mContext.createDisplayContext(display);
final int displayId = display.getDisplayId();
if (mAms.isDisplayProxyed(displayId)) {
return;
}
if (!mServiceDetectsGestures.contains(displayId)) {
mServiceDetectsGestures.put(displayId, false);
}
if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) {
if (mAutoclickController == null) {
mAutoclickController = new AutoclickController(
mContext, mUserId, mAms.getTraceManager());
}
addFirstEventHandler(displayId, mAutoclickController);
}
if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
TouchExplorer explorer = new TouchExplorer(displayContext, mAms);
if ((mEnabledFeatures & FLAG_SERVICE_HANDLES_DOUBLE_TAP) != 0) {
explorer.setServiceHandlesDoubleTap(true);
}
if ((mEnabledFeatures & FLAG_REQUEST_MULTI_FINGER_GESTURES) != 0) {
explorer.setMultiFingerGesturesEnabled(true);
}
if ((mEnabledFeatures & FLAG_REQUEST_2_FINGER_PASSTHROUGH) != 0) {
explorer.setTwoFingerPassthroughEnabled(true);
}
if ((mEnabledFeatures & FLAG_SEND_MOTION_EVENTS) != 0) {
explorer.setSendMotionEventsEnabled(true);
}
explorer.setServiceDetectsGestures(mServiceDetectsGestures.get(displayId));
addFirstEventHandler(displayId, explorer);
mTouchExplorer.put(displayId, explorer);
}
if ((mEnabledFeatures & FLAG_FEATURE_INTERCEPT_GENERIC_MOTION_EVENTS) != 0) {
addFirstEventHandler(
displayId,
new BaseEventStreamTransformation() {
@Override
public void onMotionEvent(
MotionEvent event, MotionEvent rawEvent, int policyFlags) {
boolean passAlongEvent = true;
if (anyServiceWantsGenericMotionEvent(event)) {
// Some service wants this event, so try to deliver it to at least
// one service.
if (mAms.sendMotionEventToListeningServices(event)) {
// A service accepted this event, so prevent it from passing
// down the stream by default.
passAlongEvent = false;
}
// However, if a service is observing these events instead of
// consuming them then ensure
// it is always passed along to the next stage of the event stream.
if (anyServiceWantsToObserveMotionEvent(event)) {
passAlongEvent = true;
}
}
if (passAlongEvent) {
super.onMotionEvent(event, rawEvent, policyFlags);
}
}
});
}
if ((mEnabledFeatures & FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER) != 0
|| ((mEnabledFeatures & FLAG_FEATURE_MAGNIFICATION_SINGLE_FINGER_TRIPLE_TAP) != 0)
|| ((mEnabledFeatures & FLAG_FEATURE_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP) != 0)
|| ((mEnabledFeatures & FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER) != 0)) {
final MagnificationGestureHandler magnificationGestureHandler =
createMagnificationGestureHandler(displayId, displayContext);
addFirstEventHandler(displayId, magnificationGestureHandler);
mMagnificationGestureHandler.put(displayId, magnificationGestureHandler);
}
if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) {
MotionEventInjector injector =
new MotionEventInjector(mContext.getMainLooper(), mAms.getTraceManager());
addFirstEventHandler(displayId, injector);
mMotionEventInjectors.put(displayId, injector);
}
}
private void enableDisplayIndependentFeatures() {
if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) {
mAms.setMotionEventInjectors(mMotionEventInjectors);
}
if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) {
mKeyboardInterceptor = new KeyboardInterceptor(mAms,
LocalServices.getService(WindowManagerPolicy.class));
// Since the display id of KeyEvent always would be -1 and it would be dispatched to
// the display with input focus directly, we only need one KeyboardInterceptor for
// default display.
addFirstEventHandler(Display.DEFAULT_DISPLAY, mKeyboardInterceptor);
}
}
/**
* Adds an event handler to the event handler chain for giving display. The handler is added at
* the beginning of the chain.
*
* @param displayId The logical display id.
* @param handler The handler to be added to the event handlers list.
*/
private void addFirstEventHandler(int displayId, EventStreamTransformation handler) {
EventStreamTransformation eventHandler = mEventHandler.get(displayId);
if (eventHandler != null) {
handler.setNext(eventHandler);
} else {
handler.setNext(this);
}
eventHandler = handler;
mEventHandler.put(displayId, eventHandler);
}
private void disableFeatures() {
final ArrayList<Display> displaysList = mAms.getValidDisplayList();
for (int i = displaysList.size() - 1; i >= 0; i--) {
disableFeaturesForDisplay(displaysList.get(i).getDisplayId());
}
mAms.setMotionEventInjectors(null);
disableDisplayIndependentFeatures();
resetAllStreamState();
}
private void disableFeaturesForDisplay(int displayId) {
if (DEBUG) {
Slog.i(TAG, "disableFeaturesForDisplay() : display Id = " + displayId);
}
final MotionEventInjector injector = mMotionEventInjectors.get(displayId);
if (injector != null) {
injector.onDestroy();
mMotionEventInjectors.remove(displayId);
}
final TouchExplorer explorer = mTouchExplorer.get(displayId);
if (explorer != null) {
explorer.onDestroy();
mTouchExplorer.remove(displayId);
}
final MagnificationGestureHandler handler = mMagnificationGestureHandler.get(displayId);
if (handler != null) {
handler.onDestroy();
mMagnificationGestureHandler.remove(displayId);
}
final EventStreamTransformation eventStreamTransformation = mEventHandler.get(displayId);
if (eventStreamTransformation != null) {
mEventHandler.remove(displayId);
}
}
void enableFeaturesForDisplayIfInstalled(Display display) {
if (mInstalled) {
resetStreamStateForDisplay(display.getDisplayId());
enableFeaturesForDisplay(display);
}
}
void disableFeaturesForDisplayIfInstalled(int displayId) {
if (mInstalled) {
disableFeaturesForDisplay(displayId);
resetStreamStateForDisplay(displayId);
}
}
private void disableDisplayIndependentFeatures() {
if (mAutoclickController != null) {
mAutoclickController.onDestroy();
mAutoclickController = null;
}
if (mKeyboardInterceptor != null) {
mKeyboardInterceptor.onDestroy();
mKeyboardInterceptor = null;
}
}
private MagnificationGestureHandler createMagnificationGestureHandler(
int displayId, Context displayContext) {
final boolean detectControlGestures = (mEnabledFeatures
& FLAG_FEATURE_MAGNIFICATION_SINGLE_FINGER_TRIPLE_TAP) != 0;
final boolean detectTwoFingerTripleTap = (mEnabledFeatures
& FLAG_FEATURE_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP) != 0;
final boolean triggerable = (mEnabledFeatures
& FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER) != 0;
MagnificationGestureHandler magnificationGestureHandler;
if (mAms.getMagnificationMode(displayId)
== Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) {
final Context uiContext = displayContext.createWindowContext(
TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, null /* options */);
magnificationGestureHandler = new WindowMagnificationGestureHandler(uiContext,
mAms.getMagnificationConnectionManager(), mAms.getTraceManager(),
mAms.getMagnificationController(),
detectControlGestures,
detectTwoFingerTripleTap,
triggerable, displayId);
} else {
final Context uiContext = displayContext.createWindowContext(
TYPE_MAGNIFICATION_OVERLAY, null /* options */);
FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper =
new FullScreenMagnificationVibrationHelper(uiContext);
magnificationGestureHandler = new FullScreenMagnificationGestureHandler(uiContext,
mAms.getMagnificationController().getFullScreenMagnificationController(),
mAms.getTraceManager(),
mAms.getMagnificationController(),
detectControlGestures,
detectTwoFingerTripleTap,
triggerable,
new WindowMagnificationPromptController(displayContext, mUserId), displayId,
fullScreenMagnificationVibrationHelper);
}
return magnificationGestureHandler;
}
void resetAllStreamState() {
final ArrayList<Display> displaysList = mAms.getValidDisplayList();
for (int i = displaysList.size() - 1; i >= 0; i--) {
resetStreamStateForDisplay(displaysList.get(i).getDisplayId());
}
if (mKeyboardStreamState != null) {
mKeyboardStreamState.reset();
}
}
void resetStreamStateForDisplay(int displayId) {
final EventStreamState touchScreenStreamState = mTouchScreenStreamStates.get(displayId);
if (touchScreenStreamState != null) {
touchScreenStreamState.reset();
mTouchScreenStreamStates.remove(displayId);
}
final EventStreamState mouseStreamState = mMouseStreamStates.get(displayId);
if (mouseStreamState != null) {
mouseStreamState.reset();
mMouseStreamStates.remove(displayId);
}
}
@Override
public void onDestroy() {
/* ignore */
}
/**
* Called to refresh the magnification mode on the given display.
* It's responsible for changing {@link MagnificationGestureHandler} based on the current mode.
*
* @param display The logical display
*/
@MainThread
public void refreshMagnificationMode(Display display) {
final int displayId = display.getDisplayId();
final MagnificationGestureHandler magnificationGestureHandler =
mMagnificationGestureHandler.get(displayId);
if (magnificationGestureHandler == null) {
return;
}
if (magnificationGestureHandler.getMode() == mAms.getMagnificationMode(displayId)) {
return;
}
magnificationGestureHandler.onDestroy();
final MagnificationGestureHandler currentMagnificationGestureHandler =
createMagnificationGestureHandler(displayId,
mContext.createDisplayContext(display));
switchEventStreamTransformation(displayId, magnificationGestureHandler,
currentMagnificationGestureHandler);
mMagnificationGestureHandler.put(displayId, currentMagnificationGestureHandler);
}
@MainThread
private void switchEventStreamTransformation(int displayId,
EventStreamTransformation oldStreamTransformation,
EventStreamTransformation currentStreamTransformation) {
EventStreamTransformation eventStreamTransformation = mEventHandler.get(displayId);
if (eventStreamTransformation == null) {
return;
}
if (eventStreamTransformation == oldStreamTransformation) {
currentStreamTransformation.setNext(oldStreamTransformation.getNext());
mEventHandler.put(displayId, currentStreamTransformation);
} else {
while (eventStreamTransformation != null) {
if (eventStreamTransformation.getNext() == oldStreamTransformation) {
eventStreamTransformation.setNext(currentStreamTransformation);
currentStreamTransformation.setNext(oldStreamTransformation.getNext());
return;
} else {
eventStreamTransformation = eventStreamTransformation.getNext();
}
}
}
}
/**
* 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 mSource;
EventStreamState() {
mSource = -1;
}
/**
* Updates the input source of the device associated with the state. If the source changes,
* resets internal state.
*
* @param source Updated input source.
* @return Whether the input source has changed.
*/
public boolean updateInputSource(int source) {
if (mSource == source) {
return false;
}
// Reset clears internal state, so make sure it's called before |mSource| is updated.
reset();
mSource = source;
return true;
}
/**
* @return Whether input source is valid.
*/
public boolean inputSourceValid() {
return mSource >= 0;
}
/**
* Resets the event stream state.
*/
public void reset() {
mSource = -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;
}
}
private class GenericMotionEventStreamState extends EventStreamState {
@Override
public boolean shouldProcessMotionEvent(MotionEvent event) {
return anyServiceWantsGenericMotionEvent(event);
}
@Override
public boolean shouldProcessScroll() {
return true;
}
}
private boolean anyServiceWantsToObserveMotionEvent(MotionEvent event) {
// Disable SOURCE_TOUCHSCREEN generic event interception if any service is performing
// touch exploration.
if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)
&& (mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
return false;
}
final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
return (mCombinedGenericMotionEventSources
& mCombinedMotionEventObservedSources
& eventSourceWithoutClass)
!= 0;
}
private boolean anyServiceWantsGenericMotionEvent(MotionEvent event) {
// Disable SOURCE_TOUCHSCREEN generic event interception if any service is performing
// touch exploration.
if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)
&& (mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
return false;
}
final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
return (mCombinedGenericMotionEventSources & eventSourceWithoutClass) != 0;
}
public void setCombinedGenericMotionEventSources(int sources) {
mCombinedGenericMotionEventSources = sources;
}
public void setCombinedMotionEventObservedSources(int sources) {
mCombinedMotionEventObservedSources = sources;
}
/**
* Keeps state of streams of events from all keyboard devices.
*/
private static class KeyboardEventStreamState extends EventStreamState {
private SparseBooleanArray mEventSequenceStartedMap = new SparseBooleanArray();
public KeyboardEventStreamState() {
reset();
}
@Override
final public void reset() {
super.reset();
mEventSequenceStartedMap.clear();
}
/*
* Key events from different devices may be interleaved. For example, the volume up and
* down keys can come from different input sources.
*/
@Override
public boolean updateInputSource(int deviceId) {
return false;
}
// We manage all input source simultaneously; there is no concept of validity.
@Override
public boolean inputSourceValid() {
return true;
}
@Override
final public boolean shouldProcessKeyEvent(KeyEvent event) {
// For each keyboard device, wait for a down event from a device to start processing
int deviceId = event.getDeviceId();
if (mEventSequenceStartedMap.get(deviceId, false)) {
return true;
}
boolean shouldProcess = event.getAction() == KeyEvent.ACTION_DOWN;
mEventSequenceStartedMap.put(deviceId, shouldProcess);
return shouldProcess;
}
}
public void setGestureDetectionPassthroughRegion(int displayId, Region region) {
if (region != null && mTouchExplorer.contains(displayId)) {
mTouchExplorer.get(displayId).setGestureDetectionPassthroughRegion(region);
}
}
public void setTouchExplorationPassthroughRegion(int displayId, Region region) {
if (region != null && mTouchExplorer.contains(displayId)) {
mTouchExplorer.get(displayId).setTouchExplorationPassthroughRegion(region);
}
}
public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) {
if (mTouchExplorer.contains(displayId)) {
mTouchExplorer.get(displayId).setServiceDetectsGestures(mode);
}
mServiceDetectsGestures.put(displayId, mode);
}
public void resetServiceDetectsGestures() {
mServiceDetectsGestures.clear();
}
public void requestTouchExploration(int displayId) {
if (mTouchExplorer.contains(displayId)) {
mTouchExplorer.get(displayId).requestTouchExploration();
}
}
public void requestDragging(int displayId, int pointerId) {
if (mTouchExplorer.contains(displayId)) {
mTouchExplorer.get(displayId).requestDragging(pointerId);
}
}
public void requestDelegating(int displayId) {
if (mTouchExplorer.contains(displayId)) {
mTouchExplorer.get(displayId).requestDelegating();
}
}
public void onDoubleTap(int displayId) {
if (mTouchExplorer.contains(displayId)) {
mTouchExplorer.get(displayId).onDoubleTap();
}
}
public void onDoubleTapAndHold(int displayId) {
if (mTouchExplorer.contains(displayId)) {
mTouchExplorer.get(displayId).onDoubleTapAndHold();
}
}
/**
* Dumps all {@link AccessibilityInputFilter}s here.
*/
public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
if (mEventHandler == null) {
return;
}
pw.append("A11yInputFilter Info : ");
pw.println();
final ArrayList<Display> displaysList = mAms.getValidDisplayList();
for (int i = 0; i < displaysList.size(); i++) {
final int displayId = displaysList.get(i).getDisplayId();
EventStreamTransformation next = mEventHandler.get(displayId);
if (next != null) {
pw.append("Enabled features of Display [");
pw.append(Integer.toString(displayId));
pw.append("] = ");
final StringJoiner joiner = new StringJoiner(",", "[", "]");
while (next != null) {
if (next instanceof MagnificationGestureHandler) {
joiner.add("MagnificationGesture");
} else if (next instanceof KeyboardInterceptor) {
joiner.add("KeyboardInterceptor");
} else if (next instanceof TouchExplorer) {
joiner.add("TouchExplorer");
} else if (next instanceof AutoclickController) {
joiner.add("AutoclickController");
} else if (next instanceof MotionEventInjector) {
joiner.add("MotionEventInjector");
}
next = next.getNext();
}
pw.append(joiner.toString());
}
pw.println();
}
}
}