Merge "[A11y] Adds ability for magnifier to follow mouse and stylus" into main
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 099cb28..d16a665 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -45,9 +45,11 @@
 
 import com.android.server.LocalServices;
 import com.android.server.accessibility.gestures.TouchExplorer;
+import com.android.server.accessibility.magnification.FullScreenMagnificationController;
 import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler;
 import com.android.server.accessibility.magnification.FullScreenMagnificationVibrationHelper;
 import com.android.server.accessibility.magnification.MagnificationGestureHandler;
+import com.android.server.accessibility.magnification.MouseEventHandler;
 import com.android.server.accessibility.magnification.WindowMagnificationGestureHandler;
 import com.android.server.accessibility.magnification.WindowMagnificationPromptController;
 import com.android.server.policy.WindowManagerPolicy;
@@ -864,15 +866,21 @@
                     TYPE_MAGNIFICATION_OVERLAY, null /* options */);
             FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper =
                     new FullScreenMagnificationVibrationHelper(uiContext);
-            magnificationGestureHandler = new FullScreenMagnificationGestureHandler(uiContext,
-                    mAms.getMagnificationController().getFullScreenMagnificationController(),
-                    mAms.getTraceManager(),
-                    mAms.getMagnificationController(),
-                    detectControlGestures,
-                    detectTwoFingerTripleTap,
-                    triggerable,
-                    new WindowMagnificationPromptController(displayContext, mUserId), displayId,
-                    fullScreenMagnificationVibrationHelper);
+            FullScreenMagnificationController controller =
+                    mAms.getMagnificationController().getFullScreenMagnificationController();
+            magnificationGestureHandler =
+                    new FullScreenMagnificationGestureHandler(
+                            uiContext,
+                            controller,
+                            mAms.getTraceManager(),
+                            mAms.getMagnificationController(),
+                            detectControlGestures,
+                            detectTwoFingerTripleTap,
+                            triggerable,
+                            new WindowMagnificationPromptController(displayContext, mUserId),
+                            displayId,
+                            fullScreenMagnificationVibrationHelper,
+                            new MouseEventHandler(controller));
         }
         return magnificationGestureHandler;
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index 159022b..b052d23 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -16,6 +16,8 @@
 
 package com.android.server.accessibility.magnification;
 
+import static android.view.InputDevice.SOURCE_MOUSE;
+import static android.view.InputDevice.SOURCE_STYLUS;
 import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
@@ -183,7 +185,10 @@
     private final int mMinimumVelocity;
     private final int mMaximumVelocity;
 
-    public FullScreenMagnificationGestureHandler(@UiContext Context context,
+    private MouseEventHandler mMouseEventHandler;
+
+    public FullScreenMagnificationGestureHandler(
+            @UiContext Context context,
             FullScreenMagnificationController fullScreenMagnificationController,
             AccessibilityTraceManager trace,
             Callback callback,
@@ -192,7 +197,8 @@
             boolean detectShortcutTrigger,
             @NonNull WindowMagnificationPromptController promptController,
             int displayId,
-            FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) {
+            FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper,
+            MouseEventHandler mouseEventHandler) {
         this(
                 context,
                 fullScreenMagnificationController,
@@ -207,9 +213,8 @@
                 /* magnificationLogger= */ null,
                 ViewConfiguration.get(context),
                 new OneFingerPanningSettingsProvider(
-                        context,
-                        Flags.enableMagnificationOneFingerPanningGesture()
-                ));
+                        context, Flags.enableMagnificationOneFingerPanningGesture()),
+                mouseEventHandler);
     }
 
     /** Constructor for tests. */
@@ -227,8 +232,8 @@
             FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper,
             MagnificationLogger magnificationLogger,
             ViewConfiguration viewConfiguration,
-            OneFingerPanningSettingsProvider oneFingerPanningSettingsProvider
-    ) {
+            OneFingerPanningSettingsProvider oneFingerPanningSettingsProvider,
+            MouseEventHandler mouseEventHandler) {
         super(displayId, detectSingleFingerTripleTap, detectTwoFingerTripleTap,
                 detectShortcutTrigger, trace, callback);
         if (DEBUG_ALL) {
@@ -318,6 +323,7 @@
         mOverscrollEdgeSlop = context.getResources().getDimensionPixelSize(
                 R.dimen.accessibility_fullscreen_magnification_gesture_edge_slop);
         mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+        mMouseEventHandler = mouseEventHandler;
 
         if (mDetectShortcutTrigger) {
             mScreenStateReceiver = new ScreenStateReceiver(context, this);
@@ -331,15 +337,28 @@
 
     @Override
     void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
-        if (event.getActionMasked() == ACTION_DOWN) {
-            cancelFling();
-        }
+        if (event.getSource() == SOURCE_TOUCHSCREEN) {
+            if (event.getActionMasked() == ACTION_DOWN) {
+                cancelFling();
+            }
+            handleTouchEventWith(mCurrentState, event, rawEvent, policyFlags);
+        } else if (Flags.enableMagnificationFollowsMouse()
+                && (event.getSource() == SOURCE_MOUSE || event.getSource() == SOURCE_STYLUS)) {
+            if (mFullScreenMagnificationController.isActivated(mDisplayId)) {
+                // TODO(b/354696546): Allow mouse/stylus to activate whichever display they are
+                // over, rather than only interacting with the current display.
 
-        handleEventWith(mCurrentState, event, rawEvent, policyFlags);
+                // Send through the mouse/stylus event handler.
+                mMouseEventHandler.onEvent(event, mDisplayId);
+            }
+            // Dispatch to normal event handling flow.
+            dispatchTransformedEvent(event, rawEvent, policyFlags);
+        }
     }
 
-    private void handleEventWith(State stateHandler,
-            MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+    private void handleTouchEventWith(
+            State stateHandler, MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+
         // To keep InputEventConsistencyVerifiers within GestureDetectors happy
         mPanningScalingState.mScrollGestureDetector.onTouchEvent(event);
         mPanningScalingState.mScaleGestureDetector.onTouchEvent(event);
@@ -1421,6 +1440,11 @@
 
         protected void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
                 int policyFlags) {
+            if (Flags.enableMagnificationFollowsMouse()
+                    && !event.isFromSource(SOURCE_TOUCHSCREEN)) {
+                // Only touch events need to be cached and sent later.
+                return;
+            }
             if (event.getActionMasked() == ACTION_DOWN) {
                 mPreLastDown = mLastDown;
                 mLastDown = MotionEvent.obtain(event);
@@ -1458,7 +1482,7 @@
                 mDelayedEventQueue = info.mNext;
 
                 info.event.setDownTime(info.event.getDownTime() + offset);
-                handleEventWith(mDelegatingState, info.event, info.rawEvent, info.policyFlags);
+                handleTouchEventWith(mDelegatingState, info.event, info.rawEvent, info.policyFlags);
 
                 info.recycle();
             } while (mDelayedEventQueue != null);
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java
index 11d0713..08411c2 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java
@@ -16,6 +16,8 @@
 
 package com.android.server.accessibility.magnification;
 
+import static android.view.InputDevice.SOURCE_MOUSE;
+import static android.view.InputDevice.SOURCE_STYLUS;
 import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_UP;
@@ -139,12 +141,35 @@
         }
     }
 
+    /**
+     * Some touchscreen, mouse and stylus events may modify magnifier state. Checks for whether the
+     * event should not be dispatched to the magnifier.
+     *
+     * @param event The event to check.
+     * @return `true` if the event should be sent through the normal event flow or `false` if it
+     *     should be observed by magnifier.
+     */
     private boolean shouldDispatchTransformedEvent(MotionEvent event) {
-        if ((!mDetectSingleFingerTripleTap && !mDetectTwoFingerTripleTap && !mDetectShortcutTrigger)
-                || !event.isFromSource(SOURCE_TOUCHSCREEN)) {
-            return true;
+        if (event.getSource() == SOURCE_TOUCHSCREEN) {
+            if (mDetectSingleFingerTripleTap
+                    || mDetectTwoFingerTripleTap
+                    || mDetectShortcutTrigger) {
+                // Observe touchscreen events while magnification activation is detected.
+                return false;
+            }
         }
-        return false;
+        if (Flags.enableMagnificationFollowsMouse()) {
+            if (event.isFromSource(SOURCE_MOUSE) || event.isFromSource(SOURCE_STYLUS)) {
+                // Note that mouse events include other mouse-like pointing devices
+                // such as touchpads and pointing sticks.
+                // Observe any mouse or stylus movement.
+                // We observe all movement to ensure that events continue to come in order,
+                // even though only some movement types actually move the viewport.
+                return false;
+            }
+        }
+        // Magnification dispatches (ignores) all other events
+        return true;
     }
 
     final void dispatchTransformedEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MouseEventHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/MouseEventHandler.java
new file mode 100644
index 0000000..845249e
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MouseEventHandler.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 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 android.view.InputDevice.SOURCE_MOUSE;
+import static android.view.MotionEvent.ACTION_HOVER_MOVE;
+import static android.view.MotionEvent.ACTION_MOVE;
+
+import android.view.MotionEvent;
+
+import com.android.server.accessibility.AccessibilityManagerService;
+
+/** MouseEventHandler handles mouse and stylus events that should move the viewport. */
+public final class MouseEventHandler {
+    private final FullScreenMagnificationController mFullScreenMagnificationController;
+
+    public MouseEventHandler(FullScreenMagnificationController fullScreenMagnificationController) {
+        mFullScreenMagnificationController = fullScreenMagnificationController;
+    }
+
+    /**
+     * Handles a mouse or stylus event, moving the magnifier if needed.
+     *
+     * @param event The mouse or stylus MotionEvent to consume
+     * @param displayId The display that is being magnified
+     */
+    public void onEvent(MotionEvent event, int displayId) {
+        if (event.getAction() == ACTION_HOVER_MOVE
+                || (event.getAction() == ACTION_MOVE && event.getSource() == SOURCE_MOUSE)) {
+            final float eventX = event.getX();
+            final float eventY = event.getY();
+
+            // Only move the viewport when over a magnified region.
+            // TODO(b/354696546): Ensure this doesn't stop the viewport from reaching the
+            // corners and edges at high levels of magnification.
+            if (mFullScreenMagnificationController.magnificationRegionContains(
+                    displayId, eventX, eventY)) {
+                mFullScreenMagnificationController.setCenter(
+                        displayId,
+                        eventX,
+                        eventY,
+                        /* animate= */ false,
+                        AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
+            }
+        }
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
index 6f1141f..1818cdd 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
@@ -148,6 +148,10 @@
 
     @Override
     void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+        if (event.getSource() != SOURCE_TOUCHSCREEN) {
+            // Window Magnification viewport doesn't move with mouse events (yet).
+            return;
+        }
         // To keep InputEventConsistencyVerifiers within GestureDetectors happy.
         mObservePanningScalingState.mPanningScalingHandler.onTouchEvent(event);
         mCurrentState.onMotionEvent(event, rawEvent, policyFlags);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 60bcecc..957ee06 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -17,6 +17,7 @@
 package com.android.server.accessibility.magnification;
 
 import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_HOVER_MOVE;
 import static android.view.MotionEvent.ACTION_MOVE;
 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
 import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT;
@@ -27,6 +28,8 @@
 
 import static com.android.server.testutils.TestUtils.strictMock;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -300,7 +303,8 @@
                         mMockFullScreenMagnificationVibrationHelper,
                         mMockMagnificationLogger,
                         ViewConfiguration.get(mContext),
-                        mMockOneFingerPanningSettingsProvider);
+                        mMockOneFingerPanningSettingsProvider,
+                        new MouseEventHandler(mFullScreenMagnificationController));
         // OverscrollHandler is only supported on watches.
         // @See config_enable_a11y_fullscreen_magnification_overscroll_handler
         if (isWatch()) {
@@ -1398,6 +1402,302 @@
         mFullScreenMagnificationController.reset(DISPLAY_0, /* animate= */ false);
     }
 
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testMouseMoveEventsDoNotMoveMagnifierViewport() {
+        runMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE);
+    }
+
+    @Test
+    public void testStylusMoveEventsDoNotMoveMagnifierViewport() {
+        runMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_STYLUS);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testMouseHoverMoveEventsDoNotMoveMagnifierViewport() {
+        runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testStylusHoverMoveEventsDoNotMoveMagnifierViewport() {
+        runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_STYLUS);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testMouseHoverMoveEventsMoveMagnifierViewport() {
+        runHoverMovesViewportTest(InputDevice.SOURCE_MOUSE);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testStylusHoverMoveEventsMoveMagnifierViewport() {
+        runHoverMovesViewportTest(InputDevice.SOURCE_STYLUS);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testMouseDownEventsDoNotMoveMagnifierViewport() {
+        runDownDoesNotMoveViewportTest(InputDevice.SOURCE_MOUSE);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testStylusDownEventsDoNotMoveMagnifierViewport() {
+        runDownDoesNotMoveViewportTest(InputDevice.SOURCE_STYLUS);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testMouseUpEventsDoNotMoveMagnifierViewport() {
+        runUpDoesNotMoveViewportTest(InputDevice.SOURCE_MOUSE);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testStylusUpEventsDoNotMoveMagnifierViewport() {
+        runUpDoesNotMoveViewportTest(InputDevice.SOURCE_STYLUS);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+    public void testMouseMoveEventsMoveMagnifierViewport() {
+        final EventCaptor eventCaptor = new EventCaptor();
+        mMgh.setNext(eventCaptor);
+
+        float centerX =
+                (INITIAL_MAGNIFICATION_BOUNDS.left + INITIAL_MAGNIFICATION_BOUNDS.width()) / 2.0f;
+        float centerY =
+                (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.height()) / 2.0f;
+        float scale = 6.2f; // value is unimportant but unique among tests to increase coverage.
+        mFullScreenMagnificationController.setScaleAndCenter(
+                DISPLAY_0, centerX, centerY, scale, /* animate= */ false, 1);
+        MotionEvent event = mouseEvent(centerX, centerY, ACTION_HOVER_MOVE);
+        send(event, InputDevice.SOURCE_MOUSE);
+        fastForward(20);
+        event = mouseEvent(centerX, centerY, ACTION_DOWN);
+        send(event, InputDevice.SOURCE_MOUSE);
+        fastForward(20);
+
+        // Mouse drag event does impact magnifier viewport.
+        event = mouseEvent(centerX + 30, centerY + 60, ACTION_MOVE);
+        send(event, InputDevice.SOURCE_MOUSE);
+        fastForward(20);
+
+        assertThat(mFullScreenMagnificationController.getCenterX(DISPLAY_0))
+                .isEqualTo(centerX + 30);
+        assertThat(mFullScreenMagnificationController.getCenterY(DISPLAY_0))
+                .isEqualTo(centerY + 60);
+
+        // The mouse events were not consumed by magnifier.
+        assertThat(eventCaptor.mEvents.size()).isEqualTo(3);
+        assertThat(eventCaptor.mEvents.get(0).getSource()).isEqualTo(InputDevice.SOURCE_MOUSE);
+        assertThat(eventCaptor.mEvents.get(1).getSource()).isEqualTo(InputDevice.SOURCE_MOUSE);
+        assertThat(eventCaptor.mEvents.get(2).getSource()).isEqualTo(InputDevice.SOURCE_MOUSE);
+
+        final List<Integer> expectedActions = new ArrayList();
+        expectedActions.add(Integer.valueOf(ACTION_HOVER_MOVE));
+        expectedActions.add(Integer.valueOf(ACTION_DOWN));
+        expectedActions.add(Integer.valueOf(ACTION_MOVE));
+        assertActionsInOrder(eventCaptor.mEvents, expectedActions);
+    }
+
+    private void runHoverMovesViewportTest(int source) {
+        final EventCaptor eventCaptor = new EventCaptor();
+        mMgh.setNext(eventCaptor);
+
+        float centerX =
+                (INITIAL_MAGNIFICATION_BOUNDS.left + INITIAL_MAGNIFICATION_BOUNDS.width()) / 2.0f;
+        float centerY =
+                (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.height()) / 2.0f;
+        float scale = 4.0f; // value is unimportant but unique among tests to increase coverage.
+        mFullScreenMagnificationController.setScaleAndCenter(
+                DISPLAY_0, centerX, centerY, scale, /* animate= */ false, 1);
+
+        // HOVER_MOVE should change magnifier viewport.
+        MotionEvent event = motionEvent(centerX + 20, centerY, ACTION_HOVER_MOVE);
+        send(event, source);
+        fastForward(20);
+
+        assertThat(mFullScreenMagnificationController.getCenterX(DISPLAY_0))
+                .isEqualTo(centerX + 20);
+        assertThat(mFullScreenMagnificationController.getCenterY(DISPLAY_0)).isEqualTo(centerY);
+
+        // Make sure mouse events are sent onward and not blocked after moving the viewport.
+        assertThat(eventCaptor.mEvents.size()).isEqualTo(1);
+        assertThat(eventCaptor.mEvents.get(0).getSource()).isEqualTo(source);
+
+        // Send another hover.
+        event = motionEvent(centerX + 20, centerY + 40, ACTION_HOVER_MOVE);
+        send(event, source);
+        fastForward(20);
+
+        assertThat(mFullScreenMagnificationController.getCenterX(DISPLAY_0))
+                .isEqualTo(centerX + 20);
+        assertThat(mFullScreenMagnificationController.getCenterY(DISPLAY_0))
+                .isEqualTo(centerY + 40);
+
+        assertThat(eventCaptor.mEvents.size()).isEqualTo(2);
+        assertThat(eventCaptor.mEvents.get(1).getSource()).isEqualTo(source);
+
+        final List<Integer> expectedActions = new ArrayList();
+        expectedActions.add(Integer.valueOf(ACTION_HOVER_MOVE));
+        expectedActions.add(Integer.valueOf(ACTION_HOVER_MOVE));
+        assertActionsInOrder(eventCaptor.mEvents, expectedActions);
+    }
+
+    private void runDownDoesNotMoveViewportTest(int source) {
+        final EventCaptor eventCaptor = new EventCaptor();
+        mMgh.setNext(eventCaptor);
+
+        float centerX =
+                (INITIAL_MAGNIFICATION_BOUNDS.left + INITIAL_MAGNIFICATION_BOUNDS.width()) / 2.0f;
+        float centerY =
+                (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.height()) / 2.0f;
+        float scale = 5.3f; // value is unimportant but unique among tests to increase coverage.
+        mFullScreenMagnificationController.setScaleAndCenter(
+                DISPLAY_0, centerX, centerY, scale, /* animate= */ false, 1);
+        MotionEvent event = motionEvent(centerX, centerY, ACTION_HOVER_MOVE);
+        send(event, source);
+        fastForward(20);
+
+        // Down event doesn't impact magnifier viewport.
+        event = motionEvent(centerX + 20, centerY + 40, ACTION_DOWN);
+        send(event, source);
+        fastForward(20);
+
+        assertThat(mFullScreenMagnificationController.getCenterX(DISPLAY_0)).isEqualTo(centerX);
+        assertThat(mFullScreenMagnificationController.getCenterY(DISPLAY_0)).isEqualTo(centerY);
+
+        // The events were not consumed by magnifier.
+        assertThat(eventCaptor.mEvents.size()).isEqualTo(2);
+        assertThat(eventCaptor.mEvents.get(0).getSource()).isEqualTo(source);
+        assertThat(eventCaptor.mEvents.get(1).getSource()).isEqualTo(source);
+
+        final List<Integer> expectedActions = new ArrayList();
+        expectedActions.add(Integer.valueOf(ACTION_HOVER_MOVE));
+        expectedActions.add(Integer.valueOf(ACTION_DOWN));
+        assertActionsInOrder(eventCaptor.mEvents, expectedActions);
+    }
+
+    private void runUpDoesNotMoveViewportTest(int source) {
+        final EventCaptor eventCaptor = new EventCaptor();
+        mMgh.setNext(eventCaptor);
+
+        float centerX =
+                (INITIAL_MAGNIFICATION_BOUNDS.left + INITIAL_MAGNIFICATION_BOUNDS.width()) / 2.0f;
+        float centerY =
+                (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.height()) / 2.0f;
+        float scale = 2.7f; // value is unimportant but unique among tests to increase coverage.
+        mFullScreenMagnificationController.setScaleAndCenter(
+                DISPLAY_0, centerX, centerY, scale, /* animate= */ false, 1);
+        MotionEvent event = motionEvent(centerX, centerY, ACTION_HOVER_MOVE);
+        send(event, source);
+        fastForward(20);
+        event = motionEvent(centerX, centerY, ACTION_DOWN);
+        send(event, source);
+        fastForward(20);
+
+        // Up event should not move the viewport.
+        event = motionEvent(centerX + 30, centerY + 60, ACTION_UP);
+        send(event, source);
+        fastForward(20);
+
+        // The events were not consumed by magnifier.
+        assertThat(eventCaptor.mEvents.size()).isEqualTo(3);
+        assertThat(eventCaptor.mEvents.get(0).getSource()).isEqualTo(source);
+        assertThat(eventCaptor.mEvents.get(1).getSource()).isEqualTo(source);
+        assertThat(eventCaptor.mEvents.get(2).getSource()).isEqualTo(source);
+
+        final List<Integer> expectedActions = new ArrayList();
+        expectedActions.add(Integer.valueOf(ACTION_HOVER_MOVE));
+        expectedActions.add(Integer.valueOf(ACTION_DOWN));
+        expectedActions.add(Integer.valueOf(ACTION_UP));
+        assertActionsInOrder(eventCaptor.mEvents, expectedActions);
+    }
+
+    private void runMoveEventsDoNotMoveMagnifierViewport(int source) {
+        final EventCaptor eventCaptor = new EventCaptor();
+        mMgh.setNext(eventCaptor);
+
+        float centerX =
+                (INITIAL_MAGNIFICATION_BOUNDS.left + INITIAL_MAGNIFICATION_BOUNDS.width()) / 2.0f;
+        float centerY =
+                (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.height()) / 2.0f;
+        float scale = 3.8f; // value is unimportant but unique among tests to increase coverage.
+        mFullScreenMagnificationController.setScaleAndCenter(
+                DISPLAY_0, centerX, centerY, scale, /* animate= */ false, 1);
+        centerX = mFullScreenMagnificationController.getCenterX(DISPLAY_0);
+        centerY = mFullScreenMagnificationController.getCenterY(DISPLAY_0);
+
+        MotionEvent event = motionEvent(centerX, centerY, ACTION_DOWN);
+        send(event, source);
+        fastForward(20);
+
+        // Drag event doesn't impact magnifier viewport.
+        event = stylusEvent(centerX + 18, centerY + 42, ACTION_MOVE);
+        send(event, source);
+        fastForward(20);
+
+        assertThat(mFullScreenMagnificationController.getCenterX(DISPLAY_0)).isEqualTo(centerX);
+        assertThat(mFullScreenMagnificationController.getCenterY(DISPLAY_0)).isEqualTo(centerY);
+
+        // The events were not consumed by magnifier.
+        assertThat(eventCaptor.mEvents.size()).isEqualTo(2);
+        assertThat(eventCaptor.mEvents.get(0).getSource()).isEqualTo(source);
+        assertThat(eventCaptor.mEvents.get(1).getSource()).isEqualTo(source);
+
+        final List<Integer> expectedActions = new ArrayList();
+        expectedActions.add(Integer.valueOf(ACTION_DOWN));
+        expectedActions.add(Integer.valueOf(ACTION_MOVE));
+        assertActionsInOrder(eventCaptor.mEvents, expectedActions);
+    }
+
+    private void runHoverMoveEventsDoNotMoveMagnifierViewport(int source) {
+        final EventCaptor eventCaptor = new EventCaptor();
+        mMgh.setNext(eventCaptor);
+
+        float centerX =
+                (INITIAL_MAGNIFICATION_BOUNDS.left + INITIAL_MAGNIFICATION_BOUNDS.width()) / 2.0f;
+        float centerY =
+                (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.height()) / 2.0f;
+        float scale = 4.0f; // value is unimportant but unique among tests to increase coverage.
+        mFullScreenMagnificationController.setScaleAndCenter(
+                DISPLAY_0, centerX, centerY, scale, /* animate= */ false, 1);
+        centerX = mFullScreenMagnificationController.getCenterX(DISPLAY_0);
+        centerY = mFullScreenMagnificationController.getCenterY(DISPLAY_0);
+
+        // HOVER_MOVE should not change magnifier viewport.
+        MotionEvent event = motionEvent(centerX + 20, centerY, ACTION_HOVER_MOVE);
+        send(event, source);
+        fastForward(20);
+
+        assertThat(mFullScreenMagnificationController.getCenterX(DISPLAY_0)).isEqualTo(centerX);
+        assertThat(mFullScreenMagnificationController.getCenterY(DISPLAY_0)).isEqualTo(centerY);
+
+        // Make sure events are sent onward and not blocked after moving the viewport.
+        assertThat(eventCaptor.mEvents.size()).isEqualTo(1);
+        assertThat(eventCaptor.mEvents.get(0).getSource()).isEqualTo(source);
+
+        // Send another hover.
+        event = motionEvent(centerX + 20, centerY + 40, ACTION_HOVER_MOVE);
+        send(event, source);
+        fastForward(20);
+
+        assertThat(mFullScreenMagnificationController.getCenterX(DISPLAY_0)).isEqualTo(centerX);
+        assertThat(mFullScreenMagnificationController.getCenterY(DISPLAY_0)).isEqualTo(centerY);
+
+        assertThat(eventCaptor.mEvents.size()).isEqualTo(2);
+        assertThat(eventCaptor.mEvents.get(1).getSource()).isEqualTo(source);
+
+        final List<Integer> expectedActions = new ArrayList();
+        expectedActions.add(Integer.valueOf(ACTION_HOVER_MOVE));
+        expectedActions.add(Integer.valueOf(ACTION_HOVER_MOVE));
+        assertActionsInOrder(eventCaptor.mEvents, expectedActions);
+    }
+
     private void enableOneFingerPanning(boolean enable) {
         mMockOneFingerPanningEnabled = enable;
         when(mMockOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()).thenReturn(enable);
@@ -1795,8 +2095,14 @@
         mMgh.notifyShortcutTriggered();
     }
 
+    /** Sends the MotionEvent from a Touchscreen source */
     private void send(MotionEvent event) {
-        event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+        send(event, InputDevice.SOURCE_TOUCHSCREEN);
+    }
+
+    /** Sends the MotionEvent from the given source type. */
+    private void send(MotionEvent event, int source) {
+        event.setSource(source);
         try {
             mMgh.onMotionEvent(event, event, /* policyFlags */ 0);
         } catch (Throwable t) {
@@ -1810,9 +2116,30 @@
         return ev;
     }
 
+    private static MotionEvent fromMouse(MotionEvent ev) {
+        ev.setSource(InputDevice.SOURCE_MOUSE);
+        return ev;
+    }
+
+    private static MotionEvent fromStylus(MotionEvent ev) {
+        ev.setSource(InputDevice.SOURCE_STYLUS);
+        return ev;
+    }
+
+    private MotionEvent motionEvent(float x, float y, int action) {
+        return MotionEvent.obtain(mLastDownTime, mClock.now(), action, x, y, 0);
+    }
+
+    private MotionEvent mouseEvent(float x, float y, int action) {
+        return fromMouse(motionEvent(x, y, action));
+    }
+
+    private MotionEvent stylusEvent(float x, float y, int action) {
+        return fromStylus(motionEvent(x, y, action));
+    }
+
     private MotionEvent moveEvent(float x, float y) {
-        return fromTouchscreen(
-                MotionEvent.obtain(mLastDownTime, mClock.now(), ACTION_MOVE, x, y, 0));
+        return fromTouchscreen(motionEvent(x, y, ACTION_MOVE));
     }
 
     private MotionEvent downEvent() {