Merge "Handle taps on the animating bubble" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 6163dad..95c4e25 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -153,6 +153,16 @@
                 context.deviceProfile.widthPx,
                 windowLayoutParams.height
             )
+
+            // if there's an animating bubble add it to the touch region so that it's clickable
+            val animatingBubbleBounds =
+                controllers.bubbleControllers
+                    .getOrNull()
+                    ?.bubbleBarViewController
+                    ?.animatingBubbleBounds
+            if (animatingBubbleBounds != null) {
+                defaultTouchableRegion.op(animatingBubbleBounds, Region.Op.UNION)
+            }
         }
 
         // Pre-calculate insets for different providers across different rotations for this gravity
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index db069d5..308e4ce 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -104,6 +104,8 @@
      * updates the bounds and accounts for translation.
      */
     private final Rect mBubbleBarBounds = new Rect();
+    /** The bounds of the animating bubble in the coordinate space of the BubbleBarView. */
+    private final Rect mAnimatingBubbleBounds = new Rect();
     // The amount the bubbles overlap when they are stacked in the bubble bar
     private final float mIconOverlapAmount;
     // The spacing between the bubbles when bubble bar is expanded
@@ -460,6 +462,30 @@
         return mBubbleBarBounds;
     }
 
+    /** Returns the bounds of the animating bubble, or {@code null} if no bubble is animating. */
+    @Nullable
+    public Rect getAnimatingBubbleBounds() {
+        if (mIsAnimatingNewBubble) {
+            return mAnimatingBubbleBounds;
+        }
+        return null;
+    }
+
+    /**
+     * Updates the animating bubble bounds. This should be called when the bubble is fully animated
+     * in so that we can include it in taskbar touchable region.
+     *
+     * <p>The bounds are adjusted to the coordinate space of BubbleBarView so that it can be used
+     * by taskbar.
+     */
+    public void updateAnimatingBubbleBounds(int left, int top, int width, int height) {
+        Rect bubbleBarBounds = getBubbleBarBounds();
+        mAnimatingBubbleBounds.left = bubbleBarBounds.left + left;
+        mAnimatingBubbleBounds.top = bubbleBarBounds.top + top;
+        mAnimatingBubbleBounds.right = mAnimatingBubbleBounds.left + width;
+        mAnimatingBubbleBounds.bottom = mAnimatingBubbleBounds.top + height;
+    }
+
     /**
      * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
      * respectively. If the value is not in range of 0 to 1 it will be normalized.
@@ -852,10 +878,15 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (!mIsBarExpanded) {
+        if (!mIsBarExpanded && !mIsAnimatingNewBubble) {
             // When the bar is collapsed, all taps on it should expand it.
             return true;
         }
         return super.onInterceptTouchEvent(ev);
     }
+
+    /** Whether a new bubble is currently animating. */
+    public boolean isAnimatingNewBubble() {
+        return mIsAnimatingNewBubble;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0e62eaf..be9a94d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -138,6 +138,15 @@
         if (bubble == null) {
             Log.e(TAG, "bubble click listener, bubble was null");
         }
+
+        if (mBarView.isAnimatingNewBubble()) {
+            mBubbleBarViewAnimator.onBubbleClickedWhileAnimating();
+            mBubbleStashController.showBubbleBarImmediate();
+            setExpanded(true);
+            mBubbleBarController.showAndSelectBubble(bubble);
+            return;
+        }
+
         final String currentlySelected = mBubbleBarController.getSelectedBubbleKey();
         if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) {
             // Tapping the currently selected bubble while expanded collapses the view.
@@ -213,6 +222,12 @@
         return mBarView.getBubbleBarBounds();
     }
 
+    /** The bounds of the animating bubble, or {@code null} if no bubble is animating. */
+    @Nullable
+    public Rect getAnimatingBubbleBounds() {
+        return mBarView.getAnimatingBubbleBounds();
+    }
+
     /** The horizontal margin of the bubble bar from the edge of the screen. */
     public int getHorizontalMargin() {
         return mBarView.getHorizontalMargin();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 76d86de..bea0af8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -347,7 +347,7 @@
                 hotseatCellHeight - mUnstashedHeight) / 2;
     }
 
-    float getBubbleBarTranslationY() {
+    public float getBubbleBarTranslationY() {
         // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
         // Otherwise we're either showing in an app or in overview. In either case adjust it so
         // the bubble bar aligns with the taskbar.
@@ -374,4 +374,19 @@
     public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() {
         return mHandleViewController.getPhysicsAnimator();
     }
+
+    /** Notifies taskbar that it should update its touchable region. */
+    public void updateTaskbarTouchRegion() {
+        mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
+    }
+
+    /** Shows the bubble bar immediately without animation. */
+    public void showBubbleBarImmediate() {
+        mHandleViewController.setTranslationYForSwipe(0);
+        mIconTranslationYForStash.updateValue(getBubbleBarTranslationY());
+        mIconAlphaForStash.setValue(1);
+        mIconScaleForStash.updateValue(1);
+        mIsStashed = false;
+        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 2d8983f..da36944 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -35,6 +35,8 @@
     private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
 ) {
 
+    private var animatingBubble: AnimatingBubble? = null
+
     private companion object {
         /** The time to show the flyout. */
         const val FLYOUT_DELAY_MS: Long = 2500
@@ -54,26 +56,40 @@
         const val BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y = -20f
     }
 
+    /** Wrapper around the animating bubble with its show and hide animations. */
+    private data class AnimatingBubble(
+        val bubbleView: BubbleView,
+        val showAnimation: Runnable,
+        val hideAnimation: Runnable
+    )
+
     /** An interface for scheduling jobs. */
     interface Scheduler {
 
         /** Schedule the given [block] to run. */
-        fun post(block: () -> Unit)
+        fun post(block: Runnable)
 
         /** Schedule the given [block] to start with a delay of [delayMillis]. */
-        fun postDelayed(delayMillis: Long, block: () -> Unit)
+        fun postDelayed(delayMillis: Long, block: Runnable)
+
+        /** Cancel the given [block] if it hasn't started yet. */
+        fun cancel(block: Runnable)
     }
 
     /** A [Scheduler] that uses a Handler to run jobs. */
     private class HandlerScheduler(private val view: View) : Scheduler {
 
-        override fun post(block: () -> Unit) {
+        override fun post(block: Runnable) {
             view.post(block)
         }
 
-        override fun postDelayed(delayMillis: Long, block: () -> Unit) {
+        override fun postDelayed(delayMillis: Long, block: Runnable) {
             view.postDelayed(block, delayMillis)
         }
+
+        override fun cancel(block: Runnable) {
+            view.removeCallbacks(block)
+        }
     }
 
     private val springConfig =
@@ -91,6 +107,7 @@
         // and the second part hides it after a delay.
         val showAnimation = buildShowAnimation(bubbleView, b.key)
         val hideAnimation = buildHideAnimation(bubbleView)
+        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
@@ -113,7 +130,7 @@
     private fun buildShowAnimation(
         bubbleView: BubbleView,
         key: String,
-    ): () -> Unit = {
+    ) = Runnable {
         bubbleBarView.prepareForAnimatingBubbleWhileStashed(key)
         // calculate the initial translation x the bubble should have in order to align it with the
         // stash handle.
@@ -140,7 +157,7 @@
 
                     // map the path [0, BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y] to [0,1]
                     val fraction = ty / BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
-                    target.alpha = 1 - fraction / 2
+                    target.alpha = 1 - fraction
                 }
                 ty >= totalTranslationY -> {
                     // this is the second leg of the animation. the handle should be completely
@@ -173,6 +190,16 @@
                 }
             }
         }
+        animator.addEndListener { _, _, _, _, _, _, _ ->
+            // the bubble is now fully settled in. make it touchable
+            bubbleBarView.updateAnimatingBubbleBounds(
+                bubbleView.left,
+                bubbleView.top,
+                bubbleView.width,
+                bubbleView.height
+            )
+            bubbleStashController.updateTaskbarTouchRegion()
+        }
         animator.start()
     }
 
@@ -189,7 +216,7 @@
      * 1. In the second part the bubble is fully hidden and the handle animates in.
      * 1. The third part is the overshoot. The handle is made fully visible.
      */
-    private fun buildHideAnimation(bubbleView: BubbleView): () -> Unit = {
+    private fun buildHideAnimation(bubbleView: BubbleView) = Runnable {
         // this is the total distance that both the stashed handle and the bubble will be traveling
         val totalTranslationY =
             BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y + BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
@@ -230,6 +257,7 @@
             }
         }
         animator.addEndListener { _, _, _, _, _, _, _ ->
+            animatingBubble = null
             bubbleView.alpha = 0f
             bubbleView.translationY = 0f
             bubbleView.scaleY = 1f
@@ -237,9 +265,18 @@
                 bubbleBarView.alpha = 0f
             }
             bubbleBarView.onAnimatingBubbleCompleted()
+            bubbleStashController.updateTaskbarTouchRegion()
         }
         animator.start()
     }
+
+    /** Handles clicking on the animating bubble while the animation is still playing. */
+    fun onBubbleClickedWhileAnimating() {
+        val hideAnimation = animatingBubble?.hideAnimation ?: return
+        scheduler.cancel(hideAnimation)
+        bubbleBarView.onAnimatingBubbleCompleted()
+        animatingBubble = null
+    }
 }
 
 /** The X position in screen coordinates of the center of the bubble. */
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 d90e048..3d8484d 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
@@ -130,18 +130,90 @@
         assertThat(handle.translationY).isEqualTo(0)
     }
 
-    private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
+    @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)
 
-        var delayedBlock: (() -> Unit)? = null
-            private set
+            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)
 
-        override fun post(block: () -> Unit) {
-            block.invoke()
+            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)
+
+        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)
         }
 
-        override fun postDelayed(delayMillis: Long, block: () -> Unit) {
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY).isEqualTo(-70)
+        assertThat(overflowView.visibility).isEqualTo(INVISIBLE)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleView.alpha).isEqualTo(1)
+        assertThat(bubbleView.translationY).isEqualTo(-20)
+        assertThat(bubbleView.scaleY).isEqualTo(1)
+
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        animator.onBubbleClickedWhileAnimating()
+
+        assertThat(animatorScheduler.delayedBlock).isNull()
+        assertThat(overflowView.visibility).isEqualTo(VISIBLE)
+        assertThat(overflowView.alpha).isEqualTo(1)
+        assertThat(bubbleView.alpha).isEqualTo(1)
+        assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.background).isNotNull()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+    }
+
+    private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
+
+        var delayedBlock: Runnable? = null
+            private set
+
+        override fun post(block: Runnable) {
+            block.run()
+        }
+
+        override fun postDelayed(delayMillis: Long, block: Runnable) {
             check(delayedBlock == null) { "there is already a pending block waiting to run" }
             delayedBlock = block
         }
+
+        override fun cancel(block: Runnable) {
+            check(delayedBlock == block) { "the pending block does not match the canceled block" }
+            delayedBlock = null
+        }
     }
 }