| /* |
| * 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(); |
| } |
| } |
| } |