Interrupt bubble animation on stash change

This change handles cancelling the currently running bubble animation when the stash state is changing.

Demo - http://recall/-/bJtug1HhvXkkeA4MQvIaiP/4jnBgnFaIPez6m7fVLSlf

Flag: ACONFIG com.android.wm.shell.enable_bubble_bar DEVELOPMENT
Bug: 280605846
Test: atest BubbleBarViewAnimatorTest
Change-Id: I34628f8ad741228dd21285ad66e45ef2909fbdab
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0b92748..715a4d0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -172,6 +172,13 @@
         }
     }
 
+    /** Notifies that the stash state is changing. */
+    public void onStashStateChanging() {
+        if (isAnimatingNewBubble()) {
+            mBubbleBarViewAnimator.onStashStateChangingWhileAnimating();
+        }
+    }
+
     //
     // The below animators are exposed to BubbleStashController so it can manage the stashing
     // animation.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index f689a05..4b3416c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -250,6 +250,11 @@
                 && !mBubblesShowingOnHome
                 && !mBubblesShowingOnOverview;
         if (mIsStashed != isStashed) {
+            // notify the view controller that the stash state is about to change so that it can
+            // cancel an ongoing animation if there is one.
+            // note that this has to be called before updating mIsStashed with the new value,
+            // otherwise interrupting an ongoing animation may update it again with the wrong state
+            mBarViewController.onStashStateChanging();
             mIsStashed = isStashed;
             if (mAnimator != null) {
                 mAnimator.cancel();
@@ -423,4 +428,29 @@
         mIsStashed = true;
         onIsStashedChanged();
     }
+
+    /**
+     * Updates the values of the internal animators after the new bubble animation was interrupted
+     *
+     * @param isStashed whether the current state should be stashed
+     * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the
+     *                              bubble bar is showing to ensure that the stash animator runs
+     *                              smoothly.
+     */
+    public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) {
+        if (isStashed) {
+            mBubbleStashedHandleAlpha.setValue(1);
+            mIconAlphaForStash.setValue(0);
+            mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
+            mIconTranslationYForStash.updateValue(getStashTranslation());
+        } else {
+            mBubbleStashedHandleAlpha.setValue(0);
+            mHandleViewController.setTranslationYForSwipe(0);
+            mIconAlphaForStash.setValue(1);
+            mIconScaleForStash.updateValue(1);
+            mIconTranslationYForStash.updateValue(bubbleBarTranslationY);
+        }
+        mIsStashed = isStashed;
+        onIsStashedChanged();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index a6d0ff8..66521c1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -179,7 +179,17 @@
                 }
             }
         }
-        animator.addEndListener { _, _, _, _, _, _, _ ->
+        animator.addEndListener { _, _, _, canceled, _, _, _ ->
+            // if the show animation was canceled, also cancel the hide animation. this is typically
+            // canceled in this class, but could potentially be canceled elsewhere.
+            if (canceled) {
+                val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener
+                scheduler.cancel(hideAnimation)
+                animatingBubble = null
+                bubbleBarView.onAnimatingBubbleCompleted()
+                bubbleBarView.relativePivotY = 1f
+                return@addEndListener
+            }
             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
             bubbleStashController.updateTaskbarTouchRegion()
         }
@@ -200,6 +210,7 @@
      * 3. The third part is the overshoot. The handle is made fully visible.
      */
     private fun buildHideAnimation() = Runnable {
+        if (animatingBubble == null) return@Runnable
         val offset = bubbleStashController.diffBetweenHandleAndBarCenters
         val stashedHandleTranslationY =
             bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
@@ -238,9 +249,9 @@
                 }
             }
         }
-        animator.addEndListener { _, _, _, _, _, _, _ ->
+        animator.addEndListener { _, _, _, canceled, _, _, _ ->
             animatingBubble = null
-            bubbleStashController.stashBubbleBarImmediate()
+            if (!canceled) bubbleStashController.stashBubbleBarImmediate()
             bubbleBarView.onAnimatingBubbleCompleted()
             bubbleBarView.relativePivotY = 1f
             bubbleStashController.updateTaskbarTouchRegion()
@@ -256,4 +267,18 @@
         bubbleBarView.relativePivotY = 1f
         animatingBubble = null
     }
+
+    /** Notifies the animator that the taskbar area was touched during an animation. */
+    fun onStashStateChangingWhileAnimating() {
+        val hideAnimation = animatingBubble?.hideAnimation ?: return
+        scheduler.cancel(hideAnimation)
+        animatingBubble = null
+        bubbleStashController.stashedHandlePhysicsAnimator.cancel()
+        bubbleBarView.onAnimatingBubbleCompleted()
+        bubbleBarView.relativePivotY = 1f
+        bubbleStashController.onNewBubbleAnimationInterrupted(
+            /* isStashed= */ bubbleBarView.alpha == 0f,
+            bubbleBarView.translationY
+        )
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index f46fdac..7065075 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -43,6 +43,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -52,47 +53,23 @@
 class BubbleBarViewAnimatorTest {
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val animatorScheduler = TestBubbleBarViewAnimatorScheduler()
+    private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler
+    private lateinit var overflowView: BubbleView
+    private lateinit var bubbleView: BubbleView
+    private lateinit var bubble: BubbleBarBubble
+    private lateinit var bubbleBarView: BubbleBarView
+    private lateinit var bubbleStashController: BubbleStashController
 
     @Before
     fun setUp() {
+        animatorScheduler = TestBubbleBarViewAnimatorScheduler()
         PhysicsAnimatorTestUtils.prepareForTest()
     }
 
     @Test
     fun animateBubbleInForStashed() {
-        lateinit var overflowView: BubbleView
-        lateinit var bubbleView: BubbleView
-        lateinit var bubble: BubbleBarBubble
-        val bubbleBarView = BubbleBarView(context)
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
-            val inflater = LayoutInflater.from(context)
-
-            val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
-            overflowView =
-                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
-            overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
-            bubbleBarView.addView(overflowView)
-
-            val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
-            bubbleView =
-                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
-            bubble =
-                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
-            bubbleView.setBubble(bubble)
-            bubbleBarView.addView(bubbleView)
-        }
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-
-        val bubbleStashController = mock<BubbleStashController>()
-        whenever(bubbleStashController.isStashed).thenReturn(true)
-        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
-            .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
-        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
-            .thenReturn(HANDLE_TRANSLATION)
-        whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
-            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        setUpBubbleBar()
+        setUpBubbleStashController()
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
@@ -106,7 +83,7 @@
         }
 
         // let the animation start and wait for it to complete
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(handle.alpha).isEqualTo(0)
@@ -123,7 +100,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
         // let the animation start and wait for it to complete
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(handle.alpha).isEqualTo(1)
@@ -135,38 +112,8 @@
 
     @Test
     fun animateBubbleInForStashed_tapAnimatingBubble() {
-        lateinit var overflowView: BubbleView
-        lateinit var bubbleView: BubbleView
-        lateinit var bubble: BubbleBarBubble
-        val bubbleBarView = BubbleBarView(context)
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
-            val inflater = LayoutInflater.from(context)
-
-            val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
-            overflowView =
-                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
-            overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
-            bubbleBarView.addView(overflowView)
-
-            val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
-            bubbleView =
-                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
-            bubble =
-                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
-            bubbleView.setBubble(bubble)
-            bubbleBarView.addView(bubbleView)
-        }
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-
-        val bubbleStashController = mock<BubbleStashController>()
-        whenever(bubbleStashController.isStashed).thenReturn(true)
-        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
-            .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
-        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
-            .thenReturn(HANDLE_TRANSLATION)
-        whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
-            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        setUpBubbleBar()
+        setUpBubbleStashController()
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
@@ -180,7 +127,7 @@
         }
 
         // let the animation start and wait for it to complete
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(handle.alpha).isEqualTo(0)
@@ -206,6 +153,151 @@
         assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
     }
 
+    @Test
+    fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+        assertThat(handleAnimator.isRunning()).isTrue()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.onStashStateChangingWhileAnimating()
+        }
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
+
+        // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
+        // again
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        assertThat(handleAnimator.isRunning()).isFalse()
+    }
+
+    @Test
+    fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble)
+        }
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // execute the hide bubble animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        // wait for the hide animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        assertThat(handleAnimator.isRunning()).isTrue()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.onStashStateChangingWhileAnimating()
+        }
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
+
+        // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
+        // again
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        assertThat(handleAnimator.isRunning()).isFalse()
+    }
+
+    @Test
+    fun animateBubbleInForStashed_showAnimationCanceled() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+        assertThat(handleAnimator.isRunning()).isTrue()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        handleAnimator.cancel()
+        assertThat(handleAnimator.isRunning()).isFalse()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animatorScheduler.delayedBlock).isNull()
+    }
+
+    private fun setUpBubbleBar() {
+        bubbleBarView = BubbleBarView(context)
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+            val inflater = LayoutInflater.from(context)
+
+            val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
+            overflowView =
+                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+            overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
+            bubbleBarView.addView(overflowView)
+
+            val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
+            bubbleView =
+                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+            bubble =
+                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
+            bubbleView.setBubble(bubble)
+            bubbleBarView.addView(bubbleView)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+    }
+
+    private fun setUpBubbleStashController() {
+        bubbleStashController = mock<BubbleStashController>()
+        whenever(bubbleStashController.isStashed).thenReturn(true)
+        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
+            .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
+        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
+            .thenReturn(HANDLE_TRANSLATION)
+        whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+    }
+
     private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
 
         var delayedBlock: Runnable? = null