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

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

Merge "Magnification pans diagonally with same radius as straight." into main

parents adbf70a7 3c730b71
Loading
Loading
Loading
Loading
+117 −36
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
import static android.util.MathUtils.sqrt;

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

@@ -38,7 +39,6 @@ import android.graphics.Region;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.DisplayMetrics;
@@ -116,8 +116,14 @@ public class MagnificationController implements MagnificationConnectionManager.C
    private final Executor mBackgroundExecutor;

    private final Handler mHandler;
    private @PanDirection int mActivePanDirection = PAN_DIRECTION_DOWN;
    // Prefer this to SystemClock, because it allows for tests to influence behavior.
    private SystemClock mSystemClock;
    private boolean[] mActivePanDirections = {false, false, false, false};
    private int mActivePanDisplay = Display.INVALID_DISPLAY;
    // The time that panning by keyboard last took place. Since users can pan
    // 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 boolean mRepeatKeysEnabled = true;

    private @ZoomDirection int mActiveZoomDirection = ZOOM_DIRECTION_IN;
@@ -186,6 +192,25 @@ public class MagnificationController implements MagnificationConnectionManager.C
        void onResult(int displayId, boolean success);
    }

    /**
     * Functional interface for providing time. Tests may extend this interface to "control time".
     */
    @VisibleForTesting
    interface SystemClock {
        /**
         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
         */
        long uptimeMillis();
    }

    /** The real system clock for use in production. */
    private static class SystemClockImpl implements SystemClock {
        @Override
        public long uptimeMillis() {
            return android.os.SystemClock.uptimeMillis();
        }
    }


    /**
     * An interface to configure how much the magnification scale should be affected when moving in
@@ -311,6 +336,7 @@ public class MagnificationController implements MagnificationConnectionManager.C
        mScaleProvider = scaleProvider;
        mBackgroundExecutor = backgroundExecutor;
        mHandler = new Handler(looper);
        mSystemClock = new SystemClockImpl();
        LocalServices.getService(WindowManagerInternal.class)
                .getAccessibilityController().setUiChangesForAccessibilityCallbacks(this);
        mSupportWindowMagnification = context.getPackageManager().hasSystemFeature(
@@ -327,10 +353,12 @@ public class MagnificationController implements MagnificationConnectionManager.C
    public MagnificationController(AccessibilityManagerService ams, Object lock,
            Context context, FullScreenMagnificationController fullScreenMagnificationController,
            MagnificationConnectionManager magnificationConnectionManager,
            MagnificationScaleProvider scaleProvider, Executor backgroundExecutor, Looper looper) {
            MagnificationScaleProvider scaleProvider, Executor backgroundExecutor, Looper looper,
            SystemClock systemClock) {
        this(ams, lock, context, scaleProvider, backgroundExecutor, looper);
        mFullScreenMagnificationController = fullScreenMagnificationController;
        mMagnificationConnectionManager = magnificationConnectionManager;
        mSystemClock = systemClock;
    }

    @Override
@@ -368,13 +396,13 @@ public class MagnificationController implements MagnificationConnectionManager.C
    @Override
    public void onPanMagnificationStart(int displayId,
            @MagnificationController.PanDirection int direction) {
        // TODO(b/355499907): Handle multiple pan gestures at the same time (e.g. user may try to
        // pan diagonally) by decreasing diagonal movement by sqrt(2) to make it appear the same
        // speed as non-diagonal movement.
        panMagnificationByStep(displayId, direction);
        mActivePanDirection = direction;
        // Update the current panning state for any callbacks.
        boolean isAlreadyPanning = mActivePanDisplay != Display.INVALID_DISPLAY;
        mActivePanDisplay = displayId;
        if (mRepeatKeysEnabled) {
        mActivePanDirections[direction] = true;
        // React immediately to any new key press by panning in the new composite direction.
        panMagnificationByStep(mActivePanDisplay, mActivePanDirections);
        if (!isAlreadyPanning && mRepeatKeysEnabled) {
            mHandler.sendMessageDelayed(
                    PooledLambda.obtainMessage(MagnificationController::maybeContinuePan, this),
                    mInitialKeyboardRepeatIntervalMs);
@@ -382,9 +410,14 @@ public class MagnificationController implements MagnificationConnectionManager.C
    }

    @Override
    public void onPanMagnificationStop(int displayId,
            @MagnificationController.PanDirection int direction) {
        if (direction == mActivePanDirection) {
    public void onPanMagnificationStop(@MagnificationController.PanDirection int direction) {
        // Stop panning in this direction.
        mActivePanDirections[direction] = false;
        if (!mActivePanDirections[PAN_DIRECTION_LEFT]
                && !mActivePanDirections[PAN_DIRECTION_RIGHT]
                && !mActivePanDirections[PAN_DIRECTION_UP]
                && !mActivePanDirections[PAN_DIRECTION_DOWN]) {
            // Stop all panning if no more pan directions were in started.
            mActivePanDisplay = Display.INVALID_DISPLAY;
        }
    }
@@ -392,9 +425,14 @@ public class MagnificationController implements MagnificationConnectionManager.C
    @Override
    public void onScaleMagnificationStart(int displayId,
            @MagnificationController.ZoomDirection int direction) {
        scaleMagnificationByStep(displayId, direction);
        if (mActiveZoomDisplay != Display.INVALID_DISPLAY) {
            // Only allow one zoom direction at a time (even if the other keyboard
            // shortcut has been pressed). Return early if we are already zooming.
            return;
        }
        mActiveZoomDirection = direction;
        mActiveZoomDisplay = displayId;
        scaleMagnificationByStep(displayId, direction);
        if (mRepeatKeysEnabled) {
            mHandler.sendMessageDelayed(
                    PooledLambda.obtainMessage(MagnificationController::maybeContinueZoom, this),
@@ -403,16 +441,27 @@ public class MagnificationController implements MagnificationConnectionManager.C
    }

    @Override
    public void onScaleMagnificationStop(int displayId,
            @MagnificationController.ZoomDirection int direction) {
    public void onScaleMagnificationStop(@MagnificationController.ZoomDirection int direction) {
        if (direction == mActiveZoomDirection) {
            mActiveZoomDisplay = Display.INVALID_DISPLAY;
        }
    }

    @Override
    public void onKeyboardInteractionStop() {
        mActiveZoomDisplay = Display.INVALID_DISPLAY;
        mActivePanDisplay = Display.INVALID_DISPLAY;
        mActivePanDirections = new boolean[]{false, false, false, false};
    }

    private void maybeContinuePan() {
        if (mActivePanDisplay != Display.INVALID_DISPLAY) {
            panMagnificationByStep(mActivePanDisplay, mActivePanDirection);
        if (mActivePanDisplay == Display.INVALID_DISPLAY) {
            return;
        }
        if (mSystemClock.uptimeMillis() - mLastPannedTime >= KEYBOARD_REPEAT_INTERVAL_MS) {
            panMagnificationByStep(mActivePanDisplay, mActivePanDirections);
        }
        if (mRepeatKeysEnabled) {
            mHandler.sendMessageDelayed(
                    PooledLambda.obtainMessage(MagnificationController::maybeContinuePan, this),
                    KEYBOARD_REPEAT_INTERVAL_MS);
@@ -422,11 +471,14 @@ public class MagnificationController implements MagnificationConnectionManager.C
    private void maybeContinueZoom() {
        if (mActiveZoomDisplay != Display.INVALID_DISPLAY) {
            scaleMagnificationByStep(mActiveZoomDisplay, mActiveZoomDirection);
            if (mRepeatKeysEnabled) {
                mHandler.sendMessageDelayed(
                    PooledLambda.obtainMessage(MagnificationController::maybeContinueZoom, this),
                        PooledLambda.obtainMessage(MagnificationController::maybeContinueZoom,
                                this),
                        KEYBOARD_REPEAT_INTERVAL_MS);
            }
        }
    }

    public void setRepeatKeysEnabled(boolean isRepeatKeysEnabled) {
        mRepeatKeysEnabled = isRepeatKeysEnabled;
@@ -719,7 +771,7 @@ public class MagnificationController implements MagnificationConnectionManager.C
    public void onWindowMagnificationActivationState(int displayId, boolean activated) {
        if (activated) {
            synchronized (mLock) {
                mWindowModeEnabledTimeArray.put(displayId, SystemClock.uptimeMillis());
                mWindowModeEnabledTimeArray.put(displayId, mSystemClock.uptimeMillis());
                setCurrentMagnificationModeAndSwitchDelegate(displayId,
                        ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
                mLastMagnificationActivatedModeArray.put(displayId,
@@ -733,7 +785,7 @@ public class MagnificationController implements MagnificationConnectionManager.C
            synchronized (mLock) {
                setCurrentMagnificationModeAndSwitchDelegate(displayId,
                        ACCESSIBILITY_MAGNIFICATION_MODE_NONE);
                duration = SystemClock.uptimeMillis() - mWindowModeEnabledTimeArray.get(displayId);
                duration = mSystemClock.uptimeMillis() - mWindowModeEnabledTimeArray.get(displayId);
                scale = mMagnificationConnectionManager.getLastActivatedScale(displayId);
            }
            logMagnificationUsageState(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, duration, scale);
@@ -830,7 +882,7 @@ public class MagnificationController implements MagnificationConnectionManager.C

        if (activated) {
            synchronized (mLock) {
                mFullScreenModeEnabledTimeArray.put(displayId, SystemClock.uptimeMillis());
                mFullScreenModeEnabledTimeArray.put(displayId, mSystemClock.uptimeMillis());
                setCurrentMagnificationModeAndSwitchDelegate(displayId,
                        ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
                mLastMagnificationActivatedModeArray.put(displayId,
@@ -844,7 +896,7 @@ public class MagnificationController implements MagnificationConnectionManager.C
            synchronized (mLock) {
                setCurrentMagnificationModeAndSwitchDelegate(displayId,
                        ACCESSIBILITY_MAGNIFICATION_MODE_NONE);
                duration = SystemClock.uptimeMillis()
                duration = mSystemClock.uptimeMillis()
                        - mFullScreenModeEnabledTimeArray.get(displayId);
                scale = mFullScreenMagnificationController.getLastActivatedScale(displayId);
            }
@@ -1132,7 +1184,7 @@ public class MagnificationController implements MagnificationConnectionManager.C
     * @param displayId The logical display id.
     * @param direction Whether the scale should be zoomed in or out.
     */
    public void scaleMagnificationByStep(int displayId, @ZoomDirection int direction) {
    private void scaleMagnificationByStep(int displayId, @ZoomDirection int direction) {
        if (getFullScreenMagnificationController().isActivated(displayId)) {
            final float magnificationScale = getFullScreenMagnificationController().getScale(
                    displayId);
@@ -1157,9 +1209,14 @@ public class MagnificationController implements MagnificationConnectionManager.C
     * param.
     *
     * @param displayId The logical display id.
     * @param direction Whether the direction should be left/right/up/down.
     * @param directions The directions to pan, indexed by {@code PanDirection}. If two or more
     *                   are active, panning may be diagonal.
     */
    public void panMagnificationByStep(int displayId, @PanDirection int direction) {
    private void panMagnificationByStep(int displayId, boolean[] directions) {
        if (directions.length != 4) {
            Slog.d(TAG, "Invalid number of panning directions");
            return;
        }
        final boolean fullscreenActivated =
                getFullScreenMagnificationController().isActivated(displayId);
        final boolean windowActivated =
@@ -1168,21 +1225,43 @@ public class MagnificationController implements MagnificationConnectionManager.C
            return;
        }

        int numDirections = (directions[PAN_DIRECTION_LEFT] ? 1 : 0)
                + (directions[PAN_DIRECTION_RIGHT] ? 1 : 0)
                + (directions[PAN_DIRECTION_UP] ? 1 : 0)
                + (directions[PAN_DIRECTION_DOWN] ? 1 : 0);
        if (numDirections == 0) {
            return;
        }

        final float scale = fullscreenActivated
                ? getFullScreenMagnificationController().getScale(displayId)
                        : getMagnificationConnectionManager().getScale(displayId);
        final float step = mPanStepProvider.nextPanStep(scale, displayId);
        float step = mPanStepProvider.nextPanStep(scale, displayId);

        // If the user is trying to pan diagonally (2 directions), divide by the sqrt(2)
        // so that the apparent step length (the radius of the step) is the same as
        // panning in just one direction.
        // Note that if numDirections is 3 or 4, opposite directions will cancel and
        // there's no need to rescale {@code step}.
        if (numDirections == 2) {
            step /= sqrt(2);
        }

        // If two directions cancel out, they will be added and subtracted below for net change 0.
        // This makes the logic simpler than removing out opposite directions manually.
        float offsetX = 0;
        float offsetY = 0;
        if (direction == PAN_DIRECTION_LEFT) {
            offsetX = -step;
        } else if (direction == PAN_DIRECTION_RIGHT) {
            offsetX = step;
        } else if (direction == PAN_DIRECTION_UP) {
            offsetY = -step;
        } else if (direction == PAN_DIRECTION_DOWN) {
            offsetY = step;
        if (directions[PAN_DIRECTION_LEFT]) {
            offsetX -= step;
        }
        if (directions[PAN_DIRECTION_RIGHT]) {
            offsetX += step;
        }
        if (directions[PAN_DIRECTION_UP]) {
            offsetY -= step;
        }
        if (directions[PAN_DIRECTION_DOWN]) {
            offsetY += step;
        }

        if (fullscreenActivated) {
@@ -1194,6 +1273,8 @@ public class MagnificationController implements MagnificationConnectionManager.C
            getMagnificationConnectionManager().moveWindowMagnification(displayId, offsetX,
                    offsetY);
        }

        mLastPannedTime = mSystemClock.uptimeMillis();
    }

    private final class DisableMagnificationCallback implements
+18 −8
Original line number Diff line number Diff line
@@ -46,10 +46,8 @@ public class MagnificationKeyHandler extends BaseEventStreamTransformation {
         * arrows had been pressed at the same time (e.g. diagonal panning).
         *
         * @param displayId The logical display ID
         * @param direction The direction in which panning stopped
         */
        void onPanMagnificationStop(int displayId,
                @MagnificationController.PanDirection int direction);
        void onPanMagnificationStop(int displayId);

        /**
         * Called when a keyboard shortcut to scale magnification in direction `direction` is
@@ -65,14 +63,18 @@ public class MagnificationKeyHandler extends BaseEventStreamTransformation {
         * Called when a keyboard shortcut to scale magnification in direction `direction` is
         * unpressed by a user.
         *
         * @param displayId The logical display ID
         * @param direction The direction in which scaling stopped
         */
        void onScaleMagnificationStop(int displayId,
                @MagnificationController.ZoomDirection int direction);
        void onScaleMagnificationStop(@MagnificationController.ZoomDirection int direction);

        /**
         * Called when all keyboard interaction with magnification should be stopped.
         */
        void onKeyboardInteractionStop();
    }

    protected final MagnificationKeyHandler.Callback mCallback;
    private boolean mIsKeyboardInteracting = false;

    public MagnificationKeyHandler(Callback callback) {
        mCallback = callback;
@@ -88,6 +90,12 @@ public class MagnificationKeyHandler extends BaseEventStreamTransformation {
        boolean modifiersPressed = event.isAltPressed() && event.isMetaPressed();
        if (!modifiersPressed) {
            super.onKeyEvent(event, policyFlags);
            if (mIsKeyboardInteracting) {
                // When modifier keys are no longer pressed, ensure that scaling and
                // panning are fully stopped.
                mCallback.onKeyboardInteractionStop();
                mIsKeyboardInteracting = false;
            }
            return;
        }
        boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN;
@@ -102,8 +110,9 @@ public class MagnificationKeyHandler extends BaseEventStreamTransformation {
            };
            if (isDown) {
                mCallback.onPanMagnificationStart(getDisplayId(event), panDirection);
                mIsKeyboardInteracting = true;
            } else {
                mCallback.onPanMagnificationStop(getDisplayId(event), panDirection);
                mCallback.onPanMagnificationStop(panDirection);
            }
            return;
        } else if (keyCode == KeyEvent.KEYCODE_EQUALS || keyCode == KeyEvent.KEYCODE_MINUS) {
@@ -113,8 +122,9 @@ public class MagnificationKeyHandler extends BaseEventStreamTransformation {
            }
            if (isDown) {
                mCallback.onScaleMagnificationStart(getDisplayId(event), zoomDirection);
                mIsKeyboardInteracting = true;
            } else {
                mCallback.onScaleMagnificationStop(getDisplayId(event), zoomDirection);
                mCallback.onScaleMagnificationStop(zoomDirection);
            }
            return;
        }
+566 −48

File changed.

Preview size limit exceeded, changes collapsed.

+68 −31
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

/**
@@ -78,9 +79,10 @@ public class MagnificationKeyHandlerTest {

        // No callbacks were called.
        verify(mCallback, times(0)).onPanMagnificationStart(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStart(anyInt(), anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt());
        verify(mCallback, times(0)).onKeyboardInteractionStop();

        // The event was passed on.
        verify(mNextHandler, times(1)).onKeyEvent(event, 0);
@@ -95,9 +97,10 @@ public class MagnificationKeyHandlerTest {

        // No callbacks were called.
        verify(mCallback, times(0)).onPanMagnificationStart(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStart(anyInt(), anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt());
        verify(mCallback, times(0)).onKeyboardInteractionStop();

        // The event was passed on.
        verify(mNextHandler, times(1)).onKeyEvent(event, 0);
@@ -112,9 +115,10 @@ public class MagnificationKeyHandlerTest {

        // No callbacks were called.
        verify(mCallback, times(0)).onPanMagnificationStart(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStart(anyInt(), anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt());
        verify(mCallback, times(0)).onKeyboardInteractionStop();

        // The event was passed on.
        verify(mNextHandler, times(1)).onKeyEvent(event, 0);
@@ -157,48 +161,79 @@ public class MagnificationKeyHandlerTest {
        mMkh.onKeyEvent(downLeftEvent, 0);
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_LEFT);
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt());

        Mockito.clearInvocations(mCallback);

        // Also press the down arrow key.
        final KeyEvent downDownEvent = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_DPAD_DOWN, 0, KeyEvent.META_META_ON | KeyEvent.META_ALT_ON);
        mMkh.onKeyEvent(downDownEvent, 0);
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
        verify(mCallback, times(0)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_LEFT);
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_DOWN);
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt());

        Mockito.clearInvocations(mCallback);

        // Lift the left arrow key.
        final KeyEvent upLeftEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_DPAD_LEFT, 0, KeyEvent.META_META_ON | KeyEvent.META_ALT_ON);
        mMkh.onKeyEvent(upLeftEvent, 0);
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
        verify(mCallback, times(0)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_LEFT);
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_DOWN);
        verify(mCallback, times(1)).onPanMagnificationStop(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_LEFT);
        verify(mCallback, times(0)).onPanMagnificationStop(Display.DEFAULT_DISPLAY,
        verify(mCallback, times(0)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_DOWN);
        verify(mCallback, times(1)).onPanMagnificationStop(PAN_DIRECTION_LEFT);
        verify(mCallback, times(0)).onPanMagnificationStop(PAN_DIRECTION_DOWN);

        Mockito.clearInvocations(mCallback);

        // Lift the down arrow key.
        final KeyEvent upDownEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_DPAD_DOWN, 0, KeyEvent.META_META_ON | KeyEvent.META_ALT_ON);
        mMkh.onKeyEvent(upDownEvent, 0);
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
        verify(mCallback, times(0)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_LEFT);
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_DOWN);
        verify(mCallback, times(1)).onPanMagnificationStop(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_LEFT);
        verify(mCallback, times(1)).onPanMagnificationStop(Display.DEFAULT_DISPLAY,
        verify(mCallback, times(0)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_DOWN);
        verify(mCallback, times(0)).onPanMagnificationStop(PAN_DIRECTION_LEFT);
        verify(mCallback, times(1)).onPanMagnificationStop(PAN_DIRECTION_DOWN);

        // The event was not passed on.
        verify(mNextHandler, times(0)).onKeyEvent(any(), anyInt());
    }

    @Test
    public void testPanMagnification_modifiersReleasedBeforeArrows() {
        final KeyEvent downEvent = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_DPAD_DOWN, 0,
                KeyEvent.META_META_ON | KeyEvent.META_ALT_ON);
        mMkh.onKeyEvent(downEvent, 0);

        // Pan started.
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_DOWN);
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt());
        verify(mCallback, times(0)).onKeyboardInteractionStop();

        Mockito.clearInvocations(mCallback);

        // Lift the "meta" key.
        final KeyEvent upEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_META_LEFT,
                0,
                KeyEvent.META_ALT_ON);
        mMkh.onKeyEvent(upEvent, 0);

        // Pan ended.
        verify(mCallback, times(0)).onPanMagnificationStart(Display.DEFAULT_DISPLAY,
                PAN_DIRECTION_DOWN);
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt());
        verify(mCallback, times(1)).onKeyboardInteractionStop();

    }

    private void testPanMagnification(int keyCode, int panDirection) {
        final KeyEvent downEvent = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyCode, 0,
                KeyEvent.META_META_ON | KeyEvent.META_ALT_ON);
@@ -206,19 +241,21 @@ public class MagnificationKeyHandlerTest {

        // Pan started.
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY, panDirection);
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt());

        Mockito.clearInvocations(mCallback);

        final KeyEvent upEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0,
                KeyEvent.META_META_ON | KeyEvent.META_ALT_ON);
        mMkh.onKeyEvent(upEvent, 0);

        // Pan ended.
        verify(mCallback, times(1)).onPanMagnificationStart(Display.DEFAULT_DISPLAY, panDirection);
        verify(mCallback, times(1)).onPanMagnificationStop(Display.DEFAULT_DISPLAY, panDirection);
        verify(mCallback, times(0)).onPanMagnificationStart(Display.DEFAULT_DISPLAY, panDirection);
        verify(mCallback, times(1)).onPanMagnificationStop(panDirection);

        // Scale callbacks were not called.
        verify(mCallback, times(0)).onScaleMagnificationStart(anyInt(), anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt());

        // The events were not passed on.
        verify(mNextHandler, times(0)).onKeyEvent(any(), anyInt());
@@ -232,25 +269,25 @@ public class MagnificationKeyHandlerTest {
        // Scale started.
        verify(mCallback, times(1)).onScaleMagnificationStart(Display.DEFAULT_DISPLAY,
                zoomDirection);
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onScaleMagnificationStop(anyInt());

        Mockito.clearInvocations(mCallback);

        final KeyEvent upEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0,
                KeyEvent.META_META_ON | KeyEvent.META_ALT_ON);
        mMkh.onKeyEvent(upEvent, 0);

        // Scale ended.
        verify(mCallback, times(1)).onScaleMagnificationStart(Display.DEFAULT_DISPLAY,
                zoomDirection);
        verify(mCallback, times(1)).onScaleMagnificationStop(Display.DEFAULT_DISPLAY,
        verify(mCallback, times(0)).onScaleMagnificationStart(Display.DEFAULT_DISPLAY,
                zoomDirection);
        verify(mCallback, times(1)).onScaleMagnificationStop(zoomDirection);

        // Pan callbacks were not called.
        verify(mCallback, times(0)).onPanMagnificationStart(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt(), anyInt());
        verify(mCallback, times(0)).onPanMagnificationStop(anyInt());

        // The events were not passed on.
        verify(mNextHandler, times(0)).onKeyEvent(any(), anyInt());

    }

}