Fix dismiss related issues with bubbles:

- If a bubble is added while the stack is magnetized to the dismiss target,
  it's added in the wrong place.
- If all bubbles are cancelled while the stack is magnetized,
  the stack will reappear at (0,0)

Test: atest SystemUITests
Fixes: 134064858
Change-Id: I7596a471e79190cbce4bbe4541de07783377eed3
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index 7838489..9567cc5 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -239,6 +239,12 @@
         final float destinationRelativeX = stackShouldFlingLeft
                 ? stackBounds.left : stackBounds.right;
 
+        // If all bubbles were removed during a drag event, just return the X we would have animated
+        // to if there were still bubbles.
+        if (mLayout == null || mLayout.getChildCount() == 0) {
+            return destinationRelativeX;
+        }
+
         // Minimum velocity required for the stack to make it to the targeted side of the screen,
         // taking friction into account (4.2f is the number that friction scalars are multiplied by
         // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
@@ -531,13 +537,19 @@
         mWithinDismissTarget = true;
         mFirstBubbleSpringingToTouch = false;
 
-        animationForChildAtIndex(0)
-                .translationX(mLayout.getWidth() / 2f - mBubbleIconBitmapSize / 2f)
-                .translationY(destY, after)
-                .withPositionStartVelocities(velX, velY)
-                .withStiffness(SpringForce.STIFFNESS_MEDIUM)
-                .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
-                .start();
+        springFirstBubbleWithStackFollowing(
+                DynamicAnimation.TRANSLATION_X,
+                new SpringForce()
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+                        .setStiffness(SpringForce.STIFFNESS_MEDIUM),
+                velX, mLayout.getWidth() / 2f - mBubbleIconBitmapSize / 2f);
+
+        springFirstBubbleWithStackFollowing(
+                DynamicAnimation.TRANSLATION_Y,
+                new SpringForce()
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+                        .setStiffness(SpringForce.STIFFNESS_MEDIUM),
+                velY, destY, after);
     }
 
     /**
@@ -565,7 +577,7 @@
      */
     protected void springFirstBubbleWithStackFollowing(
             DynamicAnimation.ViewProperty property, SpringForce spring,
-            float vel, float finalPosition) {
+            float vel, float finalPosition, @Nullable Runnable... after) {
 
         if (mLayout.getChildCount() == 0) {
             return;
@@ -579,6 +591,13 @@
         SpringAnimation springAnimation =
                 new SpringAnimation(this, firstBubbleProperty)
                         .setSpring(spring)
+                        .addEndListener((dynamicAnimation, b, v, v1) -> {
+                            if (after != null) {
+                                for (Runnable callback : after) {
+                                    callback.run();
+                                }
+                            }
+                        })
                         .setStartVelocity(vel);
 
         cancelStackPositionAnimation(property);
@@ -635,6 +654,11 @@
 
     @Override
     void onChildAdded(View child, int index) {
+        // Don't animate additions within the dismiss target.
+        if (mWithinDismissTarget) {
+            return;
+        }
+
         if (mLayout.getChildCount() == 1) {
             // If this is the first child added, position the stack in its starting position.
             moveStackToStartPosition();
@@ -656,8 +680,12 @@
                 .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR))
                 .start();
 
+        // If there are other bubbles, pull them into the correct position.
         if (mLayout.getChildCount() > 0) {
             animationForChildAtIndex(0).translationX(mStackPosition.x).start();
+        } else {
+            // If there's no other bubbles, and we were in the dismiss target, reset the flag.
+            mWithinDismissTarget = false;
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
index 31a7d5a..d79128c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
@@ -339,10 +339,10 @@
 
         @Override
         protected void springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property,
-                SpringForce spring, float vel, float finalPosition) {
+                SpringForce spring, float vel, float finalPosition, Runnable... after) {
             mMainThreadHandler.post(() ->
                     super.springFirstBubbleWithStackFollowing(
-                            property, spring, vel, finalPosition));
+                            property, spring, vel, finalPosition, after));
         }
     }
 }