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

Commit bea505e1 authored by Yongshun Liu's avatar Yongshun Liu
Browse files

a11y: Throttle onMouseMove in MagnificationController

This change introduces a 300ms throttle to the onMouseMove event
handler in MagnificationController. This prevents the handler from
being called too frequently during rapid mouse movements, which can
cause performance degradation.

New unit tests are added to verify that events are correctly
rate-limited and that they are processed again after the throttle
period has passed.

Bug: 424272057
Flag: EXEMPT bugfix
Test: atest MagnificationControllerTest
Test: atest IMagnificationConnectionTest
Change-Id: I79d959631963612d861333f708dccf193529beac
parent 388f0cd6
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -91,6 +91,21 @@ public final class AccessibilityUtils {
    @VisibleForTesting
    public static final String MENU_SERVICE_RELATIVE_CLASS_NAME = ".AccessibilityMenuService";

    /**
     * The delay time in milliseconds for showing the magnification button.
     * @hide
     */
    public static final long MAGNIFICATION_SHOW_BUTTON_DELAY_MS = 300;

    /**
     * The interval in milliseconds for throttling UI changes and reduce IPC to show/hide the
     * magnification mode switch button. It's set to be the same as
     * {@link #MAGNIFICATION_SHOW_BUTTON_DELAY_MS} so that users won't notice a delay.
     * @hide
     */
    public static final long MAGNIFICATION_HANDLE_UI_CHANGE_INTERVAL_MS =
            MAGNIFICATION_SHOW_BUTTON_DELAY_MS;

    /**
     * {@link ComponentName} for the Accessibility Menu {@link AccessibilityService} as provided
     * inside the system build, used for automatic migration to this version of the service.
+2 −2
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import android.window.InputTransferToken;

import androidx.annotation.NonNull;

import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.systemui.LauncherProxyService;
@@ -72,7 +73,6 @@ import javax.inject.Inject;
public class MagnificationImpl implements Magnification, CommandQueue.Callbacks {
    private static final String TAG = "Magnification";

    @VisibleForTesting static final int DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS = 300;
    private static final int MSG_SHOW_MAGNIFICATION_BUTTON_INTERNAL = 1;

    private final ModeSwitchesController mModeSwitchesController;
@@ -429,7 +429,7 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
        mHandler.sendMessageDelayed(
                mHandler.obtainMessage(
                        MSG_SHOW_MAGNIFICATION_BUTTON_INTERNAL, displayId, magnificationMode),
                DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS);
                AccessibilityUtils.MAGNIFICATION_SHOW_BUTTON_DELAY_MS);
    }

    @MainThread
+8 −9
Original line number Diff line number Diff line
@@ -16,8 +16,6 @@

package com.android.systemui.accessibility;

import static com.android.systemui.accessibility.MagnificationImpl.DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -44,6 +42,7 @@ import android.view.accessibility.IRemoteMagnificationAnimationCallback;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.systemui.LauncherProxyService;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.model.SysUiState;
@@ -200,10 +199,10 @@ public class IMagnificationConnectionTest extends SysuiTestCase {
        // showMagnificationButton request to Magnification.
        processAllPendingMessages();

        // The delayed message would be processed after DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS.
        // So call this processAllPendingMessages with a timeout to verify the showButton
        // will be called.
        int timeout = DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS + 100;
        // The delayed message would be processed after
        // AccessibilityUtils.MAGNIFICATION_SHOW_BUTTON_DELAY_MS. So call this
        // processAllPendingMessages with a timeout to verify the showButton will be called.
        long timeout = AccessibilityUtils.MAGNIFICATION_SHOW_BUTTON_DELAY_MS + 100;
        processAllPendingMessages(timeout);
        verify(mModeSwitchesController).showButton(TEST_DISPLAY,
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
@@ -222,7 +221,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase {

        // The isMagnificationSettingsShowing will be checked after timeout, so
        // process all message after a timeout here to verify the showButton will not be called.
        processAllPendingMessages(DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS + 100);
        processAllPendingMessages(AccessibilityUtils.MAGNIFICATION_SHOW_BUTTON_DELAY_MS + 100);
        verify(mModeSwitchesController, never()).showButton(TEST_DISPLAY,
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
    }
@@ -249,7 +248,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase {

        // Call this processAllPendingMessages with a timeout to ensure the delayed show button
        // message should be removed and thus the showButton will not be called after timeout.
        int timeout = DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS + 100;
        long timeout = AccessibilityUtils.MAGNIFICATION_SHOW_BUTTON_DELAY_MS + 100;
        processAllPendingMessages(/* timeForwardMs= */ timeout);
        verify(mModeSwitchesController, never()).showButton(TEST_DISPLAY,
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
@@ -281,7 +280,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase {
        processAllPendingMessages(/* timeForwardMs=*/ 0);
    }

    private void processAllPendingMessages(int timeForwardMs) {
    private void processAllPendingMessages(long timeForwardMs) {
        if (timeForwardMs > 0) {
            mTestableLooper.moveTimeForward(timeForwardMs);
        }
+10 −0
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ import android.view.ViewConfiguration;
import android.view.accessibility.MagnificationAnimationCallback;

import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -125,6 +126,7 @@ public class MagnificationController implements MagnificationConnectionManager.C
    // in multiple directions at once (for example, up + left), tracking last
    // panned time ensures that panning doesn't occur too frequently.
    private long mLastPannedTime = 0;
    private long mLastMouseMoveTriggeredUiChangeTime = 0;
    private boolean mRepeatKeysEnabled = true;

    private @ZoomDirection int mActiveZoomDirection = ZOOM_DIRECTION_IN;
@@ -386,16 +388,24 @@ public class MagnificationController implements MagnificationConnectionManager.C

    @Override
    public void onTouchInteractionStart(int displayId, int mode) {
        // TODO(435498747): Add throttling for touch similarly to mouse events.
        handleUserInteractionChanged(displayId, mode);
    }

    @Override
    public void onTouchInteractionEnd(int displayId, int mode) {
        // TODO(435498747): Add throttling for touch similarly to mouse events.
        handleUserInteractionChanged(displayId, mode);
    }

    @Override
    public void onMouseMove(int displayId, int mode) {
        final long currentTime = mSystemClock.uptimeMillis();
        if (currentTime - mLastMouseMoveTriggeredUiChangeTime
                < AccessibilityUtils.MAGNIFICATION_HANDLE_UI_CHANGE_INTERVAL_MS) {
            return;
        }
        mLastMouseMoveTriggeredUiChangeTime = currentTime;
        handleUserInteractionChanged(displayId, mode);
    }

+42 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.floatThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
@@ -81,6 +82,7 @@ import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.LocalServices;
@@ -2192,6 +2194,46 @@ public class MagnificationControllerTest {
                eq(TEST_DISPLAY));
    }

    @Test
    public void onMouseMove_withThrottle_shouldRateLimit() throws RemoteException {
        mMagnificationController.setMagnificationCapabilities(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL);
        setMagnificationEnabled(MODE_FULLSCREEN);
        clearInvocations(mMagnificationConnectionManager);

        // The first call should go through and trigger a UI update.
        mMagnificationController.onMouseMove(TEST_DISPLAY, MODE_FULLSCREEN);
        verify(mMagnificationConnectionManager).showMagnificationButton(
                TEST_DISPLAY, MODE_FULLSCREEN);
        clearInvocations(mMagnificationConnectionManager);

        // Subsequent calls within the throttle period should be ignored.
        mMagnificationController.onMouseMove(TEST_DISPLAY, MODE_FULLSCREEN);
        mMagnificationController.onMouseMove(TEST_DISPLAY, MODE_FULLSCREEN);
        verify(mMagnificationConnectionManager, never()).showMagnificationButton(
                TEST_DISPLAY, MODE_FULLSCREEN);
    }

    @Test
    public void onMouseMove_withThrottle_shouldNotRateLimitAfterDelay() throws RemoteException {
        mMagnificationController.setMagnificationCapabilities(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL);
        setMagnificationEnabled(MODE_FULLSCREEN);
        clearInvocations(mMagnificationConnectionManager);

        // The first call should go through and trigger a UI update.
        mMagnificationController.onMouseMove(TEST_DISPLAY, MODE_FULLSCREEN);
        verify(mMagnificationConnectionManager).showMagnificationButton(
                TEST_DISPLAY, MODE_FULLSCREEN);
        clearInvocations(mMagnificationConnectionManager);

        // Advance time past the throttle period. The next call should now go through.
        mSystemClock.advanceTime(AccessibilityUtils.MAGNIFICATION_HANDLE_UI_CHANGE_INTERVAL_MS + 1);
        mMagnificationController.onMouseMove(TEST_DISPLAY, MODE_FULLSCREEN);
        verify(mMagnificationConnectionManager).showMagnificationButton(
                TEST_DISPLAY, MODE_FULLSCREEN);
    }

    @Test
    public void enableWindowMode_showMagnificationButton()
            throws RemoteException {