Merge "Update dismiss view location in one-handed mode" into main
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index c5bc9eb..35c1e8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -3521,7 +3521,14 @@
      */
     void onVerticalOffsetChanged(int offset) {
         // adjust dismiss view vertical position, so that it is still visible to the user
-        mDismissView.setPadding(/* left = */ 0, /* top = */ 0, /* right = */ 0, offset);
+        ViewGroup.LayoutParams lp = mDismissView.getLayoutParams();
+        if (lp instanceof FrameLayout.LayoutParams) {
+            FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) lp;
+            layoutParams.bottomMargin = offset;
+            mDismissView.setLayoutParams(layoutParams);
+        }
+        mMagneticTarget.setScreenVerticalOffset(offset);
+        mMagneticTarget.updateLocationOnScreen();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
index aac1d062..7c931df 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
@@ -352,8 +352,8 @@
 
         val targetObjectIsInMagneticFieldOf = associatedTargets.firstOrNull { target ->
             val distanceFromTargetCenter = hypot(
-                    ev.rawX - target.centerOnScreen.x,
-                    ev.rawY - target.centerOnScreen.y)
+                    ev.rawX - target.centerOnDisplayX(),
+                    ev.rawY - target.centerOnDisplayY())
             distanceFromTargetCenter < target.magneticFieldRadiusPx
         }
 
@@ -406,7 +406,6 @@
 
         // First, check for relevant gestures concluding with an ACTION_UP.
         if (ev.action == MotionEvent.ACTION_UP) {
-
             velocityTracker.computeCurrentVelocity(1000 /* units */)
             val velX = velocityTracker.xVelocity
             val velY = velocityTracker.yVelocity
@@ -542,7 +541,7 @@
         // Whether velocity is sufficient, depending on whether we're flinging into a target at the
         // top or the bottom of the screen.
         val velocitySufficient =
-                if (rawY < target.centerOnScreen.y) velY > flingToTargetMinVelocity
+                if (rawY < target.centerOnDisplayY()) velY > flingToTargetMinVelocity
                 else velY < flingToTargetMinVelocity
 
         if (!velocitySufficient) {
@@ -560,15 +559,15 @@
             val yIntercept = rawY - slope * rawX
 
             // ...calculate the x value when y = the target's y-coordinate.
-            targetCenterXIntercept = (target.centerOnScreen.y - yIntercept) / slope
+            targetCenterXIntercept = (target.centerOnDisplayY() - yIntercept) / slope
         }
 
         // The width of the area we're looking for a fling towards.
         val targetAreaWidth = target.targetView.width * flingToTargetWidthPercent
 
         // Velocity was sufficient, so return true if the intercept is within the target area.
-        return targetCenterXIntercept > target.centerOnScreen.x - targetAreaWidth / 2 &&
-                targetCenterXIntercept < target.centerOnScreen.x + targetAreaWidth / 2
+        return targetCenterXIntercept > target.centerOnDisplayX() - targetAreaWidth / 2 &&
+                targetCenterXIntercept < target.centerOnDisplayX() + targetAreaWidth / 2
     }
 
     /** Cancel animations on this object's x/y properties. */
@@ -601,6 +600,22 @@
     ) {
         val centerOnScreen = PointF()
 
+        /**
+         * Set screen vertical offset amount.
+         *
+         * Screen surface may be vertically shifted in some cases, for example when one-handed mode
+         * is enabled. [MagneticTarget] and [MagnetizedObject] set their location in screen
+         * coordinates (see [MagneticTarget.centerOnScreen] and
+         * [MagnetizedObject.getLocationOnScreen] respectively).
+         *
+         * When a [MagnetizedObject] is dragged, the touch location is determined by
+         * [MotionEvent.getRawX] and [MotionEvent.getRawY]. These work in display coordinates. When
+         * screen is shifted due to one-handed mode, display coordinates and screen coordinates do
+         * not match. To determine if a [MagnetizedObject] is dragged into a [MagneticTarget], view
+         * location on screen is translated to display coordinates using this offset value.
+         */
+        var screenVerticalOffset: Int = 0
+
         private val tempLoc = IntArray(2)
 
         fun updateLocationOnScreen() {
@@ -614,6 +629,23 @@
                         tempLoc[1] + targetView.height / 2f - targetView.translationY)
             }
         }
+
+        /**
+         * Get target center coordinate on x-axis on display. [centerOnScreen] has to be up to date
+         * by calling [updateLocationOnScreen] first.
+         */
+        fun centerOnDisplayX(): Float {
+            return centerOnScreen.x
+        }
+
+        /**
+         * Get target center coordinate on y-axis on display. [centerOnScreen] has to be up to date
+         * by calling [updateLocationOnScreen] first. Use [screenVerticalOffset] to update the
+         * screen offset compared to the display.
+         */
+        fun centerOnDisplayY(): Float {
+            return centerOnScreen.y + screenVerticalOffset
+        }
     }
 
     companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
index 9f1ee6c..a9f054e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
@@ -30,6 +30,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyFloat
 import org.mockito.Mockito
 import org.mockito.Mockito.`when`
@@ -97,7 +98,7 @@
 
         // The mock target view will pretend that it's 200x200, and at (400, 800). This means it's
         // occupying the bounds (400, 800, 600, 1000) and it has a center of (500, 900).
-        `when`(targetView.width).thenReturn(targetSize)  // width = 200
+        `when`(targetView.width).thenReturn(targetSize) // width = 200
         `when`(targetView.height).thenReturn(targetSize) // height = 200
         doAnswer { invocation ->
             (invocation.arguments[0] as IntArray).also { location ->
@@ -275,11 +276,11 @@
         // Forcefully fling the object towards the target (but never touch the magnetic field).
         dispatchMotionEvents(
                 getMotionEvent(
-                        x = targetCenterX,
+                        x = 0,
                         y = 0,
                         action = MotionEvent.ACTION_DOWN),
                 getMotionEvent(
-                        x = targetCenterX,
+                        x = targetCenterX / 2,
                         y = targetCenterY / 2),
                 getMotionEvent(
                         x = targetCenterX,
@@ -405,15 +406,78 @@
         verify(magnetListener).onStuckToTarget(magneticTarget)
     }
 
+    @Test
+    fun testMagneticTargetHasScreenOffset_moveIntoAndReleaseInTarget() {
+        magneticTarget.screenVerticalOffset = 500
+
+        dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = targetCenterY))
+        // Moved into the target location, but it should be shifted due to screen offset.
+        // Should not get stuck.
+        verify(magnetListener, never()).onStuckToTarget(magneticTarget)
+
+        dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = targetCenterY + 500))
+        verify(magnetListener).onStuckToTarget(magneticTarget)
+
+        dispatchMotionEvents(
+            getMotionEvent(
+                x = targetCenterX,
+                y = targetCenterY + 500,
+                action = MotionEvent.ACTION_UP
+            )
+        )
+
+        verify(magnetListener).onReleasedInTarget(magneticTarget)
+        verifyNoMoreInteractions(magnetListener)
+    }
+
+    @Test
+    fun testMagneticTargetHasScreenOffset_screenOffsetUpdates() {
+        magneticTarget.screenVerticalOffset = 500
+        val adjustedTargetCenter = targetCenterY + 500
+
+        dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = adjustedTargetCenter))
+        dispatchMotionEvents(getMotionEvent(x = 0, y = 0))
+        verify(magnetListener).onStuckToTarget(magneticTarget)
+        verify(magnetListener)
+                .onUnstuckFromTarget(eq(magneticTarget), anyFloat(), anyFloat(), anyBoolean())
+
+        // Offset if removed, we should now get stuck at the target location
+        magneticTarget.screenVerticalOffset = 0
+        dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = targetCenterY))
+        verify(magnetListener, times(2)).onStuckToTarget(magneticTarget)
+    }
+
+    @Test
+    fun testMagneticTargetHasScreenOffset_flingTowardsTarget() {
+        timeStep = 10
+
+        magneticTarget.screenVerticalOffset = 500
+        val adjustedTargetCenter = targetCenterY + 500
+
+        // Forcefully fling the object towards the target (but never touch the magnetic field).
+        dispatchMotionEvents(
+            getMotionEvent(x = 0, y = 0, action = MotionEvent.ACTION_DOWN),
+            getMotionEvent(x = targetCenterX / 2, y = adjustedTargetCenter / 2),
+            getMotionEvent(
+                x = targetCenterX,
+                y = adjustedTargetCenter - magneticFieldRadius * 2,
+                action = MotionEvent.ACTION_UP
+            )
+        )
+
+        // Nevertheless it should have ended up stuck to the target.
+        verify(magnetListener, times(1)).onStuckToTarget(magneticTarget)
+    }
+
     private fun getSecondMagneticTarget(): MagnetizedObject.MagneticTarget {
         // The first target view is at bounds (400, 800, 600, 1000) and it has a center of
         // (500, 900). We'll add a second one at bounds (0, 800, 200, 1000) with center (100, 900).
         val secondTargetView = mock(View::class.java)
-        var secondTargetCenterX = 100
-        var secondTargetCenterY = 900
+        val secondTargetCenterX = 100
+        val secondTargetCenterY = 900
 
         `when`(secondTargetView.context).thenReturn(context)
-        `when`(secondTargetView.width).thenReturn(targetSize)  // width = 200
+        `when`(secondTargetView.width).thenReturn(targetSize) // width = 200
         `when`(secondTargetView.height).thenReturn(targetSize) // height = 200
         doAnswer { invocation ->
             (invocation.arguments[0] as Runnable).run()