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

Commit cf191b8c authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Re-implement full screen magnification continuous cursor following" into main

parents 1eeb9667 872880ee
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