Merge "Fix typo in MR2ProviderServiceProxy"
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/MotionEventsHandlerBase.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/MotionEventsHandlerBase.java
new file mode 100644
index 0000000..2a64185
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/MotionEventsHandlerBase.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 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.systemui.plugins;
+
+import android.view.MotionEvent;
+
+/** Handles both trackpad and touch events and report displacements in both axis's. */
+public interface MotionEventsHandlerBase {
+
+    void onMotionEvent(MotionEvent ev);
+
+    float getDisplacementX(MotionEvent ev);
+
+    float getDisplacementY(MotionEvent ev);
+
+    String dump();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java
index 5f6f11c..054e300 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java
@@ -48,6 +48,9 @@
     /** Sets the base LayoutParams for the UI. */
     void setLayoutParams(WindowManager.LayoutParams layoutParams);
 
+    /** Sets the motion events handler for the plugin. */
+    default void setMotionEventsHandler(MotionEventsHandlerBase motionEventsHandler) {}
+
     /** Updates the UI based on the motion events passed in device coordinates. */
     void onMotionEvent(MotionEvent motionEvent);
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 6e927b0..1950c69 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -38,6 +38,7 @@
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.internal.util.LatencyTracker
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.MotionEventsHandlerBase
 import com.android.systemui.plugins.NavigationEdgeBackPlugin
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -498,6 +499,10 @@
         windowManager.addView(mView, layoutParams)
     }
 
+    override fun setMotionEventsHandler(motionEventsHandler: MotionEventsHandlerBase?) {
+        TODO("Not yet implemented")
+    }
+
     private fun isFlung() = velocityTracker!!.run {
         computeCurrentVelocity(1000)
         abs(xVelocity) > MIN_FLING_VELOCITY
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 6c99b67..e32c301 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -18,6 +18,8 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
 
 import static com.android.systemui.classifier.Classifier.BACK_GESTURE;
+import static com.android.systemui.navigationbar.gestural.Utilities.getTrackpadScale;
+import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadMotionEvent;
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
@@ -241,6 +243,7 @@
     private boolean mIsBackGestureAllowed;
     private boolean mGestureBlockingActivityRunning;
     private boolean mIsNewBackAffordanceEnabled;
+    private boolean mIsTrackpadGestureBackEnabled;
     private boolean mIsButtonForceVisible;
 
     private InputMonitor mInputMonitor;
@@ -269,6 +272,7 @@
     private LogArray mGestureLogOutsideInsets = new LogArray(MAX_NUM_LOGGED_GESTURES);
 
     private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver;
+    private final MotionEventsHandler mMotionEventsHandler;
 
     private final NavigationEdgeBackPlugin.BackCallback mBackCallback =
             new NavigationEdgeBackPlugin.BackCallback() {
@@ -401,6 +405,7 @@
 
         mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(
                 mContext.getMainThreadHandler(), mContext, this::onNavigationSettingsChanged);
+        mMotionEventsHandler = new MotionEventsHandler(featureFlags, getTrackpadScale(context));
 
         updateCurrentUserResources();
     }
@@ -578,6 +583,7 @@
 
             // Add a nav bar panel window
             mIsNewBackAffordanceEnabled = mFeatureFlags.isEnabled(Flags.NEW_BACK_AFFORDANCE);
+            mIsTrackpadGestureBackEnabled = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_BACK);
             resetEdgeBackPlugin();
             mPluginManager.addPluginListener(
                     this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false);
@@ -611,6 +617,7 @@
         }
         mEdgeBackPlugin = edgeBackPlugin;
         mEdgeBackPlugin.setBackCallback(mBackCallback);
+        mEdgeBackPlugin.setMotionEventsHandler(mMotionEventsHandler);
         mEdgeBackPlugin.setLayoutParams(createLayoutParams());
         updateDisplaySize();
     }
@@ -854,6 +861,7 @@
     }
 
     private void onMotionEvent(MotionEvent ev) {
+        mMotionEventsHandler.onMotionEvent(ev);
         int action = ev.getActionMasked();
         if (action == MotionEvent.ACTION_DOWN) {
             if (DEBUG_MISSING_GESTURE) {
@@ -863,15 +871,24 @@
             // Verify if this is in within the touch region and we aren't in immersive mode, and
             // either the bouncer is showing or the notification panel is hidden
             mInputEventReceiver.setBatchingEnabled(false);
-            mIsOnLeftEdge = ev.getX() <= mEdgeWidthLeft + mLeftInset;
+            boolean isTrackpadEvent = isTrackpadMotionEvent(mIsTrackpadGestureBackEnabled, ev);
+            if (isTrackpadEvent) {
+                // TODO: show the back arrow based on the direction of the swipe.
+                mIsOnLeftEdge = false;
+            } else {
+                mIsOnLeftEdge = ev.getX() <= mEdgeWidthLeft + mLeftInset;
+            }
             mMLResults = 0;
             mLogGesture = false;
             mInRejectedExclusion = false;
             boolean isWithinInsets = isWithinInsets((int) ev.getX(), (int) ev.getY());
-            mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed && isWithinInsets
+            // Trackpad back gestures don't have zones, so we don't need to check if the down event
+            // is within insets.
+            mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed
+                    && (isTrackpadEvent || isWithinInsets)
                     && !mGestureBlockingActivityRunning
                     && !QuickStepContract.isBackGestureDisabled(mSysUiFlags)
-                    && isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
+                    && (isTrackpadEvent || isWithinTouchRegion((int) ev.getX(), (int) ev.getY()));
             if (mAllowGesture) {
                 mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge);
                 mEdgeBackPlugin.onMotionEvent(ev);
@@ -885,8 +902,8 @@
 
             // For debugging purposes, only log edge points
             (isWithinInsets ? mGestureLogInsideInsets : mGestureLogOutsideInsets).log(String.format(
-                    "Gesture [%d,alw=%B,%B,%B,%B,disp=%s,wl=%d,il=%d,wr=%d,ir=%d,excl=%s]",
-                    System.currentTimeMillis(), mAllowGesture, mIsOnLeftEdge,
+                    "Gesture [%d,alw=%B,%B,%B,%B,%B,disp=%s,wl=%d,il=%d,wr=%d,ir=%d,excl=%s]",
+                    System.currentTimeMillis(), isTrackpadEvent, mAllowGesture, mIsOnLeftEdge,
                     mIsBackGestureAllowed,
                     QuickStepContract.isBackGestureDisabled(mSysUiFlags), mDisplaySize,
                     mEdgeWidthLeft, mLeftInset, mEdgeWidthRight, mRightInset, mExcludeRegion));
@@ -920,8 +937,8 @@
                         mLogGesture = false;
                         return;
                     }
-                    float dx = Math.abs(ev.getX() - mDownPoint.x);
-                    float dy = Math.abs(ev.getY() - mDownPoint.y);
+                    float dx = Math.abs(mMotionEventsHandler.getDisplacementX(ev));
+                    float dy = Math.abs(mMotionEventsHandler.getDisplacementY(ev));
                     if (dy > dx && dy > mTouchSlop) {
                         if (mAllowGesture) {
                             logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_VERTICAL_MOVE);
@@ -1055,6 +1072,7 @@
         pw.println("  mGestureLogInsideInsets=" + String.join("\n", mGestureLogInsideInsets));
         pw.println("  mGestureLogOutsideInsets=" + String.join("\n", mGestureLogOutsideInsets));
         pw.println("  mEdgeBackPlugin=" + mEdgeBackPlugin);
+        pw.println("  mMotionEventsHandler=" + mMotionEventsHandler);
         if (mEdgeBackPlugin != null) {
             mEdgeBackPlugin.dump(pw);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/MotionEventsHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/MotionEventsHandler.java
new file mode 100644
index 0000000..e9b5453
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/MotionEventsHandler.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 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.systemui.navigationbar.gestural;
+
+import static android.view.MotionEvent.AXIS_GESTURE_X_OFFSET;
+import static android.view.MotionEvent.AXIS_GESTURE_Y_OFFSET;
+
+import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadMotionEvent;
+
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.plugins.MotionEventsHandlerBase;
+
+/** Handles both trackpad and touch events and report displacements in both axis's. */
+public class MotionEventsHandler implements MotionEventsHandlerBase {
+
+    private final boolean mIsTrackpadGestureBackEnabled;
+    private final int mScale;
+
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    private float mCurrentTrackpadOffsetX = 0;
+    private float mCurrentTrackpadOffsetY = 0;
+
+    public MotionEventsHandler(FeatureFlags featureFlags, int scale) {
+        mIsTrackpadGestureBackEnabled = featureFlags.isEnabled(Flags.TRACKPAD_GESTURE_BACK);
+        mScale = scale;
+    }
+
+    @Override
+    public void onMotionEvent(MotionEvent ev) {
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                onActionDown(ev);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                onActionMove(ev);
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                onActionUp(ev);
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void onActionDown(MotionEvent ev) {
+        reset();
+        if (!isTrackpadMotionEvent(mIsTrackpadGestureBackEnabled, ev)) {
+            mDownPos.set(ev.getX(), ev.getY());
+            mLastPos.set(mDownPos);
+        }
+    }
+
+    private void onActionMove(MotionEvent ev) {
+        updateMovements(ev);
+    }
+
+    private void onActionUp(MotionEvent ev) {
+        updateMovements(ev);
+    }
+
+    private void updateMovements(MotionEvent ev) {
+        if (isTrackpadMotionEvent(mIsTrackpadGestureBackEnabled, ev)) {
+            mCurrentTrackpadOffsetX += ev.getAxisValue(AXIS_GESTURE_X_OFFSET) * mScale;
+            mCurrentTrackpadOffsetY += ev.getAxisValue(AXIS_GESTURE_Y_OFFSET) * mScale;
+        } else {
+            mLastPos.set(ev.getX(), ev.getY());
+        }
+    }
+
+    private void reset() {
+        mDownPos.set(0, 0);
+        mLastPos.set(0, 0);
+        mCurrentTrackpadOffsetX = 0;
+        mCurrentTrackpadOffsetY = 0;
+    }
+
+    @Override
+    public float getDisplacementX(MotionEvent ev) {
+        return isTrackpadMotionEvent(mIsTrackpadGestureBackEnabled, ev) ? mCurrentTrackpadOffsetX
+                : mLastPos.x - mDownPos.x;
+    }
+
+    @Override
+    public float getDisplacementY(MotionEvent ev) {
+        return isTrackpadMotionEvent(mIsTrackpadGestureBackEnabled, ev) ? mCurrentTrackpadOffsetY
+                : mLastPos.y - mDownPos.y;
+    }
+
+    @Override
+    public String dump() {
+        return "mDownPos: " + mDownPos + ", mLastPos: " + mLastPos + ", mCurrentTrackpadOffsetX: "
+                + mCurrentTrackpadOffsetX + ", mCurrentTrackpadOffsetY: " + mCurrentTrackpadOffsetY;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
index 1230708..4a07159 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
@@ -55,6 +55,8 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.plugins.MotionEventsHandlerBase;
 import com.android.systemui.plugins.NavigationEdgeBackPlugin;
 import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
 import com.android.systemui.statusbar.VibratorHelper;
@@ -200,8 +202,6 @@
      */
     private boolean mIsLeftPanel;
 
-    private float mStartX;
-    private float mStartY;
     private float mCurrentAngle;
     /**
      * The current translation of the arrow
@@ -232,6 +232,8 @@
     private final Handler mHandler = new Handler();
     private final Runnable mFailsafeRunnable = this::onFailsafe;
 
+    private MotionEventsHandlerBase mMotionEventsHandler;
+
     private DynamicAnimation.OnAnimationEndListener mSetGoneEndListener
             = new DynamicAnimation.OnAnimationEndListener() {
         @Override
@@ -437,6 +439,11 @@
         mWindowManager.addView(this, mLayoutParams);
     }
 
+    @Override
+    public void setMotionEventsHandler(MotionEventsHandlerBase motionEventsHandler) {
+        mMotionEventsHandler = motionEventsHandler;
+    }
+
     /**
      * Adjusts the sampling rect to conform to the actual visible bounding box of the arrow.
      */
@@ -481,8 +488,6 @@
             case MotionEvent.ACTION_DOWN:
                 mDragSlopPassed = false;
                 resetOnDown();
-                mStartX = event.getX();
-                mStartY = event.getY();
                 setVisibility(VISIBLE);
                 updatePosition(event.getY());
                 mRegionSamplingHelper.start(mSamplingRect);
@@ -726,10 +731,9 @@
     }
 
     private void handleMoveEvent(MotionEvent event) {
-        float x = event.getX();
-        float y = event.getY();
-        float touchTranslation = MathUtils.abs(x - mStartX);
-        float yOffset = y - mStartY;
+        float xOffset = mMotionEventsHandler.getDisplacementX(event);
+        float touchTranslation = MathUtils.abs(xOffset);
+        float yOffset = mMotionEventsHandler.getDisplacementY(event);
         float delta = touchTranslation - mPreviousTouchTranslation;
         if (Math.abs(delta) > 0) {
             if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) {
@@ -790,16 +794,14 @@
         }
 
         // Last if the direction in Y is bigger than X * 2 we also abort
-        if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) {
+        if (Math.abs(yOffset) > Math.abs(xOffset) * 2) {
             triggerBack = false;
         }
         if (DEBUG_MISSING_GESTURE && mTriggerBack != triggerBack) {
             Log.d(DEBUG_MISSING_GESTURE_TAG, "set mTriggerBack=" + triggerBack
                     + ", mTotalTouchDelta=" + mTotalTouchDelta
                     + ", mMinDeltaForSwitch=" + mMinDeltaForSwitch
-                    + ", yOffset=" + yOffset
-                    + ", x=" + x
-                    + ", mStartX=" + mStartX);
+                    + ", yOffset=" + yOffset + mMotionEventsHandler.dump());
         }
         setTriggerBack(triggerBack, true /* animated */);
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
new file mode 100644
index 0000000..335172e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 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.systemui.navigationbar.gestural;
+
+import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+public final class Utilities {
+
+    private static final int TRACKPAD_GESTURE_SCALE = 60;
+
+    public static boolean isTrackpadMotionEvent(boolean isTrackpadGestureBackEnabled,
+            MotionEvent event) {
+        // TODO: ideally should use event.getClassification(), but currently only the move
+        // events get assigned the correct classification.
+        return isTrackpadGestureBackEnabled
+                && (event.getSource() & SOURCE_TOUCHSCREEN) != SOURCE_TOUCHSCREEN;
+    }
+
+    public static int getTrackpadScale(Context context) {
+        return ViewConfiguration.get(context).getScaledTouchSlop() * TRACKPAD_GESTURE_SCALE;
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java
new file mode 100644
index 0000000..509d5f0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 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.systemui.navigationbar.gestural;
+
+import static android.view.InputDevice.SOURCE_TOUCHPAD;
+import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.MotionEvent.AXIS_GESTURE_X_OFFSET;
+import static android.view.MotionEvent.AXIS_GESTURE_Y_OFFSET;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.MotionEvent;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link MotionEventsHandler}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MotionEventsHandlerTest extends SysuiTestCase {
+
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    private static int SCALE = 100;
+
+    private MotionEventsHandler mMotionEventsHandler;
+
+    @Before
+    public void setUp() {
+        mFeatureFlags.set(Flags.TRACKPAD_GESTURE_BACK, true);
+        mMotionEventsHandler = new MotionEventsHandler(mFeatureFlags, SCALE);
+    }
+
+    @Test
+    public void onTouchEvent_touchScreen_hasCorrectDisplacements() {
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 100, 100, 0);
+        // TODO: change to use classification after gesture library is ported.
+        down.setSource(SOURCE_TOUCHSCREEN);
+        MotionEvent move1 = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 150, 125, 0);
+        move1.setSource(SOURCE_TOUCHSCREEN);
+        MotionEvent move2 = MotionEvent.obtain(0, 2, MotionEvent.ACTION_MOVE, 200, 150, 0);
+        move2.setSource(SOURCE_TOUCHSCREEN);
+        MotionEvent up = MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, 250, 175, 0);
+        up.setSource(SOURCE_TOUCHSCREEN);
+
+        mMotionEventsHandler.onMotionEvent(down);
+        mMotionEventsHandler.onMotionEvent(move1);
+        assertThat(mMotionEventsHandler.getDisplacementX(move1)).isEqualTo(50);
+        assertThat(mMotionEventsHandler.getDisplacementY(move1)).isEqualTo(25);
+        mMotionEventsHandler.onMotionEvent(move2);
+        assertThat(mMotionEventsHandler.getDisplacementX(move2)).isEqualTo(100);
+        assertThat(mMotionEventsHandler.getDisplacementY(move2)).isEqualTo(50);
+        mMotionEventsHandler.onMotionEvent(up);
+        assertThat(mMotionEventsHandler.getDisplacementX(up)).isEqualTo(150);
+        assertThat(mMotionEventsHandler.getDisplacementY(up)).isEqualTo(75);
+    }
+
+    @Test
+    public void onTouchEvent_trackpad_hasCorrectDisplacements() {
+        MotionEvent.PointerCoords[] downPointerCoords = new MotionEvent.PointerCoords[1];
+        downPointerCoords[0] = new MotionEvent.PointerCoords();
+        downPointerCoords[0].setAxisValue(AXIS_GESTURE_X_OFFSET, 0.1f);
+        downPointerCoords[0].setAxisValue(AXIS_GESTURE_Y_OFFSET, 0.1f);
+        MotionEvent.PointerProperties[] downPointerProperties =
+                new MotionEvent.PointerProperties[1];
+        downPointerProperties[0] = new MotionEvent.PointerProperties();
+        downPointerProperties[0].id = 1;
+        downPointerProperties[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1,
+                downPointerProperties, downPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0,
+                SOURCE_TOUCHPAD, 0);
+
+        MotionEvent.PointerCoords[] movePointerCoords1 = new MotionEvent.PointerCoords[1];
+        movePointerCoords1[0] = new MotionEvent.PointerCoords();
+        movePointerCoords1[0].setAxisValue(AXIS_GESTURE_X_OFFSET, 0.2f);
+        movePointerCoords1[0].setAxisValue(AXIS_GESTURE_Y_OFFSET, 0.1f);
+        MotionEvent.PointerProperties[] movePointerProperties1 =
+                new MotionEvent.PointerProperties[1];
+        movePointerProperties1[0] = new MotionEvent.PointerProperties();
+        movePointerProperties1[0].id = 1;
+        movePointerProperties1[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
+        MotionEvent move1 = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 1,
+                movePointerProperties1, movePointerCoords1, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_TOUCHPAD,
+                0);
+
+        MotionEvent.PointerCoords[] movePointerCoords2 = new MotionEvent.PointerCoords[1];
+        movePointerCoords2[0] = new MotionEvent.PointerCoords();
+        movePointerCoords2[0].setAxisValue(AXIS_GESTURE_X_OFFSET, 0.1f);
+        movePointerCoords2[0].setAxisValue(AXIS_GESTURE_Y_OFFSET, 0.4f);
+        MotionEvent.PointerProperties[] movePointerProperties2 =
+                new MotionEvent.PointerProperties[1];
+        movePointerProperties2[0] = new MotionEvent.PointerProperties();
+        movePointerProperties2[0].id = 1;
+        movePointerProperties2[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
+        MotionEvent move2 = MotionEvent.obtain(0, 2, MotionEvent.ACTION_MOVE, 1,
+                movePointerProperties2, movePointerCoords2, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_TOUCHPAD,
+                0);
+
+        MotionEvent.PointerCoords[] upPointerCoords = new MotionEvent.PointerCoords[1];
+        upPointerCoords[0] = new MotionEvent.PointerCoords();
+        upPointerCoords[0].setAxisValue(AXIS_GESTURE_X_OFFSET, 0.1f);
+        upPointerCoords[0].setAxisValue(AXIS_GESTURE_Y_OFFSET, 0.1f);
+        MotionEvent.PointerProperties[] upPointerProperties2 =
+                new MotionEvent.PointerProperties[1];
+        upPointerProperties2[0] = new MotionEvent.PointerProperties();
+        upPointerProperties2[0].id = 1;
+        upPointerProperties2[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
+        MotionEvent up = MotionEvent.obtain(0, 2, MotionEvent.ACTION_UP, 1,
+                upPointerProperties2, upPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_TOUCHPAD, 0);
+
+        mMotionEventsHandler.onMotionEvent(down);
+        mMotionEventsHandler.onMotionEvent(move1);
+        assertThat(mMotionEventsHandler.getDisplacementX(move1)).isEqualTo(20f);
+        assertThat(mMotionEventsHandler.getDisplacementY(move1)).isEqualTo(10f);
+        mMotionEventsHandler.onMotionEvent(move2);
+        assertThat(mMotionEventsHandler.getDisplacementX(move2)).isEqualTo(30f);
+        assertThat(mMotionEventsHandler.getDisplacementY(move2)).isEqualTo(50f);
+        mMotionEventsHandler.onMotionEvent(up);
+        assertThat(mMotionEventsHandler.getDisplacementX(up)).isEqualTo(40f);
+        assertThat(mMotionEventsHandler.getDisplacementY(up)).isEqualTo(60f);
+    }
+}
diff --git a/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java
index 159285a..2878743 100644
--- a/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -67,6 +67,7 @@
 import com.android.server.testing.shadows.ShadowKeyValueBackupJob;
 import com.android.server.testing.shadows.ShadowKeyValueBackupTask;
 import com.android.server.testing.shadows.ShadowSystemServiceRegistry;
+import com.android.server.testing.shadows.ShadowUserManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -101,7 +102,8 @@
         shadows = {
             ShadowBackupEligibilityRules.class,
             ShadowApplicationPackageManager.class,
-            ShadowSystemServiceRegistry.class
+            ShadowSystemServiceRegistry.class,
+            ShadowUserManager.class
         })
 @Presubmit
 public class UserBackupManagerServiceTest {
diff --git a/services/robotests/backup/src/com/android/server/backup/internal/SetupObserverTest.java b/services/robotests/backup/src/com/android/server/backup/internal/SetupObserverTest.java
index e49425b..ed7bc74 100644
--- a/services/robotests/backup/src/com/android/server/backup/internal/SetupObserverTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/internal/SetupObserverTest.java
@@ -33,6 +33,7 @@
 import com.android.server.backup.testing.BackupManagerServiceTestUtils;
 import com.android.server.testing.shadows.ShadowApplicationPackageManager;
 import com.android.server.testing.shadows.ShadowSystemServiceRegistry;
+import com.android.server.testing.shadows.ShadowUserManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -56,7 +57,8 @@
         shadows = {
             ShadowApplicationPackageManager.class,
             ShadowJobScheduler.class,
-            ShadowSystemServiceRegistry.class
+            ShadowSystemServiceRegistry.class,
+            ShadowUserManager.class
         })
 @Presubmit
 public class SetupObserverTest {
diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
index 6af7269..1abcf38 100644
--- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
@@ -128,6 +128,7 @@
 import com.android.server.testing.shadows.ShadowBackupDataOutput;
 import com.android.server.testing.shadows.ShadowEventLog;
 import com.android.server.testing.shadows.ShadowSystemServiceRegistry;
+import com.android.server.testing.shadows.ShadowUserManager;
 
 import com.google.common.base.Charsets;
 import com.google.common.truth.IterableSubject;
@@ -175,7 +176,8 @@
             ShadowBackupDataOutput.class,
             ShadowEventLog.class,
             ShadowQueuedWork.class,
-            ShadowSystemServiceRegistry.class
+            ShadowSystemServiceRegistry.class,
+            ShadowUserManager.class
         })
 @Presubmit
 public class KeyValueBackupTaskTest  {
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowUserManager.java b/services/robotests/src/com/android/server/testing/shadows/ShadowUserManager.java
index a9e4ee5..16ba210 100644
--- a/services/robotests/src/com/android/server/testing/shadows/ShadowUserManager.java
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowUserManager.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
+import android.os.UserHandle;
 import android.os.UserManager;
 
 import org.robolectric.annotation.Implementation;
@@ -50,6 +51,12 @@
         return profileIds.get(userId).stream().mapToInt(Number::intValue).toArray();
     }
 
+    /** @see UserManager#getMainUser() */
+    @Implementation
+    public UserHandle getMainUser() {
+        return null;
+    }
+
     /** Add a collection of profile IDs, all within the same profile group. */
     public void addProfileIds(@UserIdInt int... userIds) {
         final Set<Integer> profileGroup = new HashSet<>();