Merge "Animate the bubble bar for the first bubble" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 66e5302..8c83508 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -403,9 +403,6 @@
         }
         if (bubbleToSelect != null) {
             setSelectedBubbleInternal(bubbleToSelect);
-            if (previouslySelectedBubble == null) {
-                mBubbleStashController.animateToInitialState(update.expanded);
-            }
         }
         if (update.shouldShowEducation) {
             mBubbleBarViewController.prepareToShowEducation();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0b74e15..ac02f1f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -406,10 +406,21 @@
                 return;
             }
 
+            if (!(b instanceof BubbleBarBubble bubble)) {
+                return;
+            }
+
             boolean isInApp = mTaskbarStashController.isInApp();
+            // if this is the first bubble, animate to the initial state. one bubble is the overflow
+            // so check for at most 2 children.
+            if (mBarView.getChildCount() <= 2) {
+                mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
+                return;
+            }
+
             // only animate the new bubble if we're in an app and not auto expanding
-            if (b instanceof BubbleBarBubble && isInApp && !isExpanding && !isExpanded()) {
-                mBubbleBarViewAnimator.animateBubbleInForStashed((BubbleBarBubble) b);
+            if (isInApp && !isExpanding && !isExpanded()) {
+                mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
             }
         } else {
             Log.w(TAG, "addBubble, bubble was null!");
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 4b3416c..d0462aa 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -123,21 +123,17 @@
     }
 
     /**
-     * Animates the bubble bar and handle to their initial state, transitioning from the state where
-     * both views are invisible. Called when the first bubble is added or when the device is
+     * Animates the handle (or bubble bar depending on state) to be visible after the device is
      * unlocked.
      *
      * <p>Normally either the bubble bar or the handle is visible,
      * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition
      * between these two states. But the transition from the state where both the bar and handle
      * are invisible is slightly different.
-     *
-     * <p>The initial state will depend on the current state of the device, i.e. overview, home etc
-     * and whether bubbles are requested to be expanded.
      */
-    public void animateToInitialState(boolean expanding) {
+    private void animateAfterUnlock() {
         AnimatorSet animatorSet = new AnimatorSet();
-        if (expanding || mBubblesShowingOnHome || mBubblesShowingOnOverview) {
+        if (mBubblesShowingOnHome || mBubblesShowingOnOverview) {
             mIsStashed = false;
             animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
                     mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
@@ -217,7 +213,7 @@
         if (isSysuiLocked != mIsSysuiLocked) {
             mIsSysuiLocked = isSysuiLocked;
             if (!mIsSysuiLocked && mBarViewController.hasBubbles()) {
-                animateToInitialState(false /* expanding */);
+                animateAfterUnlock();
             }
         }
     }
@@ -453,4 +449,9 @@
         mIsStashed = isStashed;
         onIsStashedChanged();
     }
+
+    /** Set the translation Y for the stashed handle. */
+    public void setHandleTranslationY(float ty) {
+        mHandleViewController.setTranslationYForSwipe(ty);
+    }
 }
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 66521c1..be935d8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -93,15 +93,15 @@
         if (animator.isRunning()) animator.cancel()
         // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
         // and the second part hides it after a delay.
-        val showAnimation = buildShowAnimation()
-        val hideAnimation = buildHideAnimation()
+        val showAnimation = buildHandleToBubbleBarAnimation()
+        val hideAnimation = buildBubbleBarToHandleAnimation()
         animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
 
     /**
-     * Returns a [Runnable] that starts the animation that shows the new or updated bubble.
+     * Returns a [Runnable] that starts the animation that morphs the handle to the bubble bar.
      *
      * Visually, the animation is divided into 2 parts. The stash handle starts animating up and
      * fading out and then the bubble bar starts animating up and fading in.
@@ -114,7 +114,7 @@
      * 3. The third part is the overshoot of the spring animation, where we make the bubble fully
      *    visible which helps avoiding further updates when we re-enter the second part.
      */
-    private fun buildShowAnimation() = Runnable {
+    private fun buildHandleToBubbleBarAnimation() = Runnable {
         // prepare the bubble bar for the animation
         bubbleBarView.onAnimatingBubbleStarted()
         bubbleBarView.visibility = VISIBLE
@@ -197,7 +197,8 @@
     }
 
     /**
-     * Returns a [Runnable] that starts the animation that hides the bubble bar.
+     * Returns a [Runnable] that starts the animation that hides the bubble bar and morphs it into
+     * the stashed handle.
      *
      * Similarly to the show animation, this is visually divided into 2 parts. We first animate the
      * bubble bar out, and then animate the stash handle in. At the end of the animation we reset
@@ -209,13 +210,14 @@
      * 2. In the second part the bubble bar is fully hidden and the handle animates in.
      * 3. The third part is the overshoot. The handle is made fully visible.
      */
-    private fun buildHideAnimation() = Runnable {
+    private fun buildBubbleBarToHandleAnimation() = Runnable {
         if (animatingBubble == null) return@Runnable
         val offset = bubbleStashController.diffBetweenHandleAndBarCenters
         val stashedHandleTranslationY =
             bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
         // this is the total distance that both the stashed handle and the bar will be traveling
         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
+        bubbleStashController.setHandleTranslationY(totalTranslationY)
         val animator = bubbleStashController.stashedHandlePhysicsAnimator
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, 0f)
@@ -259,6 +261,50 @@
         animator.start()
     }
 
+    /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
+    fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
+        val bubbleView = b.view
+        val animator = PhysicsAnimator.getInstance(bubbleView)
+        if (animator.isRunning()) animator.cancel()
+        // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
+        // and the second part hides it after a delay if we are in an app.
+        val showAnimation = buildBubbleBarBounceAnimation()
+        val hideAnimation =
+            if (isInApp && !isExpanding) {
+                buildBubbleBarToHandleAnimation()
+            } else {
+                // in this case the bubble bar remains visible so not much to do. once we implement
+                // the flyout we'll update this runnable to hide it.
+                Runnable {
+                    animatingBubble = null
+                    bubbleStashController.showBubbleBarImmediate()
+                    bubbleBarView.onAnimatingBubbleCompleted()
+                }
+            }
+        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        scheduler.post(showAnimation)
+        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+    }
+
+    private fun buildBubbleBarBounceAnimation() = Runnable {
+        // prepare the bubble bar for the animation
+        bubbleBarView.onAnimatingBubbleStarted()
+        bubbleBarView.translationY = bubbleBarView.height.toFloat()
+        bubbleBarView.visibility = VISIBLE
+        bubbleBarView.alpha = 1f
+        bubbleBarView.scaleX = 1f
+        bubbleBarView.scaleY = 1f
+
+        val animator = PhysicsAnimator.getInstance(bubbleBarView)
+        animator.setDefaultSpringConfig(springConfig)
+        animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
+        animator.addEndListener { _, _, _, _, _, _, _ ->
+            // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
+            bubbleStashController.updateTaskbarTouchRegion()
+        }
+        animator.start()
+    }
+
     /** Handles clicking on the animating bubble while the animation is still playing. */
     fun onBubbleClickedWhileAnimating() {
         val hideAnimation = animatingBubble?.hideAnimation ?: return
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 7065075..2bcfa3f 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
@@ -264,6 +264,120 @@
         assertThat(animatorScheduler.delayedBlock).isNull()
     }
 
+    @Test
+    fun animateToInitialState_inApp() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = true, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(barAnimator.isRunning()).isFalse()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(handle.alpha).isEqualTo(1)
+
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
+    @Test
+    fun animateToInitialState_inApp_autoExpanding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = true, isExpanding = true)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(barAnimator.isRunning()).isFalse()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
+    @Test
+    fun animateToInitialState_inHome() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(barAnimator.isRunning()).isFalse()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -322,3 +436,4 @@
 private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f
 private const val HANDLE_TRANSLATION = -30f
 private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f
+private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f