Merge "Add DREAMING -> GONE when dismissible; fix WmLsVis issue with that case and others." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 97d28d1..2f843f9 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -345,7 +345,7 @@
     name: "android.nfc.flags-aconfig",
     package: "android.nfc",
     container: "system",
-    srcs: ["nfc/java/android/nfc/*.aconfig"],
+    srcs: ["nfc-non-updatable/flags/*.aconfig"],
 }
 
 cc_aconfig_library {
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 47ef461..9f8505f 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -125,7 +125,10 @@
     public static final int KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK = 75;
     public static final int KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW = 76;
     public static final int KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB = 77;
-
+    public static final int KEY_GESTURE_TYPE_MAGNIFIER_PAN_LEFT = 78;
+    public static final int KEY_GESTURE_TYPE_MAGNIFIER_PAN_RIGHT = 79;
+    public static final int KEY_GESTURE_TYPE_MAGNIFIER_PAN_UP = 80;
+    public static final int KEY_GESTURE_TYPE_MAGNIFIER_PAN_DOWN = 81;
 
     public static final int FLAG_CANCELLED = 1;
 
@@ -217,7 +220,11 @@
             KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
             KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK,
             KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
-            KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB
+            KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB,
+            KEY_GESTURE_TYPE_MAGNIFIER_PAN_LEFT,
+            KEY_GESTURE_TYPE_MAGNIFIER_PAN_RIGHT,
+            KEY_GESTURE_TYPE_MAGNIFIER_PAN_UP,
+            KEY_GESTURE_TYPE_MAGNIFIER_PAN_DOWN,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface KeyGestureType {
@@ -792,6 +799,14 @@
                 return "KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW";
             case KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB:
                 return "KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB";
+            case KEY_GESTURE_TYPE_MAGNIFIER_PAN_LEFT:
+                return "KEY_GESTURE_TYPE_MAGNIFIER_PAN_LEFT";
+            case KEY_GESTURE_TYPE_MAGNIFIER_PAN_RIGHT:
+                return "KEY_GESTURE_TYPE_MAGNIFIER_PAN_RIGHT";
+            case KEY_GESTURE_TYPE_MAGNIFIER_PAN_UP:
+                return "KEY_GESTURE_TYPE_MAGNIFIER_PAN_UP";
+            case KEY_GESTURE_TYPE_MAGNIFIER_PAN_DOWN:
+                return "KEY_GESTURE_TYPE_MAGNIFIER_PAN_DOWN";
             default:
                 return Integer.toHexString(value);
         }
diff --git a/nfc-non-updatable/Android.bp b/nfc-non-updatable/Android.bp
new file mode 100644
index 0000000..ff987bb
--- /dev/null
+++ b/nfc-non-updatable/Android.bp
@@ -0,0 +1,23 @@
+package {
+    default_team: "trendy_team_fwk_nfc",
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "framework-nfc-non-updatable-sources",
+    path: "java",
+    srcs: [
+        "java/android/nfc/NfcServiceManager.java",
+        "java/android/nfc/cardemulation/ApduServiceInfo.aidl",
+        "java/android/nfc/cardemulation/ApduServiceInfo.java",
+        "java/android/nfc/cardemulation/NfcFServiceInfo.aidl",
+        "java/android/nfc/cardemulation/NfcFServiceInfo.java",
+        "java/android/nfc/cardemulation/AidGroup.aidl",
+        "java/android/nfc/cardemulation/AidGroup.java",
+    ],
+}
diff --git a/nfc-non-updatable/OWNERS b/nfc-non-updatable/OWNERS
new file mode 100644
index 0000000..f46dccd
--- /dev/null
+++ b/nfc-non-updatable/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 48448
+include platform/packages/apps/Nfc:/OWNERS
\ No newline at end of file
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc-non-updatable/flags/flags.aconfig
similarity index 100%
rename from nfc/java/android/nfc/flags.aconfig
rename to nfc-non-updatable/flags/flags.aconfig
diff --git a/nfc/java/android/nfc/NfcServiceManager.java b/nfc-non-updatable/java/android/nfc/NfcServiceManager.java
similarity index 100%
rename from nfc/java/android/nfc/NfcServiceManager.java
rename to nfc-non-updatable/java/android/nfc/NfcServiceManager.java
diff --git a/nfc/java/android/nfc/cardemulation/AidGroup.aidl b/nfc-non-updatable/java/android/nfc/cardemulation/AidGroup.aidl
similarity index 100%
rename from nfc/java/android/nfc/cardemulation/AidGroup.aidl
rename to nfc-non-updatable/java/android/nfc/cardemulation/AidGroup.aidl
diff --git a/nfc/java/android/nfc/cardemulation/AidGroup.java b/nfc-non-updatable/java/android/nfc/cardemulation/AidGroup.java
similarity index 100%
rename from nfc/java/android/nfc/cardemulation/AidGroup.java
rename to nfc-non-updatable/java/android/nfc/cardemulation/AidGroup.java
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.aidl b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.aidl
similarity index 100%
rename from nfc/java/android/nfc/cardemulation/ApduServiceInfo.aidl
rename to nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.aidl
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java
similarity index 100%
rename from nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
rename to nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java
diff --git a/nfc/java/android/nfc/cardemulation/NfcFServiceInfo.aidl b/nfc-non-updatable/java/android/nfc/cardemulation/NfcFServiceInfo.aidl
similarity index 100%
rename from nfc/java/android/nfc/cardemulation/NfcFServiceInfo.aidl
rename to nfc-non-updatable/java/android/nfc/cardemulation/NfcFServiceInfo.aidl
diff --git a/nfc/java/android/nfc/cardemulation/NfcFServiceInfo.java b/nfc-non-updatable/java/android/nfc/cardemulation/NfcFServiceInfo.java
similarity index 100%
rename from nfc/java/android/nfc/cardemulation/NfcFServiceInfo.java
rename to nfc-non-updatable/java/android/nfc/cardemulation/NfcFServiceInfo.java
diff --git a/nfc/Android.bp b/nfc/Android.bp
index 9490487..0fdb3bd 100644
--- a/nfc/Android.bp
+++ b/nfc/Android.bp
@@ -9,20 +9,6 @@
 }
 
 filegroup {
-    name: "framework-nfc-non-updatable-sources",
-    path: "java",
-    srcs: [
-        "java/android/nfc/NfcServiceManager.java",
-        "java/android/nfc/cardemulation/ApduServiceInfo.aidl",
-        "java/android/nfc/cardemulation/ApduServiceInfo.java",
-        "java/android/nfc/cardemulation/NfcFServiceInfo.aidl",
-        "java/android/nfc/cardemulation/NfcFServiceInfo.java",
-        "java/android/nfc/cardemulation/AidGroup.aidl",
-        "java/android/nfc/cardemulation/AidGroup.java",
-    ],
-}
-
-filegroup {
     name: "framework-nfc-updatable-sources",
     path: "java",
     srcs: [
@@ -34,9 +20,6 @@
         "//packages/apps/Nfc:__subpackages__",
         "//packages/modules/Nfc:__subpackages__",
     ],
-    exclude_srcs: [
-        ":framework-nfc-non-updatable-sources",
-    ],
 }
 
 java_sdk_library {
diff --git a/nfc/tests/Android.bp b/nfc/tests/Android.bp
index b6090e8..17fb810 100644
--- a/nfc/tests/Android.bp
+++ b/nfc/tests/Android.bp
@@ -29,7 +29,6 @@
         "androidx.test.rules",
         "androidx.test.runner",
         "androidx.test.ext.junit",
-        "framework-nfc.impl",
         "mockito-target-extended-minus-junit4",
         "frameworks-base-testutils",
         "truth",
@@ -40,16 +39,25 @@
         "testables",
     ],
     libs: [
+        "androidx.annotation_annotation",
+        "unsupportedappusage", // for android.compat.annotation.UnsupportedAppUsage
+        "framework-permission-s.stubs.module_lib",
+        "framework-permission.stubs.module_lib",
         "android.test.base.stubs.system",
         "android.test.mock.stubs.system",
         "android.test.runner.stubs.system",
+        "framework-nfc.impl",
     ],
     jni_libs: [
         // Required for ExtendedMockito
         "libdexmakerjvmtiagent",
         "libstaticjvmtiagent",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        ":framework-nfc-updatable-sources",
+        ":framework-nfc-non-updatable-sources",
+    ],
     platform_apis: true,
     certificate: "platform",
     test_suites: [
diff --git a/packages/SettingsLib/tests/robotests/Android.bp b/packages/SettingsLib/tests/robotests/Android.bp
index f380e7f..81358ca 100644
--- a/packages/SettingsLib/tests/robotests/Android.bp
+++ b/packages/SettingsLib/tests/robotests/Android.bp
@@ -76,7 +76,6 @@
     tools: ["soong_zip"],
     cmd: "mkdir -p $(genDir)/META-INF/services/ && touch $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider &&" +
         "echo -e 'org.robolectric.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
-        "echo -e 'org.robolectric.shadows.multidex.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
         "echo -e 'org.robolectric.shadows.httpclient.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
         //"echo -e 'com.android.settings.testutils.shadow.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
         "echo -e 'com.android.settingslib.testutils.shadow.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index e1b6c9c..5133575 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -245,22 +245,45 @@
                     final boolean complete =
                             event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
                                     && !event.isCancelled();
+
+                    // TODO(b/355499907): Receive and handle held key gestures, which can be used
+                    // for continuous scaling and panning. In addition, handle multiple pan gestures
+                    // at the same time (e.g. user may try to pan diagonally) reasonably, including
+                    // decreasing diagonal movement by sqrt(2) to make it appear the same speed
+                    // as non-diagonal movement.
+
+                    if (!complete) {
+                        return false;
+                    }
+
                     final int gestureType = event.getKeyGestureType();
                     final int displayId = isDisplayIdValid(event.getDisplayId())
                             ? event.getDisplayId() : Display.DEFAULT_DISPLAY;
 
                     switch (gestureType) {
                         case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN:
-                            if (complete) {
                                 mAms.getMagnificationController().scaleMagnificationByStep(
                                         displayId, MagnificationController.ZOOM_DIRECTION_IN);
-                            }
                             return true;
                         case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT:
-                            if (complete) {
                                 mAms.getMagnificationController().scaleMagnificationByStep(
                                         displayId, MagnificationController.ZOOM_DIRECTION_OUT);
-                            }
+                            return true;
+                        case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_LEFT:
+                            mAms.getMagnificationController().panMagnificationByStep(
+                                    displayId, MagnificationController.PAN_DIRECTION_LEFT);
+                            return true;
+                        case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_RIGHT:
+                            mAms.getMagnificationController().panMagnificationByStep(
+                                    displayId, MagnificationController.PAN_DIRECTION_RIGHT);
+                            return true;
+                        case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_UP:
+                            mAms.getMagnificationController().panMagnificationByStep(
+                                    displayId, MagnificationController.PAN_DIRECTION_UP);
+                            return true;
+                        case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_DOWN:
+                            mAms.getMagnificationController().panMagnificationByStep(
+                                    displayId, MagnificationController.PAN_DIRECTION_DOWN);
                             return true;
                     }
                     return false;
@@ -270,7 +293,11 @@
                 public boolean isKeyGestureSupported(int gestureType) {
                     return switch (gestureType) {
                         case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN,
-                             KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT -> true;
+                             KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT,
+                             KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_LEFT,
+                             KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_RIGHT,
+                             KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_UP,
+                             KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_DOWN -> true;
                         default -> false;
                     };
                 }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index 058b2be..2e131b6 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -35,14 +35,19 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.hardware.display.DisplayManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.DisplayMetrics;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
+import android.util.SparseDoubleArray;
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
+import android.util.TypedValue;
+import android.view.Display;
 import android.view.accessibility.MagnificationAnimationCallback;
 
 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
@@ -102,6 +107,7 @@
     /** Whether the platform supports window magnification feature. */
     private final boolean mSupportWindowMagnification;
     private final MagnificationScaleStepProvider mScaleStepProvider;
+    private final MagnificationPanStepProvider mPanStepProvider;
 
     private final Executor mBackgroundExecutor;
 
@@ -132,7 +138,7 @@
             .UiChangesForAccessibilityCallbacks> mAccessibilityCallbacksDelegateArray =
             new SparseArray<>();
 
-    // Direction magnifier scale can be altered.
+    // Direction magnification scale can be altered.
     public static final int ZOOM_DIRECTION_IN = 0;
     public static final int ZOOM_DIRECTION_OUT = 1;
 
@@ -140,6 +146,16 @@
     public @interface ZoomDirection {
     }
 
+    // Directions magnification center can be moved.
+    public static final int PAN_DIRECTION_LEFT = 0;
+    public static final int PAN_DIRECTION_RIGHT = 1;
+    public static final int PAN_DIRECTION_UP = 2;
+    public static final int PAN_DIRECTION_DOWN = 3;
+
+    @IntDef({PAN_DIRECTION_LEFT, PAN_DIRECTION_RIGHT, PAN_DIRECTION_UP, PAN_DIRECTION_DOWN})
+    public @interface PanDirection {
+    }
+
     /**
      * A callback to inform the magnification transition result on the given display.
      */
@@ -188,6 +204,87 @@
         }
     }
 
+    /**
+     * An interface to configure how much the magnification center should be affected when panning
+     * in steps.
+     */
+    public interface MagnificationPanStepProvider {
+        /**
+         * Calculate the next value based on the current scale.
+         *
+         * @param currentScale The current magnification scale value.
+         * @param displayId The displayId for the display being magnified.
+         * @return The next pan step value.
+         */
+        float nextPanStep(float currentScale, int displayId);
+    }
+
+    public static class DefaultMagnificationPanStepProvider implements
+            MagnificationPanStepProvider, DisplayManager.DisplayListener {
+        // We want panning to be 40 dip per keystroke at scale 2, and 1 dip per keystroke at scale
+        // 20. This can be defined using y = mx + b to get the slope and intercept.
+        // This works even if the device does not allow magnification up to 20x; we will still get
+        // a reasonable lineary ramp of panning movement for each scale step.
+        private static final float DEFAULT_SCALE = 2.0f;
+        private static final float PAN_STEP_AT_DEFAULT_SCALE_DIP = 40.0f;
+        private static final float SCALE_FOR_1_DIP_PAN = 20.0f;
+
+        private SparseDoubleArray mPanStepSlopes;
+        private SparseDoubleArray mPanStepIntercepts;
+
+        private final DisplayManager mDisplayManager;
+
+        DefaultMagnificationPanStepProvider(Context context) {
+            mDisplayManager = context.getSystemService(DisplayManager.class);
+            mDisplayManager.registerDisplayListener(this, /*handler=*/null);
+            mPanStepSlopes = new SparseDoubleArray();
+            mPanStepIntercepts = new SparseDoubleArray();
+        }
+
+        @Override
+        public void onDisplayAdded(int displayId) {
+            updateForDisplay(displayId);
+        }
+
+        @Override
+        public void onDisplayChanged(int displayId) {
+            updateForDisplay(displayId);
+        }
+
+        @Override
+        public void onDisplayRemoved(int displayId) {
+            mPanStepSlopes.delete(displayId);
+            mPanStepIntercepts.delete(displayId);
+        }
+
+        @Override
+        public float nextPanStep(float currentScale, int displayId) {
+            if (mPanStepSlopes.indexOfKey(displayId) < 0) {
+                updateForDisplay(displayId);
+            }
+            return Math.max((float) (mPanStepSlopes.get(displayId) * currentScale
+                    + mPanStepIntercepts.get(displayId)), 1);
+        }
+
+        private void updateForDisplay(int displayId) {
+            Display display = mDisplayManager.getDisplay(displayId);
+            if (display == null) {
+                return;
+            }
+            DisplayMetrics metrics = new DisplayMetrics();
+            display.getMetrics(metrics);
+            final float panStepAtDefaultScaleInPx = TypedValue.convertDimensionToPixels(
+                    TypedValue.COMPLEX_UNIT_DIP, PAN_STEP_AT_DEFAULT_SCALE_DIP, metrics);
+            final float panStepAtMaxScaleInPx = TypedValue.convertDimensionToPixels(
+                    TypedValue.COMPLEX_UNIT_DIP, 1.0f, metrics);
+            final float panStepSlope = (panStepAtMaxScaleInPx - panStepAtDefaultScaleInPx)
+                    / (SCALE_FOR_1_DIP_PAN - DEFAULT_SCALE);
+            mPanStepSlopes.put(displayId, panStepSlope);
+            mPanStepIntercepts.put(displayId,
+                    panStepAtDefaultScaleInPx - panStepSlope * DEFAULT_SCALE);
+        }
+    }
+
     public MagnificationController(AccessibilityManagerService ams, Object lock,
             Context context, MagnificationScaleProvider scaleProvider,
             Executor backgroundExecutor) {
@@ -201,6 +298,7 @@
         mSupportWindowMagnification = context.getPackageManager().hasSystemFeature(
                 FEATURE_WINDOW_MAGNIFICATION);
         mScaleStepProvider = new DefaultMagnificationScaleStepProvider();
+        mPanStepProvider = new DefaultMagnificationPanStepProvider(mContext);
 
         mAlwaysOnMagnificationFeatureFlag = new AlwaysOnMagnificationFeatureFlag(context);
         mAlwaysOnMagnificationFeatureFlag.addOnChangedListener(
@@ -935,13 +1033,12 @@
     }
 
     /**
-     * Scales the magnifier on the given display one step in/out based on the zoomIn param.
+     * Scales the magnifier on the given display one step in/out based on the direction param.
      *
      * @param displayId The logical display id.
      * @param direction Whether the scale should be zoomed in or out.
-     * @return {@code true} if the magnification scale was affected.
      */
-    public boolean scaleMagnificationByStep(int displayId, @ZoomDirection int direction) {
+    public void scaleMagnificationByStep(int displayId, @ZoomDirection int direction) {
         if (getFullScreenMagnificationController().isActivated(displayId)) {
             final float magnificationScale = getFullScreenMagnificationController().getScale(
                     displayId);
@@ -950,7 +1047,6 @@
             getFullScreenMagnificationController().setScaleAndCenter(displayId,
                     nextMagnificationScale,
                     Float.NaN, Float.NaN, true, MAGNIFICATION_GESTURE_HANDLER_ID);
-            return nextMagnificationScale != magnificationScale;
         }
 
         if (getMagnificationConnectionManager().isWindowMagnifierEnabled(displayId)) {
@@ -959,10 +1055,51 @@
             final float nextMagnificationScale = mScaleStepProvider.nextScaleStep(
                     magnificationScale, direction);
             getMagnificationConnectionManager().setScale(displayId, nextMagnificationScale);
-            return nextMagnificationScale != magnificationScale;
+        }
+    }
+
+    /**
+     * Pans the magnifier on the given display one step left/right/up/down based on the direction
+     * param.
+     *
+     * @param displayId The logical display id.
+     * @param direction Whether the direction should be left/right/up/down.
+     */
+    public void panMagnificationByStep(int displayId, @PanDirection int direction) {
+        final boolean fullscreenActivated =
+                getFullScreenMagnificationController().isActivated(displayId);
+        final boolean windowActivated =
+                getMagnificationConnectionManager().isWindowMagnifierEnabled(displayId);
+        if (!fullscreenActivated && !windowActivated) {
+            return;
         }
 
-        return false;
+        final float scale = fullscreenActivated
+                ? getFullScreenMagnificationController().getScale(displayId)
+                        : getMagnificationConnectionManager().getScale(displayId);
+        final float step = mPanStepProvider.nextPanStep(scale, displayId);
+
+        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 (fullscreenActivated) {
+            final float centerX = getFullScreenMagnificationController().getCenterX(displayId);
+            final float centerY = getFullScreenMagnificationController().getCenterY(displayId);
+            getFullScreenMagnificationController().setScaleAndCenter(displayId, scale,
+                    centerX + offsetX, centerY + offsetY, true, MAGNIFICATION_GESTURE_HANDLER_ID);
+        } else {
+            getMagnificationConnectionManager().moveWindowMagnification(displayId, offsetX,
+                    offsetY);
+        }
     }
 
     private final class DisableMagnificationCallback implements
diff --git a/services/autofill/bugfixes.aconfig b/services/autofill/bugfixes.aconfig
index 65c446e..ec6c3b7 100644
--- a/services/autofill/bugfixes.aconfig
+++ b/services/autofill/bugfixes.aconfig
@@ -74,6 +74,16 @@
 }
 
 flag {
+  name: "multiple_fill_history"
+  namespace: "autofill"
+  description: "Allows tracking per Session FillEventHistory. As a bugfix flag to guard against DeviceConfig flag"
+  bug: "365630157"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "add_session_id_to_client_state"
   namespace: "autofill"
   description: "Include the session id into the FillEventHistory events as part of ClientState"
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 0fa43ac..11710c9 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -152,6 +152,15 @@
     @GuardedBy("mLock")
     private final SparseArray<Session> mSessions = new SparseArray<>();
 
+    /**
+     * Cache of FillEventHistory for active Sessions.
+     *
+     * <p>New histories are added whenever a Session is created and are kept until Sessions are
+     * removed through removeSessionLocked()
+     */
+    @GuardedBy("mLock")
+    private final SparseArray<FillEventHistory> mFillHistories = new SparseArray<>();
+
     /** The last selection */
     @GuardedBy("mLock")
     private FillEventHistory mEventHistory;
@@ -663,6 +672,10 @@
                 flags, mInputMethodManagerInternal, isPrimaryCredential);
         mSessions.put(newSession.id, newSession);
 
+        if (Flags.multipleFillHistory() && !forAugmentedAutofillOnly) {
+            mFillHistories.put(newSession.id, new FillEventHistory(sessionId, null));
+        }
+
         return newSession;
     }
 
@@ -756,6 +769,28 @@
                         TAG,
                         "removeSessionLocked(): removed " + sessionId);
             }
+
+            FillEventHistory history = null;
+
+            if (Flags.multipleFillHistory() && mFillHistories != null) {
+                history = mFillHistories.get(sessionId);
+                mFillHistories.delete(sessionId);
+            }
+
+            if (mInfo == null || mInfo.getServiceInfo() == null) {
+                if (sVerbose) {
+                    Slog.v(TAG, "removeSessionLocked(): early return because mInfo is null");
+                }
+                return;
+            }
+
+            if (mMaster == null) {
+                if (sVerbose) {
+                    Slog.v(TAG, "removeSessionLocked(): early return because mMaster is null");
+                }
+                return;
+            }
+
             RemoteFillService remoteService =
                     new RemoteFillService(
                             getContext(),
@@ -764,7 +799,8 @@
                             /* callbacks= */ null,
                             mMaster.isInstantServiceAllowed(),
                             /* credentialAutofillService= */ null);
-            remoteService.onSessionDestroyed(null);
+
+            remoteService.onSessionDestroyed(history);
         }
     }
 
@@ -886,6 +922,10 @@
             }
         }
         mSessions.clear();
+        if (Flags.multipleFillHistory()) {
+            mFillHistories.clear();
+        }
+
         for (int i = 0; i < remoteFillServices.size(); i++) {
             remoteFillServices.valueAt(i).destroy();
         }
@@ -944,60 +984,132 @@
         return true;
     }
 
-    /**
-     * Updates the last fill selection when an authentication was selected.
-     */
-    void setAuthenticationSelected(int sessionId, @Nullable Bundle clientState,
-            int uiType, @Nullable AutofillId focusedId) {
-        synchronized (mLock) {
-            if (isValidEventLocked("setAuthenticationSelected()", sessionId)) {
-                mEventHistory.addEvent(
-                        new Event(Event.TYPE_AUTHENTICATION_SELECTED, null, clientState, null, null,
-                                null, null, null, null, null, null,
-                                NO_SAVE_UI_REASON_NONE, uiType, focusedId));
+    @GuardedBy("mLock")
+    void addEventToHistory(String eventName, int sessionId, Event event) {
+        // For the singleton filleventhistory
+        if (isValidEventLocked(eventName, sessionId)) {
+            mEventHistory.addEvent(event);
+        }
+
+        if (Flags.multipleFillHistory()) {
+            FillEventHistory history = mFillHistories.get(sessionId);
+            if (history != null) {
+                history.addEvent(event);
+            } else {
+                if (sVerbose) {
+                    Slog.v(TAG, eventName
+                            + " not logged because FillEventHistory is not tracked for: "
+                            + sessionId);
+                }
             }
         }
     }
 
     /**
-     * Updates the last fill selection when an dataset authentication was selected.
+     * Updates the last fill selection when an authentication was selected.
      */
-    void logDatasetAuthenticationSelected(@Nullable String selectedDataset, int sessionId,
-            @Nullable Bundle clientState, int uiType, @Nullable AutofillId focusedId) {
+    void setAuthenticationSelected(int sessionId, @Nullable Bundle clientState,
+            int uiType, @Nullable AutofillId focusedId, boolean shouldAdd) {
         synchronized (mLock) {
-            if (isValidEventLocked("logDatasetAuthenticationSelected()", sessionId)) {
-                mEventHistory.addEvent(
-                        new Event(Event.TYPE_DATASET_AUTHENTICATION_SELECTED, selectedDataset,
-                                clientState, null, null, null, null, null, null, null, null,
-                                NO_SAVE_UI_REASON_NONE, uiType, focusedId));
+
+            String methodName = "setAuthenticationSelected()";
+
+            if (!shouldAdd) {
+                if (sVerbose) {
+                    Slog.v(TAG, methodName + " not logged because shouldAdd is false");
+                }
+                return;
             }
+
+            Event event =
+                    new Event(
+                            Event.TYPE_AUTHENTICATION_SELECTED,
+                            null,
+                            clientState,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            NO_SAVE_UI_REASON_NONE,
+                            uiType,
+                            focusedId);
+
+            addEventToHistory(methodName, sessionId, event);
+        }
+    }
+
+    /** Updates the last fill selection when a dataset authentication was selected. */
+    void logDatasetAuthenticationSelected(
+            @Nullable String selectedDataset,
+            int sessionId,
+            @Nullable Bundle clientState,
+            int uiType,
+            @Nullable AutofillId focusedId,
+            boolean shouldAdd) {
+        synchronized (mLock) {
+            String methodName = "logDatasetAuthenticationSelected()";
+
+            if (!shouldAdd) {
+                if (sVerbose) {
+                    Slog.v(TAG, methodName + " not logged because shouldAdd is false");
+                }
+                return;
+            }
+
+            Event event = new Event(Event.TYPE_DATASET_AUTHENTICATION_SELECTED, selectedDataset,
+                                clientState, null, null, null, null, null, null, null, null,
+                                NO_SAVE_UI_REASON_NONE, uiType, focusedId);
+            addEventToHistory(methodName, sessionId, event);
         }
     }
 
     /**
      * Updates the last fill selection when an save Ui is shown.
      */
-    void logSaveShown(int sessionId, @Nullable Bundle clientState) {
+    void logSaveShown(int sessionId, @Nullable Bundle clientState, boolean shouldAdd) {
         synchronized (mLock) {
-            if (isValidEventLocked("logSaveShown()", sessionId)) {
-                mEventHistory.addEvent(new Event(Event.TYPE_SAVE_SHOWN, null, clientState, null,
-                        null, null, null, null, null, null, null, /* focusedId= */ null));
+            String methodName = "logSaveShown()";
+
+            if (!shouldAdd) {
+                if (sVerbose) {
+                    Slog.v(TAG, methodName + " not logged because shouldAdd is false");
+                }
+                return;
             }
+
+            Event event = new Event(Event.TYPE_SAVE_SHOWN, null, clientState, null,
+                        null, null, null, null, null, null, null, /* focusedId= */ null);
+
+            addEventToHistory(methodName, sessionId, event);
         }
     }
 
-    /**
-     * Updates the last fill response when a dataset was selected.
-     */
-    void logDatasetSelected(@Nullable String selectedDataset, int sessionId,
-            @Nullable Bundle clientState,  int uiType, @Nullable AutofillId focusedId) {
+    /** Updates the last fill response when a dataset was selected. */
+    void logDatasetSelected(
+            @Nullable String selectedDataset,
+            int sessionId,
+            @Nullable Bundle clientState,
+            int uiType,
+            @Nullable AutofillId focusedId,
+            boolean shouldAdd) {
         synchronized (mLock) {
-            if (isValidEventLocked("logDatasetSelected()", sessionId)) {
-                mEventHistory.addEvent(
-                        new Event(Event.TYPE_DATASET_SELECTED, selectedDataset, clientState, null,
-                                null, null, null, null, null, null, null, NO_SAVE_UI_REASON_NONE,
-                                uiType, focusedId));
+            String methodName = "logDatasetSelected()";
+
+            if (!shouldAdd) {
+                if (sVerbose) {
+                    Slog.v(TAG, methodName + " not logged because shouldAdd is false");
+                }
+                return;
             }
+
+            Event event = new Event(Event.TYPE_DATASET_SELECTED, selectedDataset, clientState, null,
+                                null, null, null, null, null, null, null, NO_SAVE_UI_REASON_NONE,
+                                uiType, focusedId);
+            addEventToHistory(methodName, sessionId, event);
         }
     }
 
@@ -1005,40 +1117,75 @@
      * Updates the last fill response when a dataset is shown.
      */
     void logDatasetShown(int sessionId, @Nullable Bundle clientState, int uiType,
-            @Nullable AutofillId focusedId) {
+            @Nullable AutofillId focusedId, boolean shouldAdd) {
         synchronized (mLock) {
-            if (isValidEventLocked("logDatasetShown", sessionId)) {
-                mEventHistory.addEvent(
-                        new Event(Event.TYPE_DATASETS_SHOWN, null, clientState, null, null, null,
+            String methodName = "logDatasetShown()";
+
+            if (!shouldAdd) {
+                if (sVerbose) {
+                    Slog.v(TAG, methodName + " not logged because shouldAdd is false");
+                }
+                return;
+            }
+
+            Event event = new Event(Event.TYPE_DATASETS_SHOWN, null, clientState, null, null, null,
                                 null, null, null, null, null, NO_SAVE_UI_REASON_NONE,
-                                uiType, focusedId));
+                                uiType, focusedId);
+            addEventToHistory(methodName, sessionId, event);
+        }
+    }
+
+    void logViewEnteredForHistory(
+            int sessionId,
+            @Nullable Bundle clientState,
+            FillEventHistory history,
+            @Nullable AutofillId focusedId) {
+        if (history.getEvents() != null) {
+            // Do not log this event more than once
+            for (Event event : history.getEvents()) {
+                if (event.getType() == Event.TYPE_VIEW_REQUESTED_AUTOFILL) {
+                    if (sVerbose) {
+                        Slog.v(TAG, "logViewEntered: already logged TYPE_VIEW_REQUESTED_AUTOFILL");
+                    }
+                    return;
+                }
             }
         }
+
+        history.addEvent(
+                new Event(Event.TYPE_VIEW_REQUESTED_AUTOFILL, null, clientState, null,
+                        null, null, null, null, null, null, null, focusedId));
     }
 
     /**
      * Updates the last fill response when a view was entered.
      */
     void logViewEntered(int sessionId, @Nullable Bundle clientState,
-            @Nullable AutofillId focusedId) {
+            @Nullable AutofillId focusedId, boolean shouldAdd) {
         synchronized (mLock) {
-            if (!isValidEventLocked("logViewEntered", sessionId)) {
+            String methodName = "logViewEntered()";
+
+            if (!shouldAdd) {
+                if (sVerbose) {
+                    Slog.v(TAG, methodName + " not logged because shouldAdd is false");
+                }
                 return;
             }
 
-            if (mEventHistory.getEvents() != null) {
-                // Do not log this event more than once
-                for (Event event : mEventHistory.getEvents()) {
-                    if (event.getType() == Event.TYPE_VIEW_REQUESTED_AUTOFILL) {
-                        Slog.v(TAG, "logViewEntered: already logged TYPE_VIEW_REQUESTED_AUTOFILL");
-                        return;
-                    }
-                }
+            // This log does not call addEventToHistory() because each distinct FillEventHistory
+            // can only contain 1 TYPE_VIEW_REQUESTED_AUTOFILL event. Therefore, checking both
+            // the singleton FillEventHistory and the per Session FillEventHistory is necessary
+
+            if (isValidEventLocked(methodName, sessionId)) {
+                logViewEnteredForHistory(sessionId, clientState, mEventHistory, focusedId);
             }
 
-            mEventHistory.addEvent(
-                    new Event(Event.TYPE_VIEW_REQUESTED_AUTOFILL, null, clientState, null,
-                            null, null, null, null, null, null, null, focusedId));
+            if (Flags.multipleFillHistory()) {
+                FillEventHistory history = mFillHistories.get(sessionId);
+                if (history != null) {
+                    logViewEnteredForHistory(sessionId, clientState, history, focusedId);
+                }
+            }
         }
     }
 
@@ -1096,12 +1243,12 @@
             @Nullable ArrayList<String> changedDatasetIds,
             @Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
             @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
-            @NonNull ComponentName appComponentName, boolean compatMode) {
+            @NonNull ComponentName appComponentName, boolean compatMode, boolean shouldAdd) {
         logContextCommittedLocked(sessionId, clientState, selectedDatasets, ignoredDatasets,
                 changedFieldIds, changedDatasetIds, manuallyFilledFieldIds,
                 manuallyFilledDatasetIds, /* detectedFieldIdsList= */ null,
                 /* detectedFieldClassificationsList= */ null, appComponentName, compatMode,
-                Event.NO_SAVE_UI_REASON_NONE);
+                Event.NO_SAVE_UI_REASON_NONE, shouldAdd);
     }
 
     @GuardedBy("mLock")
@@ -1115,9 +1262,19 @@
             @Nullable ArrayList<AutofillId> detectedFieldIdsList,
             @Nullable ArrayList<FieldClassification> detectedFieldClassificationsList,
             @NonNull ComponentName appComponentName, boolean compatMode,
-            @NoSaveReason int saveDialogNotShowReason) {
-        if (isValidEventLocked("logDatasetNotSelected()", sessionId)) {
+            @NoSaveReason int saveDialogNotShowReason,
+            boolean shouldAdd) {
+
+        String methodName = "logContextCommittedLocked()";
+
+        if (!shouldAdd) {
             if (sVerbose) {
+                Slog.v(TAG, methodName + " not logged because shouldAdd is false");
+            }
+            return;
+        }
+
+        if (sVerbose) {
                 Slog.v(TAG, "logContextCommitted() with FieldClassification: id=" + sessionId
                         + ", selectedDatasets=" + selectedDatasets
                         + ", ignoredDatasetIds=" + ignoredDatasets
@@ -1129,44 +1286,58 @@
                         + ", appComponentName=" + appComponentName.toShortString()
                         + ", compatMode=" + compatMode
                         + ", saveDialogNotShowReason=" + saveDialogNotShowReason);
-            }
-            AutofillId[] detectedFieldsIds = null;
-            FieldClassification[] detectedFieldClassifications = null;
-            if (detectedFieldIdsList != null) {
-                detectedFieldsIds = new AutofillId[detectedFieldIdsList.size()];
-                detectedFieldIdsList.toArray(detectedFieldsIds);
-                detectedFieldClassifications =
-                        new FieldClassification[detectedFieldClassificationsList.size()];
-                detectedFieldClassificationsList.toArray(detectedFieldClassifications);
-
-                final int numberFields = detectedFieldsIds.length;
-                int totalSize = 0;
-                float totalScore = 0;
-                for (int i = 0; i < numberFields; i++) {
-                    final FieldClassification fc = detectedFieldClassifications[i];
-                    final List<Match> matches = fc.getMatches();
-                    final int size = matches.size();
-                    totalSize += size;
-                    for (int j = 0; j < size; j++) {
-                        totalScore += matches.get(j).getScore();
-                    }
-                }
-
-                final int averageScore = (int) ((totalScore * 100) / totalSize);
-                mMetricsLogger.write(Helper
-                        .newLogMaker(MetricsEvent.AUTOFILL_FIELD_CLASSIFICATION_MATCHES,
-                                appComponentName, getServicePackageName(), sessionId, compatMode)
-                        .setCounterValue(numberFields)
-                        .addTaggedData(MetricsEvent.FIELD_AUTOFILL_MATCH_SCORE,
-                                averageScore));
-            }
-            mEventHistory.addEvent(new Event(Event.TYPE_CONTEXT_COMMITTED, null,
-                    clientState, selectedDatasets, ignoredDatasets,
-                    changedFieldIds, changedDatasetIds,
-                    manuallyFilledFieldIds, manuallyFilledDatasetIds,
-                    detectedFieldsIds, detectedFieldClassifications, saveDialogNotShowReason,
-                    /* focusedId= */ null));
         }
+
+        AutofillId[] detectedFieldsIds = null;
+        FieldClassification[] detectedFieldClassifications = null;
+        if (detectedFieldIdsList != null) {
+            detectedFieldsIds = new AutofillId[detectedFieldIdsList.size()];
+            detectedFieldIdsList.toArray(detectedFieldsIds);
+            detectedFieldClassifications =
+                    new FieldClassification[detectedFieldClassificationsList.size()];
+            detectedFieldClassificationsList.toArray(detectedFieldClassifications);
+
+            final int numberFields = detectedFieldsIds.length;
+            int totalSize = 0;
+            float totalScore = 0;
+            for (int i = 0; i < numberFields; i++) {
+                final FieldClassification fc = detectedFieldClassifications[i];
+                final List<Match> matches = fc.getMatches();
+                final int size = matches.size();
+                totalSize += size;
+                for (int j = 0; j < size; j++) {
+                    totalScore += matches.get(j).getScore();
+                }
+            }
+
+            final int averageScore = (int) ((totalScore * 100) / totalSize);
+            mMetricsLogger.write(
+                    Helper.newLogMaker(
+                                    MetricsEvent.AUTOFILL_FIELD_CLASSIFICATION_MATCHES,
+                                    appComponentName,
+                                    getServicePackageName(),
+                                    sessionId,
+                                    compatMode)
+                            .setCounterValue(numberFields)
+                            .addTaggedData(MetricsEvent.FIELD_AUTOFILL_MATCH_SCORE, averageScore));
+        }
+        Event event =
+                new Event(
+                        Event.TYPE_CONTEXT_COMMITTED,
+                        null,
+                        clientState,
+                        selectedDatasets,
+                        ignoredDatasets,
+                        changedFieldIds,
+                        changedDatasetIds,
+                        manuallyFilledFieldIds,
+                        manuallyFilledDatasetIds,
+                        detectedFieldsIds,
+                        detectedFieldClassifications,
+                        saveDialogNotShowReason,
+                        /* focusedId= */ null);
+
+        addEventToHistory(methodName, sessionId, event);
     }
 
     /**
@@ -1174,7 +1345,9 @@
      *
      * @param callingUid The calling uid
      * @return The history for the autofill or the augmented autofill events depending on the {@code
-     * callingUid}, or {@code null} if there is none.
+     *     callingUid}, or {@code null} if there is none.
+     * @deprecated Use {@link
+     *     android.service.autofill.AutofillService#onSessionDestroyed(FillEventHistory)}
      */
     FillEventHistory getFillEventHistory(int callingUid) {
         synchronized (mLock) {
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index ba9865d..3ecff3b 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -1962,7 +1962,7 @@
 
             if (mLogViewEntered) {
                 mLogViewEntered = false;
-                mService.logViewEntered(id, null, mCurrentViewId);
+                mService.logViewEntered(id, null, mCurrentViewId, shouldAddEventToHistory());
             }
         }
 
@@ -2866,7 +2866,12 @@
                 forceRemoveFromServiceLocked();
                 return;
             }
-            mService.setAuthenticationSelected(id, mClientState, uiType, mCurrentViewId);
+            mService.setAuthenticationSelected(
+                    id,
+                    mClientState,
+                    uiType,
+                    mCurrentViewId,
+                    shouldAddEventToHistory());
         }
 
 
@@ -2941,7 +2946,12 @@
                 if (!mLoggedInlineDatasetShown) {
                     // Chip inflation already logged, do not log again.
                     // This is needed because every chip inflation will call this.
-                    mService.logDatasetShown(this.id, mClientState, uiType, mCurrentViewId);
+                    mService.logDatasetShown(
+                            this.id,
+                            mClientState,
+                            uiType,
+                            mCurrentViewId,
+                            shouldAddEventToHistory());
                     Slog.d(TAG, "onShown(): " + uiType + ", " + numDatasetsShown);
                 }
                 mLoggedInlineDatasetShown = true;
@@ -2949,7 +2959,12 @@
                 mPresentationStatsEventLogger.logWhenDatasetShown(numDatasetsShown);
                 // Explicitly sets maybeSetSuggestionPresentedTimestampMs
                 mPresentationStatsEventLogger.maybeSetSuggestionPresentedTimestampMs();
-                mService.logDatasetShown(this.id, mClientState, uiType, mCurrentViewId);
+                mService.logDatasetShown(
+                        this.id,
+                        mClientState,
+                        uiType,
+                        mCurrentViewId,
+                        shouldAddEventToHistory());
                 Slog.d(TAG, "onShown(): " + uiType + ", " + numDatasetsShown);
             }
         }
@@ -3943,7 +3958,8 @@
                 detectedFieldClassifications,
                 mComponentName,
                 mCompatMode,
-                saveDialogNotShowReason);
+                saveDialogNotShowReason,
+                shouldAddEventToHistory());
         mSessionCommittedEventLogger.maybeSetCommitReason(commitReason);
         mSessionCommittedEventLogger.maybeSetRequestCount(mRequestCount);
         mSaveEventLogger.maybeSetSaveUiNotShownReason(saveDialogNotShowReason);
@@ -4590,7 +4606,7 @@
     }
 
     private void logSaveShown() {
-        mService.logSaveShown(id, mClientState);
+        mService.logSaveShown(id, mClientState, shouldAddEventToHistory());
     }
 
     @Nullable
@@ -5248,7 +5264,8 @@
                         // so this calling logViewEntered will be a nop.
                         // Calling logViewEntered() twice will only log it once
                         // TODO(271181979): this is broken for multiple partitions
-                        mService.logViewEntered(this.id, null, mCurrentViewId);
+                        mService.logViewEntered(
+                                this.id, null, mCurrentViewId, shouldAddEventToHistory());
                     }
 
                     // If this is the first time view is entered for inline, the last
@@ -6863,8 +6880,13 @@
             // Autofill it directly...
             if (dataset.getAuthentication() == null) {
                 if (generateEvent) {
-                    mService.logDatasetSelected(dataset.getId(), id, mClientState, uiType,
-                            mCurrentViewId);
+                    mService.logDatasetSelected(
+                            dataset.getId(),
+                            id,
+                            mClientState,
+                            uiType,
+                            mCurrentViewId,
+                            shouldAddEventToHistory());
                 }
                 if (mCurrentViewId != null) {
                     mInlineSessionController.hideInlineSuggestionsUiLocked(mCurrentViewId);
@@ -6875,7 +6897,7 @@
 
             // ...or handle authentication.
             mService.logDatasetAuthenticationSelected(dataset.getId(), id, mClientState, uiType,
-                        mCurrentViewId);
+                        mCurrentViewId, shouldAddEventToHistory());
             mPresentationStatsEventLogger.maybeSetAuthenticationType(
                     AUTHENTICATION_TYPE_DATASET_AUTHENTICATION);
             // does not matter the value of isPrimary because null response won't be overridden.
@@ -8018,6 +8040,32 @@
         mService.getMaster().logRequestLocked(historyItem);
     }
 
+    /**
+     * Don't add secondary providers to FillEventHistory
+     */
+    boolean shouldAddEventToHistory() {
+
+        FillResponse lastResponse = null;
+
+        synchronized (mLock) {
+            lastResponse = getLastResponseLocked("shouldAddEventToHistory(%s)");
+        }
+
+        // There might be events (like TYPE_VIEW_REQUESTED_AUTOFILL) that are
+        // generated before FillRequest/FillResponse mechanism are started, so
+        // still need to log it
+        if (lastResponse == null) {
+            return true;
+        }
+
+        if (mRequestId.isSecondaryProvider(lastResponse.getRequestId())) {
+            // The request was to a secondary provider - don't log these events
+            return false;
+        }
+
+        return true;
+    }
+
     private void wtf(@Nullable Exception e, String fmt, Object... args) {
         final String message = String.format(fmt, args);
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index 73d5630..8681ea5 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -221,6 +221,18 @@
             systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_EQUALS,
                     KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
                     KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_DPAD_LEFT,
+                    KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_LEFT));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_DPAD_RIGHT,
+                    KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_RIGHT));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_DPAD_UP,
+                    KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_UP));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_DPAD_DOWN,
+                    KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_DOWN));
             systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_M,
                     KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
                     KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION));
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
index c878799..4ef602f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
@@ -25,6 +25,7 @@
 import static com.android.server.wm.WindowManagerInternal.AccessibilityControllerInternal.UiChangesForAccessibilityCallbacks;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -34,6 +35,7 @@
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 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.doAnswer;
@@ -50,9 +52,11 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -60,6 +64,7 @@
 import android.provider.Settings;
 import android.test.mock.MockContentResolver;
 import android.testing.DexmakerShareClassLoaderRule;
+import android.util.DisplayMetrics;
 import android.view.Display;
 import android.view.DisplayInfo;
 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
@@ -79,6 +84,8 @@
 import com.android.server.input.InputManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 
+import com.google.common.truth.Expect;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -96,6 +103,9 @@
 @RunWith(AndroidJUnit4.class)
 public class MagnificationControllerTest {
 
+    @Rule
+    public final Expect expect = Expect.create();
+
     private static final int TEST_DISPLAY = Display.DEFAULT_DISPLAY;
     private static final int TEST_SERVICE_ID = 1;
     private static final Region INITIAL_SCREEN_MAGNIFICATION_REGION =
@@ -119,6 +129,8 @@
     @Mock
     private Context mContext;
     @Mock
+    private Resources mResources;
+    @Mock
     private PackageManager mPackageManager;
 
     @Mock
@@ -156,6 +168,7 @@
 
     @Mock
     private DisplayManagerInternal mDisplayManagerInternal;
+    private Display mDisplay;
 
     @Mock
     private Scroller mMockScroller;
@@ -210,6 +223,12 @@
         LocalServices.removeServiceForTest(DisplayManagerInternal.class);
         LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal);
 
+        DisplayManager displayManager = new DisplayManager(mContext);
+        mDisplay = displayManager.getDisplay(TEST_DISPLAY);
+        when(mContext.getSystemServiceName(DisplayManager.class)).thenReturn(
+                Context.DISPLAY_SERVICE);
+        when(mContext.getSystemService(DisplayManager.class)).thenReturn(displayManager);
+
         mScreenMagnificationController =
                 spy(
                         new FullScreenMagnificationController(
@@ -686,16 +705,19 @@
 
         float currentScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
         while (currentScale < SCALE_MAX_VALUE) {
-            assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
-                    MagnificationController.ZOOM_DIRECTION_IN)).isTrue();
+            mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                    MagnificationController.ZOOM_DIRECTION_IN);
             final float nextScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
             assertThat(nextScale).isGreaterThan(currentScale);
             currentScale = nextScale;
         }
 
         assertThat(currentScale).isEqualTo(SCALE_MAX_VALUE);
-        assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
-                MagnificationController.ZOOM_DIRECTION_IN)).isFalse();
+        // Trying to scale further does not change the scale.
+        mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.ZOOM_DIRECTION_IN);
+        final float finalScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
+        assertThat(finalScale).isEqualTo(currentScale);
     }
 
     @Test
@@ -706,16 +728,19 @@
 
         float currentScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
         while (currentScale > SCALE_MIN_VALUE) {
-            assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
-                    MagnificationController.ZOOM_DIRECTION_OUT)).isTrue();
+            mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                    MagnificationController.ZOOM_DIRECTION_OUT);
             final float nextScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
             assertThat(nextScale).isLessThan(currentScale);
             currentScale = nextScale;
         }
 
         assertThat(currentScale).isEqualTo(SCALE_MIN_VALUE);
-        assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
-                MagnificationController.ZOOM_DIRECTION_OUT)).isFalse();
+        // Trying to scale further does not change the scale.
+        mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.ZOOM_DIRECTION_OUT);
+        final float finalScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
+        assertThat(finalScale).isEqualTo(currentScale);
     }
 
     @Test
@@ -740,6 +765,121 @@
     }
 
     @Test
+    public void panMagnificationByStep_fullscreenMode_stepSizeAtScale2() throws RemoteException {
+        setMagnificationEnabled(MODE_FULLSCREEN);
+        // At scale 2.0f, each step should be about 40 dpi.
+        mMagnificationController.onPerformScaleAction(TEST_DISPLAY, 2.0f, false);
+        reset(mScreenMagnificationController);
+
+        testFullscreenMagnificationPanWithStepSize(40.0f);
+    }
+
+    @Test
+    public void panMagnificationByStep_fullscreenMode_stepSizeAtScale8() throws RemoteException {
+        setMagnificationEnabled(MODE_FULLSCREEN);
+        // At scale 8.0f, each step should be about 27 dpi.
+        mMagnificationController.onPerformScaleAction(TEST_DISPLAY, 8.0f, false);
+        reset(mScreenMagnificationController);
+
+        testFullscreenMagnificationPanWithStepSize(27.0f);
+    }
+
+    @Test
+    public void panMagnificationByStep_windowMode_stepSizeAtScale2() throws RemoteException {
+        mMagnificationConnectionManager.enableWindowMagnification(TEST_DISPLAY, 2.0f, 100f, 200f);
+
+        testWindowMagnificationPanWithStepSize(40.0f);
+    }
+
+    @Test
+    public void panMagnificationByStep_windowMode_stepSizeAtScale8() throws RemoteException {
+        setMagnificationEnabled(MODE_WINDOW);
+        // At scale 8.0f, each step should be about 27.
+        mMagnificationController.onPerformScaleAction(TEST_DISPLAY, 8.0f, false);
+        reset(mMagnificationConnectionManager);
+
+        testWindowMagnificationPanWithStepSize(27.0f);
+    }
+
+    @Test
+    public void panMagnificationByStep_fullscreenMode_reachesRightEdgeOfScreen()
+            throws RemoteException {
+        setMagnificationEnabled(MODE_FULLSCREEN);
+        // At scale 2.0f, each step should be about 40.
+        mMagnificationController.onPerformScaleAction(TEST_DISPLAY, DEFAULT_SCALE, false);
+        reset(mScreenMagnificationController);
+
+        float currentCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY);
+        float currentCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY);
+
+        DisplayMetrics metrics = new DisplayMetrics();
+        mDisplay.getMetrics(metrics);
+        float expectedStep = 40.0f * metrics.density;
+
+        // Move right, eventually we should reach the edge.
+        int maxNumSteps = (int) (metrics.widthPixels / expectedStep) + 1;
+        int numSteps = 0;
+        while (numSteps < maxNumSteps) {
+            mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                    MagnificationController.PAN_DIRECTION_RIGHT);
+            float newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY);
+            float newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY);
+            assertThat(currentCenterY).isEqualTo(newCenterY);
+
+            assertThat(newCenterX).isAtLeast(currentCenterX);
+            if (newCenterX == currentCenterX) {
+                break;
+            }
+
+            currentCenterX = newCenterX;
+            currentCenterY = newCenterY;
+            numSteps++;
+        }
+        assertWithMessage("Still not at edge after panning right " + numSteps
+                + " steps. Current position: " + currentCenterX + "," + currentCenterY)
+                .that(numSteps).isLessThan(maxNumSteps);
+    }
+
+    @Test
+    public void panMagnificationByStep_fullscreenMode_reachesBottomEdgeOfScreen()
+            throws RemoteException {
+        setMagnificationEnabled(MODE_FULLSCREEN);
+        // At scale 2.0f, each step should be about 40.
+        mMagnificationController.onPerformScaleAction(TEST_DISPLAY, DEFAULT_SCALE, false);
+        reset(mScreenMagnificationController);
+
+        float currentCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY);
+        float currentCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY);
+
+        DisplayMetrics metrics = new DisplayMetrics();
+        mDisplay.getMetrics(metrics);
+        float expectedStep = 40.0f * metrics.density;
+
+        // Move down, eventually we should reach the edge.
+        int maxNumSteps = (int) (metrics.heightPixels / expectedStep) + 1;
+        int numSteps = 0;
+        while (numSteps < maxNumSteps) {
+            mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                    MagnificationController.PAN_DIRECTION_DOWN);
+            float newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY);
+            float newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY);
+            assertThat(currentCenterX).isEqualTo(newCenterX);
+
+            assertThat(newCenterY).isAtLeast(currentCenterY);
+            if (newCenterY == currentCenterY) {
+                break;
+            }
+
+            currentCenterX = newCenterX;
+            currentCenterY = newCenterY;
+            numSteps++;
+        }
+        assertWithMessage("Still not at edge after panning down "
+                + numSteps + " steps. Current position: " + currentCenterX + "," + currentCenterY)
+                .that(numSteps).isLessThan(maxNumSteps);
+    }
+
+    @Test
     public void enableWindowMode_notifyMagnificationChanged() throws RemoteException {
         setMagnificationEnabled(MODE_WINDOW);
 
@@ -1425,6 +1565,91 @@
         return captor.getValue();
     }
 
+    private void testFullscreenMagnificationPanWithStepSize(float expectedStep) {
+        DisplayMetrics metrics = new DisplayMetrics();
+        mDisplay.getMetrics(metrics);
+        expectedStep *= metrics.density;
+
+        float currentCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY);
+        float currentCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY);
+
+        // Move right.
+        mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.PAN_DIRECTION_RIGHT);
+        float newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY);
+        float newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY);
+        expect.that(currentCenterX).isLessThan(newCenterX);
+        expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep);
+        expect.that(currentCenterY).isEqualTo(newCenterY);
+
+        currentCenterX = newCenterX;
+        currentCenterY = newCenterY;
+
+        // Move left.
+        mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.PAN_DIRECTION_LEFT);
+        newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY);
+        newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY);
+        expect.that(currentCenterX).isGreaterThan(newCenterX);
+        expect.that(currentCenterX - newCenterX).isWithin(0.01f).of(expectedStep);
+        expect.that(currentCenterY).isEqualTo(newCenterY);
+
+        currentCenterX = newCenterX;
+        currentCenterY = newCenterY;
+
+        // Move down.
+        mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.PAN_DIRECTION_DOWN);
+        newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY);
+        newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY);
+        expect.that(currentCenterX).isEqualTo(newCenterX);
+        expect.that(currentCenterY).isLessThan(newCenterY);
+        expect.that(newCenterY - currentCenterY).isWithin(0.1f).of(expectedStep);
+
+        currentCenterX = newCenterX;
+        currentCenterY = newCenterY;
+
+        // Move up.
+        mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.PAN_DIRECTION_UP);
+        newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY);
+        newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY);
+        expect.that(currentCenterX).isEqualTo(newCenterX);
+        expect.that(currentCenterY).isGreaterThan(newCenterY);
+        expect.that(currentCenterY - newCenterY).isWithin(0.01f).of(expectedStep);
+    }
+
+    private void testWindowMagnificationPanWithStepSize(float expectedStepDip)
+            throws RemoteException {
+        DisplayMetrics metrics = new DisplayMetrics();
+        mDisplay.getMetrics(metrics);
+        final float expectedStep = expectedStepDip * metrics.density;
+
+        // Move right.
+        mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.PAN_DIRECTION_RIGHT);
+        verify(mMockConnection.getConnection()).moveWindowMagnifier(eq(TEST_DISPLAY),
+                floatThat(step -> Math.abs(step - expectedStep) < 0.0001), eq(0.0f));
+
+        // Move left.
+        mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.PAN_DIRECTION_LEFT);
+        verify(mMockConnection.getConnection()).moveWindowMagnifier(eq(TEST_DISPLAY),
+                floatThat(step -> Math.abs(expectedStep - step) < 0.0001), eq(0.0f));
+
+        // Move down.
+        mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.PAN_DIRECTION_DOWN);
+        verify(mMockConnection.getConnection()).moveWindowMagnifier(eq(TEST_DISPLAY),
+                eq(0.0f), floatThat(step -> Math.abs(expectedStep - step) < 0.0001));
+
+        // Move up.
+        mMagnificationController.panMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.PAN_DIRECTION_UP);
+        verify(mMockConnection.getConnection()).moveWindowMagnifier(eq(TEST_DISPLAY),
+                eq(0.0f), floatThat(step -> Math.abs(expectedStep - step) < 0.0001));
+    }
+
     private static class WindowMagnificationMgrCallbackDelegate implements
             MagnificationConnectionManager.Callback {
         private MagnificationConnectionManager.Callback mCallback;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 80e86a1..fbd53f7 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -3169,7 +3169,7 @@
 
         doThrow(new SecurityException("no access")).when(mUgmInternal)
                 .checkGrantUriPermission(eq(UID_N_MR1), any(), eq(sound),
-                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+                    anyInt(), eq(UserHandle.getUserId(UID_N_MR1)));
 
         final NotificationChannel channel = new NotificationChannel("id2", "name2",
                 NotificationManager.IMPORTANCE_DEFAULT);
@@ -3189,7 +3189,7 @@
 
         doThrow(new SecurityException("no access")).when(mUgmInternal)
                 .checkGrantUriPermission(eq(UID_N_MR1), any(), any(),
-                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+                    anyInt(), eq(UserHandle.getUserId(UID_N_MR1)));
 
         final NotificationChannel channel = new NotificationChannel("id2", "name2",
                 NotificationManager.IMPORTANCE_DEFAULT);
@@ -3208,7 +3208,7 @@
 
         doThrow(new SecurityException("no access")).when(mUgmInternal)
                 .checkGrantUriPermission(eq(UID_N_MR1), any(), any(),
-                    anyInt(), eq(Process.myUserHandle().getIdentifier()));
+                    anyInt(), eq(UserHandle.getUserId(UID_N_MR1)));
 
         final NotificationChannel channel = new NotificationChannel("id2", "name2",
                 NotificationManager.IMPORTANCE_DEFAULT);
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index fafb0e0..4959cb3 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -782,6 +782,54 @@
                 KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
                 intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
             ),
+            TestData(
+                "META + ALT + 'Down' -> Magnification Pan Down",
+                intArrayOf(
+                    KeyEvent.KEYCODE_CTRL_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_DPAD_DOWN
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_DOWN,
+                intArrayOf(KeyEvent.KEYCODE_DPAD_DOWN),
+                KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "META + ALT + 'Up' -> Magnification Pan Up",
+                intArrayOf(
+                    KeyEvent.KEYCODE_CTRL_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_DPAD_UP
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_UP,
+                intArrayOf(KeyEvent.KEYCODE_DPAD_UP),
+                KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "META + ALT + 'Left' -> Magnification Pan Left",
+                intArrayOf(
+                    KeyEvent.KEYCODE_CTRL_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_DPAD_LEFT
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_LEFT,
+                intArrayOf(KeyEvent.KEYCODE_DPAD_LEFT),
+                KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "META + ALT + 'Right' -> Magnification Pan Right",
+                intArrayOf(
+                    KeyEvent.KEYCODE_CTRL_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_DPAD_RIGHT
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_PAN_RIGHT,
+                intArrayOf(KeyEvent.KEYCODE_DPAD_RIGHT),
+                KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
         )
     }