Merge "Handle taps on bubble bar flyout" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 058dd07..4a6b6d4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -145,6 +145,7 @@
             // if bubble bar is visible or animating new bubble, add bar bounds to the touch region
             if (isBubbleBarVisible || isAnimatingNewBubble) {
                 defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds)
+                defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.flyoutBounds)
             }
         }
         if (
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 76d3606..717ee95 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator;
 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController;
 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner;
+import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
@@ -146,7 +147,7 @@
         mTaskbarStashController = controllers.taskbarStashController;
         mTaskbarInsetsController = controllers.taskbarInsetsController;
         mBubbleBarFlyoutController = new BubbleBarFlyoutController(
-                mBubbleBarContainer, createFlyoutPositioner(), createFlyoutTopBoundaryListener());
+                mBubbleBarContainer, createFlyoutPositioner(), createFlyoutCallbacks());
         mBubbleBarViewAnimator = new BubbleBarViewAnimator(
                 mBarView, mBubbleStashController, mBubbleBarFlyoutController,
                 mBubbleBarController::showExpandedView);
@@ -175,7 +176,9 @@
 
             @Override
             public void onBubbleBarTouched() {
-                BubbleBarViewController.this.onBubbleBarTouched();
+                if (isAnimatingNewBubble()) {
+                    interruptAnimationForTouch();
+                }
             }
 
             @Override
@@ -273,8 +276,8 @@
         };
     }
 
-    private BubbleBarFlyoutController.TopBoundaryListener createFlyoutTopBoundaryListener() {
-        return new BubbleBarFlyoutController.TopBoundaryListener() {
+    private FlyoutCallbacks createFlyoutCallbacks() {
+        return new FlyoutCallbacks() {
             @Override
             public void extendTopBoundary(int space) {
                 int defaultSize = mActivity.getDefaultTaskbarWindowSize();
@@ -285,6 +288,12 @@
             public void resetTopBoundary() {
                 mActivity.setTaskbarWindowSize(mActivity.getDefaultTaskbarWindowSize());
             }
+
+            @Override
+            public void flyoutClicked() {
+                interruptAnimationForTouch();
+                expandBubbleBar();
+            }
         };
     }
 
@@ -304,12 +313,10 @@
         }
     }
 
-    private void onBubbleBarTouched() {
-        if (isAnimatingNewBubble()) {
-            mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating();
-            mBubbleStashController.onNewBubbleAnimationInterrupted(false,
-                    mBarView.getTranslationY());
-        }
+    /** Interrupts the running animation for a touch event on the bubble bar or flyout. */
+    private void interruptAnimationForTouch() {
+        mBubbleBarViewAnimator.interruptForTouch();
+        mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
     }
 
     private void expandBubbleBar() {
@@ -463,6 +470,12 @@
         return mBarView.getBubbleBarBounds();
     }
 
+    /** Returns the bounds of the flyout view if it exists, or {@code null} otherwise. */
+    @Nullable
+    public Rect getFlyoutBounds() {
+        return mBubbleBarFlyoutController.getFlyoutBounds();
+    }
+
     /** Checks that bubble bar is visible and that the motion event is within bounds. */
     public boolean isEventOverBubbleBar(MotionEvent event) {
         if (!isBubbleBarVisible()) return false;
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 8a52ca9..78e5dbd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -471,8 +471,8 @@
         bubbleStashController.updateTaskbarTouchRegion()
     }
 
-    /** Handles touching the animating bubble bar. */
-    fun onBubbleBarTouchedWhileAnimating() {
+    /** Interrupts the animation due to touching the bubble bar or flyout. */
+    fun interruptForTouch() {
         PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
         bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
         cancelFlyout()
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 d6400bb..fdbbbb0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -16,13 +16,13 @@
 
 package com.android.launcher3.taskbar.bubbles.flyout
 
+import android.graphics.Rect
 import android.view.Gravity
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.core.animation.ValueAnimator
 import com.android.launcher3.R
 import com.android.systemui.util.addListener
-import com.android.systemui.util.doOnEnd
 
 /** Creates and manages the visibility of the [BubbleBarFlyoutView]. */
 class BubbleBarFlyoutController
@@ -30,7 +30,7 @@
 constructor(
     private val container: FrameLayout,
     private val positioner: BubbleBarFlyoutPositioner,
-    private val topBoundaryListener: TopBoundaryListener,
+    private val callbacks: FlyoutCallbacks,
     private val flyoutScheduler: FlyoutScheduler = HandlerScheduler(container),
 ) {
 
@@ -47,6 +47,15 @@
         FADE,
     }
 
+    /** The bounds of the flyout. */
+    val flyoutBounds: Rect?
+        get() {
+            val flyout = this.flyout ?: return null
+            val rect = Rect(flyout.bounds)
+            rect.offset(0, flyout.translationY.toInt())
+            return rect
+        }
+
     fun setUpAndShowFlyout(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
         flyout?.let(container::removeView)
         val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler)
@@ -72,9 +81,12 @@
                 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) topBoundaryListener.extendTopBoundary(space = -flyoutTop.toInt())
+                if (flyoutTop < 0) callbacks.extendTopBoundary(space = -flyoutTop.toInt())
             },
-            onEnd = { onEnd() },
+            onEnd = {
+                onEnd()
+                flyout.setOnClickListener { callbacks.flyoutClicked() }
+            },
         )
         flyout.showFromCollapsed(message) { animator.start() }
         this.flyout = flyout
@@ -100,21 +112,15 @@
                     flyout.updateExpansionProgress(animator.animatedValue as Float)
                 }
         }
-        animator.doOnEnd {
-            container.removeView(flyout)
-            this@BubbleBarFlyoutController.flyout = null
-            topBoundaryListener.resetTopBoundary()
-            endAction()
-        }
+        animator.addListener(
+            onStart = { flyout.setOnClickListener(null) },
+            onEnd = {
+                container.removeView(flyout)
+                this@BubbleBarFlyoutController.flyout = null
+                callbacks.resetTopBoundary()
+                endAction()
+            },
+        )
         animator.start()
     }
-
-    /** Notifies when the top boundary of the flyout view changes. */
-    interface TopBoundaryListener {
-        /** Requests to extend the top boundary of the parent to fully include the flyout. */
-        fun extendTopBoundary(space: Int)
-
-        /** Resets the top boundary of the parent. */
-        fun resetTopBoundary()
-    }
 }
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 6903c87..bb8a392 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -24,6 +24,7 @@
 import android.graphics.Paint
 import android.graphics.Path
 import android.graphics.PointF
+import android.graphics.Rect
 import android.graphics.RectF
 import android.view.LayoutInflater
 import android.view.View
@@ -138,6 +139,9 @@
      */
     private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
 
+    /** The bounds of the flyout relative to the parent view. */
+    val bounds = Rect()
+
     init {
         LayoutInflater.from(context).inflate(R.layout.bubblebar_flyout, this, true)
         id = R.id.bubble_bar_flyout_view
@@ -174,6 +178,14 @@
         applyConfigurationColors(resources.configuration)
     }
 
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        bounds.left = left
+        bounds.top = top
+        bounds.right = right
+        bounds.bottom = bottom
+    }
+
     /** Sets the data for the flyout and starts playing the expand animation. */
     fun showFromCollapsed(flyoutMessage: BubbleBarFlyoutMessage, expandAnimation: () -> Unit) {
         icon.alpha = 0f
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
new file mode 100644
index 0000000..e2f010a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.flyout
+
+/** Callbacks that the flyout uses to notify of events. */
+interface FlyoutCallbacks {
+    /** Requests to extend the top boundary of the parent to fully include the flyout. */
+    fun extendTopBoundary(space: Int)
+
+    /** Resets the top boundary of the parent. */
+    fun resetTopBoundary()
+
+    /** The flyout was clicked. */
+    fun flyoutClicked()
+}
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 b37048a..b0d01d3 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
@@ -40,6 +40,7 @@
 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner
+import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks
 import com.android.launcher3.taskbar.bubbles.flyout.FlyoutScheduler
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
 import com.android.wm.shell.shared.animation.PhysicsAnimator
@@ -179,9 +180,7 @@
         // verify the hide bubble animation is pending
         assertThat(animatorScheduler.delayedBlock).isNotNull()
 
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.onBubbleBarTouchedWhileAnimating()
-        }
+        InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForTouch() }
 
         waitForFlyoutToHide()
 
@@ -992,18 +991,20 @@
                 override val collapsedElevation = 1f
                 override val distanceToRevealTriangle = 10f
             }
-        val topBoundaryListener =
-            object : BubbleBarFlyoutController.TopBoundaryListener {
+        val flyoutCallbacks =
+            object : FlyoutCallbacks {
                 override fun extendTopBoundary(space: Int) {}
 
                 override fun resetTopBoundary() {}
+
+                override fun flyoutClicked() {}
             }
         val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
         flyoutController =
             BubbleBarFlyoutController(
                 flyoutContainer,
                 flyoutPositioner,
-                topBoundaryListener,
+                flyoutCallbacks,
                 flyoutScheduler,
             )
     }
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 527bdaa..0eea741 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
@@ -44,7 +44,7 @@
 
     private lateinit var flyoutController: BubbleBarFlyoutController
     private lateinit var flyoutContainer: FrameLayout
-    private lateinit var topBoundaryListener: FakeTopBoundaryListener
+    private lateinit var flyoutCallbacks: FakeFlyoutCallbacks
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message")
     private var onLeft = true
@@ -67,15 +67,10 @@
                 override val collapsedElevation = 1f
                 override val distanceToRevealTriangle = 50f
             }
-        topBoundaryListener = FakeTopBoundaryListener()
+        flyoutCallbacks = FakeFlyoutCallbacks()
         val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
         flyoutController =
-            BubbleBarFlyoutController(
-                flyoutContainer,
-                positioner,
-                topBoundaryListener,
-                flyoutScheduler,
-            )
+            BubbleBarFlyoutController(flyoutContainer, positioner, flyoutCallbacks, flyoutScheduler)
     }
 
     @Test
@@ -140,7 +135,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animatorTestRule.advanceTimeBy(300)
         }
-        assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(50)
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
     }
 
     @Test
@@ -153,7 +148,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animatorTestRule.advanceTimeBy(300)
         }
-        assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(0)
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(0)
     }
 
     @Test
@@ -164,7 +159,7 @@
             flyoutController.collapseFlyout {}
             animatorTestRule.advanceTimeBy(300)
         }
-        assertThat(topBoundaryListener.topBoundaryReset).isTrue()
+        assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
     }
 
     @Test
@@ -178,13 +173,27 @@
             animatorTestRule.advanceTimeBy(300)
             assertThat(flyoutView.alpha).isEqualTo(0f)
         }
-        assertThat(topBoundaryListener.topBoundaryReset).isTrue()
+        assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
     }
 
-    class FakeTopBoundaryListener : BubbleBarFlyoutController.TopBoundaryListener {
+    @Test
+    fun clickFlyout_notifiesCallback() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            assertThat(flyoutView.alpha).isEqualTo(1f)
+            animatorTestRule.advanceTimeBy(300)
+            flyoutView.performClick()
+        }
+        assertThat(flyoutCallbacks.flyoutClicked).isTrue()
+    }
+
+    class FakeFlyoutCallbacks : FlyoutCallbacks {
 
         var topBoundaryExtendedSpace = 0
         var topBoundaryReset = false
+        var flyoutClicked = false
 
         override fun extendTopBoundary(space: Int) {
             topBoundaryExtendedSpace = space
@@ -193,5 +202,9 @@
         override fun resetTopBoundary() {
             topBoundaryReset = true
         }
+
+        override fun flyoutClicked() {
+            flyoutClicked = true
+        }
     }
 }