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

Commit 45dd543e authored by Yongshun Liu's avatar Yongshun Liu
Browse files

a11y: Add cursor following center+edge mode

With this, the full-screen magnification viewport has the ability to
follow the cursor automatically in center and edge mode.

Bug: b/355503630
Bug: b/355727440
Flag: com.android.server.accessibility.enable_magnification_follows_mouse_with_pointer_motion_filter
Test: FrameworksServicesTests:com.android.server.accessibility.magnification.FullScreenMagnificationPointerMotionEventFilterTest
Change-Id: Iaf2b3c45e849fb44f4abfc374a6234b7383dca5c
parent 0c53eace
Loading
Loading
Loading
Loading
+139 −0
Original line number Diff line number Diff line
@@ -125,6 +125,118 @@ public class FullScreenMagnificationController implements

    private boolean mIsPointerMotionFilterInstalled = false;

    /**
     * Full screen magnification data for {@link FullScreenMagnificationPointerMotionEventFilter}.
     */
    public static class FullScreenMagnificationData {
        // LINT.IfChange(data_declaration)
        private boolean mActivated;
        private float mScale;
        @NonNull
        private Rect mBounds;
        private float mOffsetX;
        private float mMinOffsetX;
        private float mMaxOffsetX;
        private float mOffsetY;
        private float mMinOffsetY;
        private float mMaxOffsetY;
        // LINT.ThenChange(FullScreenMagnificationController.java:data_reset)

        public FullScreenMagnificationData() {
            mBounds = new Rect();
        }

        public boolean isActivated() {
            return mActivated;
        }

        public void setActivated(boolean activated) {
            mActivated = activated;
        }

        public float getScale() {
            return mScale;
        }

        public void setScale(float scale) {
            mScale = scale;
        }

        @NonNull
        public Rect getBounds() {
            return mBounds;
        }

        public void setBounds(@NonNull Rect bounds) {
            mBounds = bounds;
        }

        public float getOffsetX() {
            return mOffsetX;
        }

        public void setOffsetX(float offsetX) {
            mOffsetX = offsetX;
        }

        public float getMinOffsetX() {
            return mMinOffsetX;
        }

        public void setMinOffsetX(float minOffsetX) {
            mMinOffsetX = minOffsetX;
        }

        public float getMaxOffsetX() {
            return mMaxOffsetX;
        }

        public void setMaxOffsetX(float maxOffsetX) {
            mMaxOffsetX = maxOffsetX;
        }

        public float getOffsetY() {
            return mOffsetY;
        }

        public void setOffsetY(float offsetY) {
            mOffsetY = offsetY;
        }

        public float getMinOffsetY() {
            return mMinOffsetY;
        }

        public void setMinOffsetY(float minOffsetY) {
            mMinOffsetY = minOffsetY;
        }

        public float getMaxOffsetY() {
            return mMaxOffsetY;
        }

        public void setMaxOffsetY(float maxOffsetY) {
            mMaxOffsetY = maxOffsetY;
        }

        /**
         * Resets the data to default.
         */
        public void reset() {
            // LINT.IfChange(data_reset)
            mActivated = false;
            mScale = 1.0f;
            mBounds.setEmpty();
            mOffsetX = 0.0f;
            mMinOffsetX = 0.0f;
            mMaxOffsetX = 0.0f;
            mOffsetY = 0.0f;
            mMinOffsetY = 0.0f;
            mMaxOffsetY = 0.0f;
            // LINT.ThenChange(FullScreenMagnificationController.java:data_declaration)
        }
    }

    /**
     * This class implements {@link WindowManagerInternal.MagnificationCallbacks} and holds
     * magnification information per display.
@@ -1255,6 +1367,33 @@ public class FullScreenMagnificationController implements
        }
    }

    /**
     * Gets the full screen magnification data needed by
     * {@link #FullScreenMagnificationPointerMotionEventFilter}.
     *
     * @param displayId The logical display id.
     * @param outMagnificationData The magnification data to populate.
     */
    public void getFullScreenMagnificationData(int displayId,
            @NonNull FullScreenMagnificationData outMagnificationData) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                outMagnificationData.reset();
            } else {
                outMagnificationData.mActivated = display.isActivated();
                outMagnificationData.mScale = display.getScale();
                display.getMagnificationBounds(outMagnificationData.mBounds);
                outMagnificationData.mOffsetX = display.getOffsetX();
                outMagnificationData.mMinOffsetX = display.getMinOffsetXLocked();
                outMagnificationData.mMaxOffsetX = display.getMaxOffsetXLocked();
                outMagnificationData.mOffsetY = display.getOffsetY();
                outMagnificationData.mMinOffsetY = display.getMinOffsetYLocked();
                outMagnificationData.mMaxOffsetY = display.getMaxOffsetYLocked();
            }
        }
    }

    /**
     * Populates the specified rect with the screen-relative bounds of the
     * magnification region. If magnification is not enabled, the returned
+106 −8
Original line number Diff line number Diff line
@@ -16,24 +16,55 @@

package com.android.server.accessibility.magnification;

import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE;

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

import android.annotation.NonNull;
import android.provider.Settings.Secure.AccessibilityMagnificationCursorFollowingMode;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.input.InputManagerInternal;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Handles pointer motion event for full screen magnification.
 * Responsible for controlling magnification's cursor following feature.
 */
public class FullScreenMagnificationPointerMotionEventFilter implements
        InputManagerInternal.AccessibilityPointerMotionFilter {
    private static final String TAG =
            FullScreenMagnificationPointerMotionEventFilter.class.getSimpleName();

    // TODO(b/413146817): Convert this from px to dip.
    @VisibleForTesting
    static final float EDGE_MODE_MARGIN_PX = 100.f;

    private final FullScreenMagnificationController mController;

    @NonNull
    private final FullScreenMagnificationController.FullScreenMagnificationData mMagnificationData;

    private final AtomicInteger mMode = new AtomicInteger(
            ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS);

    public FullScreenMagnificationPointerMotionEventFilter(
            FullScreenMagnificationController controller) {
        mController = controller;
        mMagnificationData = new FullScreenMagnificationController.FullScreenMagnificationData();
    }

    /**
     * Sets cursor following mode.
     *
     * @param mode The cursor following mode
     */
    public void setMode(@AccessibilityMagnificationCursorFollowingMode int mode) {
        mMode.set(mode);
    }

    /**
@@ -44,23 +75,90 @@ public class FullScreenMagnificationPointerMotionEventFilter implements
    @NonNull
    public float[] filterPointerMotionEvent(float dx, float dy, float currentX, float currentY,
            int displayId) {
        if (!mController.isActivated(displayId)) {
            // unrelated display.
        mController.getFullScreenMagnificationData(displayId, mMagnificationData);

        // Unrelated display.
        if (!mMagnificationData.isActivated()) {
            return new float[]{dx, dy};
        }

        // TODO(361817142): implement centered and edge following types.
        final int currentMode = mMode.get();
        final boolean continuousMode =
                currentMode == ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS;
        final boolean centerMode =
                currentMode == ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER;
        final boolean edgeMode =
                currentMode == ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE;

        // Center or edge mode.
        if (centerMode || edgeMode) {
            final float marginWidth;
            final float marginHeight;
            if (centerMode) {
                marginWidth = mMagnificationData.getBounds().width() / 2.f;
                marginHeight = mMagnificationData.getBounds().height() / 2.f;
            } else {
                marginWidth = EDGE_MODE_MARGIN_PX;
                marginHeight = EDGE_MODE_MARGIN_PX;
            }

            float moveX = 0;
            float newX = dx;
            if (dx > 0 && currentX >= mMagnificationData.getBounds().right - marginWidth) {
                moveX = dx * mMagnificationData.getScale();
                newX = 0;
                if (moveX > mMagnificationData.getOffsetX() - mMagnificationData.getMinOffsetX()) {
                    moveX = mMagnificationData.getOffsetX() - mMagnificationData.getMinOffsetX();
                    newX = dx - moveX / mMagnificationData.getScale();
                }
            } else if (dx < 0 && currentX <= mMagnificationData.getBounds().left + marginWidth) {
                moveX = dx * mMagnificationData.getScale();
                newX = 0;
                if (moveX < mMagnificationData.getOffsetX() - mMagnificationData.getMaxOffsetX()) {
                    moveX = mMagnificationData.getOffsetX() - mMagnificationData.getMaxOffsetX();
                    newX = dx - moveX / mMagnificationData.getScale();
                }
            }

            float moveY = 0;
            float newY = dy;
            if (dy > 0 && currentY >= mMagnificationData.getBounds().bottom - marginHeight) {
                moveY = dy * mMagnificationData.getScale();
                newY = 0;
                if (moveY > mMagnificationData.getOffsetY() - mMagnificationData.getMinOffsetY()) {
                    moveY = mMagnificationData.getOffsetY() - mMagnificationData.getMinOffsetY();
                    newY = dy - moveY / mMagnificationData.getScale();
                }
            } else if (dy < 0 && currentY <= mMagnificationData.getBounds().top + marginHeight) {
                moveY = dy * mMagnificationData.getScale();
                newY = 0;
                if (moveY < mMagnificationData.getOffsetY() - mMagnificationData.getMaxOffsetY()) {
                    moveY = mMagnificationData.getOffsetY() - mMagnificationData.getMaxOffsetY();
                    newY = dy - moveY / mMagnificationData.getScale();
                }
            }

            mController.offsetMagnifiedRegion(displayId, moveX, moveY,
                    MAGNIFICATION_GESTURE_HANDLER_ID);

            return new float[]{newX, newY};
        }

        // For unexpected mode, fall back to continuous mode.
        if (!continuousMode) {
            Slog.e(TAG, "Magnification cursor following falling back "
                    + "to continuous mode with unexpected mode: " + currentMode);
        }

        // 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,
        mController.setOffset(displayId, newCursorX - newCursorX * mMagnificationData.getScale(),
                newCursorY - newCursorY * mMagnificationData.getScale(),
                MAGNIFICATION_GESTURE_HANDLER_ID);

        // In the continuous mode, the cursor speed in physical display is kept.
        // Thus, we don't consume any motion delta.
        // 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};
    }
}
+158 −14
Original line number Diff line number Diff line
@@ -18,10 +18,14 @@ package com.android.server.accessibility.magnification;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.graphics.Rect;
import android.provider.Settings;

import androidx.test.runner.AndroidJUnit4;

@@ -46,9 +50,33 @@ public class FullScreenMagnificationPointerMotionEventFilterTest {
                mMockFullScreenMagnificationController);
    }

    void stubMockFullScreenMagnificationController(boolean activated, float scale, Rect bounds,
            boolean centerOrBottomRight) {
        doAnswer(invocation -> {
            FullScreenMagnificationController.FullScreenMagnificationData outMagnificationData =
                    invocation.getArgument(1);
            outMagnificationData.setActivated(activated);
            outMagnificationData.setScale(scale);
            outMagnificationData.setBounds(bounds);
            outMagnificationData.setOffsetX(centerOrBottomRight
                    ? -bounds.centerX() * (scale - 1.f)
                    : -(bounds.right * scale - bounds.width()));
            outMagnificationData.setMinOffsetX(-bounds.right * (scale - 1.f));
            outMagnificationData.setMaxOffsetX(-bounds.left * (scale - 1.f));
            outMagnificationData.setOffsetY(centerOrBottomRight
                    ? -bounds.centerY() * (scale - 1.f)
                    : -(bounds.bottom * scale - bounds.height()));
            outMagnificationData.setMinOffsetY(-bounds.bottom * (scale - 1.f));
            outMagnificationData.setMaxOffsetY(-bounds.top * (scale - 1.f));
            return null;
        }).when(mMockFullScreenMagnificationController).getFullScreenMagnificationData(anyInt(),
                any(FullScreenMagnificationController.FullScreenMagnificationData.class));
    }

    @Test
    public void inactiveDisplay_doNothing() {
        when(mMockFullScreenMagnificationController.isActivated(anyInt())).thenReturn(false);
        stubMockFullScreenMagnificationController(/* activated= */ false, /* scale= */ 3.0f,
                /* bounds= */ new Rect(0, 0, 800, 600), /* centerOrBottomRight= */ true);

        float[] delta = new float[]{1.f, 2.f};
        float[] result = mFilter.filterPointerMotionEvent(delta[0], delta[1], 3.0f, 4.0f, 0);
@@ -56,24 +84,140 @@ public class FullScreenMagnificationPointerMotionEventFilterTest {
    }

    @Test
    public void testContinuousMove() {
        when(mMockFullScreenMagnificationController.isActivated(anyInt())).thenReturn(true);
        when(mMockFullScreenMagnificationController.getScale(anyInt())).thenReturn(3.f);
    public void continuousMode() {
        mFilter.setMode(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS);

        final float scale = 3.f;
        stubMockFullScreenMagnificationController(/* activated= */ true, scale,
                /* bounds= */ new Rect(0, 0, 800, 600), /* centerOrBottomRight= */ true);

        float[] delta = new float[]{5.f, 10.f};
        float[] result = mFilter.filterPointerMotionEvent(delta[0], delta[1], 20.f, 30.f, 0);
        assertThat(result).isEqualTo(delta);
        // At the first cursor move, it goes to (20, 30) + (5, 10) = (25, 40). The scale is 3.0.
        // The expected offset is (-25 * (3-1), -40 * (3-1)) = (-50, -80).
        // The expected viewport offset is (-25 * (3-1), -40 * (3-1)) = (-50, -80). The cursor
        // movement for the physical display is kept the same.
        float[] currentXy =  new float[]{20.f, 30.f};
        float[] deltaXy = new float[]{5.f, 10.f};
        float[] adjustedDeltaXy = mFilter.filterPointerMotionEvent(deltaXy[0], deltaXy[1],
                currentXy[0], currentXy[1], 0);
        currentXy[0] += adjustedDeltaXy[0];
        currentXy[1] += adjustedDeltaXy[1];
        assertThat(adjustedDeltaXy).isEqualTo(deltaXy);
        assertThat(currentXy).isEqualTo(new float[]{25.f, 40.f});
        verify(mMockFullScreenMagnificationController)
                .setOffset(eq(0), eq(-50.f), eq(-80.f), anyInt());

        float[] delta2 = new float[]{10.f, 5.f};
        float[] result2 = mFilter.filterPointerMotionEvent(delta2[0], delta2[1], 25.f, 40.f, 0);
        assertThat(result2).isEqualTo(delta2);
        // At the second cursor move, it goes to (25, 40) + (10, 5) = (35, 40). The scale is 3.0.
        // The expected offset is (-35 * (3-1), -45 * (3-1)) = (-70, -90).
        // At the second cursor move, it goes to (25, 40) + (10, 5) = (35, 45). The scale is 3.0.
        // The expected viewport offset is (-35 * (3-1), -45 * (3-1)) = (-70, -90). The cursor
        // movement for the physical display is kept the same.
        deltaXy = new float[]{10.f, 5.f};
        adjustedDeltaXy = mFilter.filterPointerMotionEvent(deltaXy[0], deltaXy[1], currentXy[0],
                currentXy[1], 0);
        currentXy[0] += adjustedDeltaXy[0];
        currentXy[1] += adjustedDeltaXy[1];
        assertThat(adjustedDeltaXy).isEqualTo(deltaXy);
        assertThat(currentXy).isEqualTo(new float[]{35.f, 45.f});
        verify(mMockFullScreenMagnificationController)
                .setOffset(eq(0), eq(-70.f), eq(-90.f), anyInt());
    }

    @Test
    public void centerMode_viewportNotAtEdge_viewportMovesButCursorNotMove() {
        mFilter.setMode(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER);

        // The viewport is at the center of the magnified content.
        final float scale = 3.f;
        final Rect bounds = new Rect(0, 0, 800, 600);
        stubMockFullScreenMagnificationController(/* activated= */ true, scale, bounds,
                /* centerOrBottomRight= */ true);

        // The cursor stays the same. The viewport updates its position. The cursor is at the center
        // of the physical display, (400.f, 300.f).
        float[] currentXy =  new float[]{bounds.centerX(), bounds.centerY()};
        float[] deltaXy = new float[]{5.f, 10.f};
        float[] adjustedDeltaXy = mFilter.filterPointerMotionEvent(deltaXy[0], deltaXy[1],
                currentXy[0], currentXy[1], 0);
        currentXy[0] += adjustedDeltaXy[0];
        currentXy[1] += adjustedDeltaXy[1];
        assertThat(adjustedDeltaXy).isEqualTo(new float[]{0.f, 0.f});
        assertThat(currentXy).isEqualTo(new float[]{400.f, 300.f});
        verify(mMockFullScreenMagnificationController)
                .offsetMagnifiedRegion(eq(0), eq(15.f), eq(30.f), anyInt());
    }

    @Test
    public void centerMode_viewportAtEdge_cursorMovesButViewportNotMove() {
        mFilter.setMode(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER);

        // The viewport is at the bottom right corner of the magnified
        // content.
        final float scale = 3.f;
        final Rect bounds = new Rect(0, 0, 800, 600);
        stubMockFullScreenMagnificationController(/* activated= */ true, scale, bounds,
                /* centerOrBottomRight= */ false);

        // The cursor updates its position. The viewport stays the same. The cursor is at the center
        // of the physical display, (400.f, 300.f).
        float[] currentXy =  new float[]{bounds.centerX(), bounds.centerY()};
        float[] deltaXy = new float[]{5.f, 10.f};
        float[] adjustedDeltaXy = mFilter.filterPointerMotionEvent(deltaXy[0], deltaXy[1],
                currentXy[0], currentXy[1], 0);
        currentXy[0] += adjustedDeltaXy[0];
        currentXy[1] += adjustedDeltaXy[1];
        assertThat(adjustedDeltaXy).isEqualTo(new float[]{deltaXy[0], deltaXy[1]});
        // The new cursor position: {400.f + 5.f, 300.f + 10.f}.
        assertThat(currentXy).isEqualTo(new float[]{405.f, 310.f});
        verify(mMockFullScreenMagnificationController)
                .offsetMagnifiedRegion(eq(0), eq(0.f), eq(0.f), anyInt());
    }

    @Test
    public void edgeMode_cursorNotAtEdge_cursorMovesButViewportNotMove() {
        mFilter.setMode(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE);

        // The viewport is at the center of the magnified content.
        final float scale = 3.f;
        final Rect bounds = new Rect(0, 0, 800, 600);
        stubMockFullScreenMagnificationController(/* activated= */ true, scale, bounds,
                /* centerOrBottomRight= */ true);

        // The cursor updates its position. The viewport stays the same. The cursor is at the center
        // of the physical display, (400.f, 300.f).
        float[] currentXy =  new float[]{bounds.centerX(), bounds.centerY()};
        float[] deltaXy = new float[]{5.f, 10.f};
        float[] adjustedDeltaXy = mFilter.filterPointerMotionEvent(deltaXy[0], deltaXy[1],
                currentXy[0], currentXy[1], 0);
        currentXy[0] += adjustedDeltaXy[0];
        currentXy[1] += adjustedDeltaXy[1];
        assertThat(adjustedDeltaXy).isEqualTo(new float[]{deltaXy[0], deltaXy[1]});
        // The new cursor position: {400.f + 5.f, 300.f + 10.f}.
        assertThat(currentXy).isEqualTo(new float[]{405.f, 310.f});
        verify(mMockFullScreenMagnificationController)
                .offsetMagnifiedRegion(eq(0), eq(0.f), eq(0.f), anyInt());
    }

    @Test
    public void edgeMode_cursorAtEdge_cursorNotMoveButViewportMoves() {
        mFilter.setMode(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE);

        // The viewport is at the center of the magnified content.
        final float scale = 3.f;
        final Rect bounds = new Rect(0, 0, 800, 600);
        stubMockFullScreenMagnificationController(/* activated= */ true, scale, bounds,
                /* centerOrBottomRight= */ true);

        // The cursor stays the same. The viewport updates its position. The cursor is at the edge
        // of the physical display, (800.f - 100.f, 600.f - 100.f).
        final float margin = FullScreenMagnificationPointerMotionEventFilter.EDGE_MODE_MARGIN_PX;
        float[] currentXy =  new float[]{bounds.right - margin, bounds.bottom - margin};
        float[] deltaXy = new float[]{5.f, 10.f};
        float[] adjustedDeltaXy = mFilter.filterPointerMotionEvent(deltaXy[0], deltaXy[1],
                currentXy[0], currentXy[1], 0);
        currentXy[0] += adjustedDeltaXy[0];
        currentXy[1] += adjustedDeltaXy[1];
        assertThat(adjustedDeltaXy).isEqualTo(new float[]{0.f, 0.f});
        // The new cursor position: {800.f - 100.f + 0.f, 600.f - 100.f + 0.f}.
        assertThat(currentXy).isEqualTo(new float[]{700.f, 500.f});
        verify(mMockFullScreenMagnificationController)
                .offsetMagnifiedRegion(eq(0), eq(15.f), eq(30.f), anyInt());
    }
}