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

Commit 872880ee authored by Hiroki Sato's avatar Hiroki Sato
Browse files

Re-implement full screen magnification continuous cursor following

This migrates an existing full screen magnification's continuous cursor
following feature to use AccessibilityPointerMotionFilter so that other
types of cursor following features can be implemented similarly.

Bug: 361817142
Test: FullScreenMagnificationControllerTest
Test: FullScreenMagnificationGestureHandlerTest
Test: FullScreenMagnificationPointerMotionEventFilterTest
Flag: com.android.server.accessibility.enable_magnification_follows_mouse_with_pointer_motion_filter
Change-Id: I482c121cbc0d1da64e6d22aabcc3894caf89bb18
parent d833bce1
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -287,7 +287,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub

    public static final int INVALID_SERVICE_ID = -1;

    // Each service has an ID. Also provide one for magnification gesture handling
    // Each service has an ID. Also provide one for magnification gesture handling.
    // This ID is also used for mouse event handling.
    public static final int MAGNIFICATION_GESTURE_HANDLER_ID = 0;

    private static int sIdCounter = MAGNIFICATION_GESTURE_HANDLER_ID + 1;
+53 −3
Original line number Diff line number Diff line
@@ -121,6 +121,8 @@ public class FullScreenMagnificationController implements
    @NonNull private final Supplier<MagnificationThumbnail> mThumbnailSupplier;
    @NonNull private final Supplier<Boolean> mMagnificationConnectionStateSupplier;

    private boolean mIsPointerMotionFilterInstalled = false;

    /**
     * This class implements {@link WindowManagerInternal.MagnificationCallbacks} and holds
     * magnification information per display.
@@ -830,9 +832,17 @@ public class FullScreenMagnificationController implements
                return;
            }

            final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX;
            final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY;
            if (updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY)) {
            setOffset(mCurrentMagnificationSpec.offsetX - offsetX,
                    mCurrentMagnificationSpec.offsetY - offsetY, id);
        }

        @GuardedBy("mLock")
        void setOffset(float offsetX, float offsetY, int id) {
            if (!mRegistered) {
                return;
            }

            if (updateCurrentSpecWithOffsetsLocked(offsetX, offsetY)) {
                onMagnificationChangedLocked(/* isScaleTransient= */ false);
            }
            if (id != INVALID_SERVICE_ID) {
@@ -1065,6 +1075,7 @@ public class FullScreenMagnificationController implements
            if (display.register()) {
                mDisplays.put(displayId, display);
                mScreenStateObserver.registerIfNecessary();
                configurePointerMotionFilter(true);
            }
        }
    }
@@ -1612,6 +1623,28 @@ public class FullScreenMagnificationController implements
        }
    }

    /**
     * Sets the offset of the magnified region.
     *
     * @param displayId The logical display id.
     * @param offsetX   the offset of the magnified region in the X coordinate, in current
     *                  screen pixels.
     * @param offsetY   the offset of the magnified region in the Y coordinate, in current
     *                  screen pixels.
     * @param id        the ID of the service requesting the change
     */
    @SuppressWarnings("GuardedBy")
    // errorprone cannot recognize an inner class guarded by an outer class member.
    public void setOffset(int displayId, float offsetX, float offsetY, int id) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return;
            }
            display.setOffset(offsetX, offsetY, id);
        }
    }

    /**
     * Offsets the magnified region. Note that the offsetX and offsetY values actually move in the
     * opposite direction as the offsets passed in here.
@@ -1885,6 +1918,7 @@ public class FullScreenMagnificationController implements
        }
        if (!hasRegister) {
            mScreenStateObserver.unregister();
            configurePointerMotionFilter(false);
        }
    }

@@ -1900,6 +1934,22 @@ public class FullScreenMagnificationController implements
        }
    }

    private void configurePointerMotionFilter(boolean enabled) {
        if (!Flags.enableMagnificationFollowsMouseWithPointerMotionFilter()) {
            return;
        }
        if (enabled == mIsPointerMotionFilterInstalled) {
            return;
        }
        if (!enabled) {
            mControllerCtx.getInputManager().registerAccessibilityPointerMotionFilter(null);
        } else {
            mControllerCtx.getInputManager().registerAccessibilityPointerMotionFilter(
                    new FullScreenMagnificationPointerMotionEventFilter(this));
        }
        mIsPointerMotionFilterInstalled = enabled;
    }

    private boolean traceEnabled() {
        return mControllerCtx.getTraceManager().isA11yTracingEnabledForTypes(
                FLAGS_WINDOW_MANAGER_INTERNAL);
+7 −2
Original line number Diff line number Diff line
@@ -182,6 +182,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
    private final int mMinimumVelocity;
    private final int mMaximumVelocity;

    @Nullable
    private final MouseEventHandler mMouseEventHandler;

    public FullScreenMagnificationGestureHandler(
@@ -313,7 +314,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
        mOverscrollEdgeSlop = context.getResources().getDimensionPixelSize(
                R.dimen.accessibility_fullscreen_magnification_gesture_edge_slop);
        mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
        mMouseEventHandler = new MouseEventHandler(mFullScreenMagnificationController);
        mMouseEventHandler =
                Flags.enableMagnificationFollowsMouseWithPointerMotionFilter()
                        ? null : new MouseEventHandler(mFullScreenMagnificationController);

        if (mDetectShortcutTrigger) {
            mScreenStateReceiver = new ScreenStateReceiver(context, this);
@@ -337,9 +340,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH

    @Override
    void handleMouseOrStylusEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        if (!mFullScreenMagnificationController.isActivated(mDisplayId)) {
        if (mMouseEventHandler == null
                || !mFullScreenMagnificationController.isActivated(mDisplayId)) {
            return;
        }

        // TODO(b/354696546): Allow mouse/stylus to activate whichever display they are
        // over, rather than only interacting with the current display.

+66 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.magnification;

import static com.android.server.accessibility.AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID;

import android.annotation.NonNull;

import com.android.server.input.InputManagerInternal;

/**
 * Handles pointer motion event for full screen magnification.
 * Responsible for controlling magnification's cursor following feature.
 */
public class FullScreenMagnificationPointerMotionEventFilter implements
        InputManagerInternal.AccessibilityPointerMotionFilter {

    private final FullScreenMagnificationController mController;

    public FullScreenMagnificationPointerMotionEventFilter(
            FullScreenMagnificationController controller) {
        mController = controller;
    }

    /**
     * This call happens on the input hot path and it is extremely performance sensitive. It
     * also must not call back into native code.
     */
    @Override
    @NonNull
    public float[] filterPointerMotionEvent(float dx, float dy, float currentX, float currentY,
            int displayId) {
        if (!mController.isActivated(displayId)) {
            // unrelated display.
            return new float[]{dx, dy};
        }

        // TODO(361817142): implement centered and edge following types.

        // Continuous cursor following.
        float scale = mController.getScale(displayId);
        final float newCursorX = currentX + dx;
        final float newCursorY = currentY + dy;
        mController.setOffset(displayId,
                newCursorX - newCursorX * scale, newCursorY - newCursorY * scale,
                MAGNIFICATION_GESTURE_HANDLER_ID);

        // In the continuous mode, the cursor speed in physical display is kept.
        // Thus, we don't consume any motion delta.
        return new float[]{dx, dy};
    }
}
+94 −0
Original line number Diff line number Diff line
@@ -30,7 +30,10 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -247,6 +250,40 @@ public class FullScreenMagnificationControllerTest {
        verify(mMockThumbnail, times(2)).hideThumbnail();
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER)
    public void testRegister_RegistersPointerMotionFilter() {
        register(DISPLAY_0);

        verify(mMockInputManager).registerAccessibilityPointerMotionFilter(
                any(InputManagerInternal.AccessibilityPointerMotionFilter.class));

        // If a filter is already registered, adding a display won't invoke another filter
        // registration.
        clearInvocations(mMockInputManager);
        register(DISPLAY_1);
        register(INVALID_DISPLAY);

        verify(mMockInputManager, times(0)).registerAccessibilityPointerMotionFilter(
                any(InputManagerInternal.AccessibilityPointerMotionFilter.class));
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER)
    public void testUnregister_UnregistersPointerMotionFilter() {
        register(DISPLAY_0);
        register(DISPLAY_1);
        clearInvocations(mMockInputManager);

        mFullScreenMagnificationController.unregister(DISPLAY_1);
        // There's still an active display. Don't unregister yet.
        verify(mMockInputManager, times(0)).registerAccessibilityPointerMotionFilter(
                nullable(InputManagerInternal.AccessibilityPointerMotionFilter.class));

        mFullScreenMagnificationController.unregister(DISPLAY_0);
        verify(mMockInputManager, times(1)).registerAccessibilityPointerMotionFilter(isNull());
    }

    @Test
    public void testInitialState_noMagnificationAndMagnificationRegionReadFromWindowManager() {
        for (int i = 0; i < DISPLAY_COUNT; i++) {
@@ -698,6 +735,63 @@ public class FullScreenMagnificationControllerTest {
        verifyNoMoreInteractions(mMockWindowManager);
    }

    @Test
    public void testSetOffset_whileMagnifying_offsetsMove() {
        for (int i = 0; i < DISPLAY_COUNT; i++) {
            setOffset_whileMagnifying_offsetsMove(i);
            resetMockWindowManager();
        }
    }

    private void setOffset_whileMagnifying_offsetsMove(int displayId) {
        register(displayId);
        PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER;
        for (final float scale : new float[]{2.0f, 2.5f, 3.0f}) {
            assertTrue(mFullScreenMagnificationController
                    .setScaleAndCenter(displayId, scale, startCenter.x, startCenter.y, true, false,
                            SERVICE_ID_1));
            mMessageCapturingHandler.sendAllMessages();

            for (final PointF center : new PointF[]{
                    INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER,
                    INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER}) {
                Mockito.clearInvocations(mMockWindowManager);
                PointF newOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, center, scale);
                mFullScreenMagnificationController.setOffset(displayId, newOffsets.x, newOffsets.y,
                        SERVICE_ID_1);
                mMessageCapturingHandler.sendAllMessages();

                MagnificationSpec expectedSpec = getMagnificationSpec(scale, newOffsets);
                verify(mMockWindowManager)
                        .setMagnificationSpec(eq(displayId), argThat(closeTo(expectedSpec)));
                assertEquals(center.x, mFullScreenMagnificationController.getCenterX(displayId),
                        0.0);
                assertEquals(center.y, mFullScreenMagnificationController.getCenterY(displayId),
                        0.0);
                verify(mMockValueAnimator, times(0)).start();
            }
        }
    }

    @Test
    public void testSetOffset_whileNotMagnifying_hasNoEffect() {
        for (int i = 0; i < DISPLAY_COUNT; i++) {
            setOffset_whileNotMagnifying_hasNoEffect(i);
            resetMockWindowManager();
        }
    }

    private void setOffset_whileNotMagnifying_hasNoEffect(int displayId) {
        register(displayId);
        Mockito.reset(mMockWindowManager);
        MagnificationSpec startSpec = getCurrentMagnificationSpec(displayId);
        mFullScreenMagnificationController.setOffset(displayId, 100, 100, SERVICE_ID_1);
        assertThat(getCurrentMagnificationSpec(displayId), closeTo(startSpec));
        mFullScreenMagnificationController.setOffset(displayId, 200, 200, SERVICE_ID_1);
        assertThat(getCurrentMagnificationSpec(displayId), closeTo(startSpec));
        verifyNoMoreInteractions(mMockWindowManager);
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE)
    public void testStartFling_whileMagnifying_flings() throws InterruptedException {
Loading