Allow updating the flyout message

Add functions to allow updating the contents of the flyout while it
is either expanding or fully expanded.

Flag: com.android.wm.shell.enable_bubble_bar
Bug: 277815200
Test: atest BubbleBarFlyoutControllerTest
Change-Id: Iea3fc7df792e02605df6b44c1da39e267f6d9d43
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
index fdbbbb0..f389d7e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -72,32 +72,63 @@
         lp.marginEnd = horizontalMargin
         container.addView(flyout, lp)
 
+        this.flyout = flyout
+        flyout.showFromCollapsed(message) { showFlyout(AnimationType.COLLAPSE, onEnd) }
+    }
+
+    private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) {
+        val flyout = this.flyout ?: return
         val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
-        animator.addUpdateListener { _ ->
-            flyout.updateExpansionProgress(animator.animatedValue as Float)
+        when (animationType) {
+            AnimationType.FADE ->
+                animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
+            AnimationType.COLLAPSE ->
+                animator.addUpdateListener { _ ->
+                    flyout.updateExpansionProgress(animator.animatedValue as Float)
+                }
         }
         animator.addListener(
-            onStart = {
-                val flyoutTop = flyout.top + flyout.translationY
-                // If the top position of the flyout is negative, then it's bleeding over the
-                // top boundary of its parent view
-                if (flyoutTop < 0) callbacks.extendTopBoundary(space = -flyoutTop.toInt())
-            },
+            onStart = { extendTopBoundary() },
             onEnd = {
-                onEnd()
+                endAction()
                 flyout.setOnClickListener { callbacks.flyoutClicked() }
             },
         )
-        flyout.showFromCollapsed(message) { animator.start() }
-        this.flyout = flyout
+        animator.start()
+    }
+
+    fun updateFlyoutFullyExpanded(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
+        val flyout = flyout ?: return
+        hideFlyout(AnimationType.FADE) {
+            flyout.updateData(message) { showFlyout(AnimationType.FADE, onEnd) }
+        }
+    }
+
+    fun updateFlyoutWhileExpanding(message: BubbleBarFlyoutMessage) {
+        val flyout = flyout ?: return
+        flyout.updateData(message) { extendTopBoundary() }
+    }
+
+    private fun extendTopBoundary() {
+        val flyout = flyout ?: return
+        val flyoutTop = flyout.top + flyout.translationY
+        // If the top position of the flyout is negative, then it's bleeding over the
+        // top boundary of its parent view
+        if (flyoutTop < 0) callbacks.extendTopBoundary(space = -flyoutTop.toInt())
     }
 
     fun cancelFlyout(endAction: () -> Unit) {
-        hideFlyout(AnimationType.FADE, endAction)
+        hideFlyout(AnimationType.FADE) {
+            cleanupFlyoutView()
+            endAction()
+        }
     }
 
     fun collapseFlyout(endAction: () -> Unit) {
-        hideFlyout(AnimationType.COLLAPSE, endAction)
+        hideFlyout(AnimationType.COLLAPSE) {
+            cleanupFlyoutView()
+            endAction()
+        }
     }
 
     private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
@@ -112,15 +143,15 @@
                     flyout.updateExpansionProgress(animator.animatedValue as Float)
                 }
         }
-        animator.addListener(
-            onStart = { flyout.setOnClickListener(null) },
-            onEnd = {
-                container.removeView(flyout)
-                this@BubbleBarFlyoutController.flyout = null
-                callbacks.resetTopBoundary()
-                endAction()
-            },
-        )
+        animator.addListener(onStart = { flyout.setOnClickListener(null) }, onEnd = { endAction() })
         animator.start()
     }
+
+    private fun cleanupFlyoutView() {
+        container.removeView(flyout)
+        this@BubbleBarFlyoutController.flyout = null
+        callbacks.resetTopBoundary()
+    }
+
+    fun hasFlyout() = flyout != null
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
index bb8a392..af8aaf8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -198,6 +198,8 @@
             } else {
                 -positioner.distanceToCollapsedPosition.x
             }
+        // TODO: b/277815200 - before collapsing, recalculate translationToCollapsedPosition because
+        // the collapsed position may have changed
         val tyToCollapsedPosition =
             positioner.distanceToCollapsedPosition.y + triangleHeight - triangleOverlap
         translationToCollapsedPosition = PointF(txToCollapsedPosition, tyToCollapsedPosition)
@@ -217,6 +219,12 @@
         scheduler.runAfterLayout(expandAnimation)
     }
 
+    /** Updates the content of the flyout and schedules [afterLayout] to run after a layout pass. */
+    fun updateData(flyoutMessage: BubbleBarFlyoutMessage, afterLayout: () -> Unit) {
+        setData(flyoutMessage)
+        scheduler.runAfterLayout(afterLayout)
+    }
+
     private fun setData(flyoutMessage: BubbleBarFlyoutMessage) {
         if (flyoutMessage.icon != null) {
             icon.visibility = VISIBLE
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
index 0eea741..50bb9bc 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
@@ -115,11 +115,13 @@
     fun hideFlyout_removedFromContainer() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+            assertThat(flyoutController.hasFlyout()).isTrue()
             assertThat(flyoutContainer.childCount).isEqualTo(1)
             flyoutController.collapseFlyout {}
             animatorTestRule.advanceTimeBy(300)
         }
         assertThat(flyoutContainer.childCount).isEqualTo(0)
+        assertThat(flyoutController.hasFlyout()).isFalse()
     }
 
     @Test
@@ -189,6 +191,61 @@
         assertThat(flyoutCallbacks.flyoutClicked).isTrue()
     }
 
+    @Test
+    fun updateFlyoutWhileExpanding() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+            assertThat(flyoutController.hasFlyout()).isTrue()
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("message")
+            // advance the animation about halfway
+            animatorTestRule.advanceTimeBy(100)
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+
+        val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            // set negative translation to verify that the top boundary extends as a result of
+            // updating while expanding
+            flyout.translationY = -50f
+            flyoutController.updateFlyoutWhileExpanding(newFlyoutMessage)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("new message")
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+    }
+
+    @Test
+    fun updateFlyoutFullyExpanded() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+            animatorTestRule.advanceTimeBy(300)
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+
+        val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            // set negative translation to verify that the top boundary extends as a result of
+            // updating while fully expanded
+            flyout.translationY = -50f
+            flyoutController.updateFlyoutFullyExpanded(newFlyoutMessage) {}
+
+            // advance the timer so that the fade out animation plays
+            animatorTestRule.advanceTimeBy(250)
+            assertThat(flyout.alpha).isEqualTo(0)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("new message")
+
+            // advance the timer so that the fade in animation plays
+            animatorTestRule.advanceTimeBy(250)
+            assertThat(flyout.alpha).isEqualTo(1)
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+    }
+
     class FakeFlyoutCallbacks : FlyoutCallbacks {
 
         var topBoundaryExtendedSpace = 0