Merge "Debounce HeadsUpNotificationInteractor.isHeadsUpOrAnimatingAway" into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index fd1b213..b5e47d1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -85,6 +85,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -181,6 +182,7 @@
 
             kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
                 buildNotificationRows(isPinned = false)
+            advanceTimeBy(50L) // account for HeadsUpNotificationInteractor debounce
             assertThat(isVisible).isFalse()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index 8b4265f..5ef3485 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -23,6 +23,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -279,6 +280,64 @@
         }
 
     @Test
+    fun isHeadsUpOrAnimatingAway_falseOnStart() =
+        testScope.runTest {
+            val isHeadsUpOrAnimatingAway by collectLastValue(underTest.isHeadsUpOrAnimatingAway)
+
+            runCurrent()
+
+            assertThat(isHeadsUpOrAnimatingAway).isFalse()
+        }
+
+    @Test
+    fun isHeadsUpOrAnimatingAway_hasPinnedRows() =
+        testScope.runTest {
+            val isHeadsUpOrAnimatingAway by collectLastValue(underTest.isHeadsUpOrAnimatingAway)
+
+            // WHEN a row is pinned
+            headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
+            runCurrent()
+
+            assertThat(isHeadsUpOrAnimatingAway).isTrue()
+        }
+
+    @Test
+    fun isHeadsUpOrAnimatingAway_headsUpAnimatingAway() =
+        testScope.runTest {
+            val isHeadsUpOrAnimatingAway by collectLastValue(underTest.isHeadsUpOrAnimatingAway)
+
+            // WHEN the last row is animating away
+            headsUpRepository.setHeadsUpAnimatingAway(true)
+            runCurrent()
+
+            assertThat(isHeadsUpOrAnimatingAway).isTrue()
+        }
+
+    @Test
+    fun isHeadsUpOrAnimatingAway_headsUpAnimatingAwayDebounced() =
+        testScope.runTest {
+            val values by collectValues(underTest.isHeadsUpOrAnimatingAway)
+
+            // GIVEN a row is pinned
+            headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
+            runCurrent()
+            assertThat(values.size).isEqualTo(2)
+            assertThat(values.first()).isFalse() // initial value
+            assertThat(values.last()).isTrue()
+
+            // WHEN the last row is removed
+            headsUpRepository.setNotifications(emptyList())
+            runCurrent()
+            // AND starts to animate away
+            headsUpRepository.setHeadsUpAnimatingAway(true)
+            runCurrent()
+
+            // THEN isHeadsUpOrAnimatingAway remained true
+            assertThat(values.size).isEqualTo(2)
+            assertThat(values.last()).isTrue()
+        }
+
+    @Test
     fun showHeadsUpStatusBar_true() =
         testScope.runTest {
             val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index cdab108..eebbb13 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalCoroutinesApi::class)
+@file:OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
@@ -27,11 +27,15 @@
 import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
 
 class HeadsUpNotificationInteractor
 @Inject
@@ -73,10 +77,21 @@
 
     val isHeadsUpOrAnimatingAway: Flow<Boolean> =
         combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
-            hasPinnedRows,
-            animatingAway ->
-            hasPinnedRows || animatingAway
-        }
+                hasPinnedRows,
+                animatingAway ->
+                hasPinnedRows || animatingAway
+            }
+            .debounce { isHeadsUpOrAnimatingAway ->
+                if (isHeadsUpOrAnimatingAway) {
+                    0
+                } else {
+                    // When the last pinned entry is removed from the [HeadsUpRepository],
+                    // there might be a delay before the View starts animating.
+                    50L
+                }
+            }
+            .onStart { emit(false) } // emit false, so we don't wait for the initial update
+            .distinctUntilChanged()
 
     private val canShowHeadsUp: Flow<Boolean> =
         combine(