Merge "Model hiding the footer in the NotificationListViewModel" into main
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index f792898..adcbbfb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -58,6 +58,7 @@
 
     private FooterViewButton mClearAllButton;
     private FooterViewButton mManageOrHistoryButton;
+    private boolean mShouldBeHidden;
     private boolean mShowHistory;
     // String cache, for performance reasons.
     // Reading them from a Resources object can be quite slow sometimes.
@@ -110,6 +111,20 @@
         setSecondaryVisible(visible, animate, onAnimationEnded);
     }
 
+    /** See {@link this#setShouldBeHidden} below. */
+    public boolean shouldBeHidden() {
+        return mShouldBeHidden;
+    }
+
+    /**
+     * Whether this view's visibility should be set to INVISIBLE. Note that this is different from
+     * the {@link StackScrollerDecorView#setVisible} method, which in turn handles visibility
+     * transitions between VISIBLE and GONE.
+     */
+    public void setShouldBeHidden(boolean hide) {
+        mShouldBeHidden = hide;
+    }
+
     @Override
     public void dump(PrintWriter pwOriginal, String[] args) {
         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index b42c07d..5eaccd9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -594,15 +594,16 @@
         );
         if (view instanceof FooterView) {
             if (FooterViewRefactor.isEnabled()) {
-                final float footerEnd = algorithmState.mCurrentExpandedYPosition
-                        + view.getIntrinsicHeight();
-                final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
-                // TODO(b/293167744): May be able to keep only noSpaceForFooter here if we add an
-                //  emission when clearAllNotifications is called, and then use that in the footer
-                //  visibility flow.
-                ((FooterView.FooterViewState) viewState).hideContent =
-                        noSpaceForFooter || (ambientState.isClearAllInProgress()
-                                && !hasNonClearableNotifs(algorithmState));
+                if (((FooterView) view).shouldBeHidden()) {
+                    viewState.hidden = true;
+                } else {
+                    final float footerEnd = algorithmState.mCurrentExpandedYPosition
+                            + view.getIntrinsicHeight();
+                    final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
+                    ((FooterView.FooterViewState) viewState).hideContent =
+                            noSpaceForFooter || (ambientState.isClearAllInProgress()
+                                    && !hasNonClearableNotifs(algorithmState));
+                }
 
             } else {
                 final boolean shadeClosed = !ambientState.isShadeExpanded();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 97cbbe8..18bb5119 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -193,13 +193,14 @@
                 },
             )
         launch {
-            viewModel.shouldShowFooterView.collect { animatedVisibility ->
+            viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
                 footerView.setVisible(
                     /* visible = */ animatedVisibility.value,
                     /* animate = */ animatedVisibility.isAnimating,
                 )
             }
         }
+        launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
         disposableHandle.awaitCancellationThenDispose()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index a6ca027..5a7433d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -31,7 +31,6 @@
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
 import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
 import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
-import com.android.systemui.util.kotlin.combine
 import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.ui.AnimatableEvent
 import com.android.systemui.util.ui.AnimatedValue
@@ -111,7 +110,32 @@
         }
     }
 
-    val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+    /**
+     * Whether the footer should not be visible for the user, even if it's present in the list (as
+     * per [shouldIncludeFooterView] below).
+     *
+     * This essentially corresponds to having the view set to INVISIBLE.
+     */
+    val shouldHideFooterView: Flow<Boolean> by lazy {
+        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            // When the shade is closed, the footer is still present in the list, but not visible.
+            // This prevents the footer from being shown when a HUN is present, while still allowing
+            // the footer to be counted as part of the shade for measurements.
+            shadeInteractor.shadeExpansion.map { it == 0f }.distinctUntilChanged()
+        }
+    }
+
+    /**
+     * Whether the footer should be part of the list or not, and whether the transition from one
+     * state to another should be animated. This essentially corresponds to transitioning the view
+     * visibility from VISIBLE to GONE and vice versa.
+     *
+     * Note that this value being true doesn't necessarily mean that the footer is visible. It could
+     * be hidden by another condition (see [shouldHideFooterView] above).
+     */
+    val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy {
         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
             flowOf(AnimatedValue.NotAnimating(false))
         } else {
@@ -120,34 +144,30 @@
                     userSetupInteractor.isUserSetUp,
                     notificationStackInteractor.isShowingOnLockscreen,
                     shadeInteractor.isQsFullscreen,
-                    remoteInputInteractor.isRemoteInputActive,
-                    shadeInteractor.shadeExpansion.map { it == 0f }.distinctUntilChanged(),
+                    remoteInputInteractor.isRemoteInputActive
                 ) {
                     hasNotifications,
                     isUserSetUp,
                     isShowingOnLockscreen,
                     qsFullScreen,
-                    isRemoteInputActive,
-                    isShadeClosed ->
+                    isRemoteInputActive ->
                     when {
-                        !hasNotifications -> VisibilityChange.HIDE_WITH_ANIMATION
+                        !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
                         // Hide the footer until the user setup is complete, to prevent access
                         // to settings (b/193149550).
-                        !isUserSetUp -> VisibilityChange.HIDE_WITH_ANIMATION
+                        !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
                         // Do not show the footer if the lockscreen is visible (incl. AOD),
                         // except if the shade is opened on top. See also b/219680200.
                         // Do not animate, as that makes the footer appear briefly when
                         // transitioning between the shade and keyguard.
-                        isShowingOnLockscreen -> VisibilityChange.HIDE_WITHOUT_ANIMATION
+                        isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
                         // Do not show the footer if quick settings are fully expanded (except
                         // for the foldable split shade view). See b/201427195 && b/222699879.
-                        qsFullScreen -> VisibilityChange.HIDE_WITH_ANIMATION
+                        qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
                         // Hide the footer if remote input is active (i.e. user is replying to a
                         // notification). See b/75984847.
-                        isRemoteInputActive -> VisibilityChange.HIDE_WITH_ANIMATION
-                        // Never show the footer if the shade is collapsed (e.g. when HUNing).
-                        isShadeClosed -> VisibilityChange.HIDE_WITHOUT_ANIMATION
-                        else -> VisibilityChange.SHOW_WITH_ANIMATION
+                        isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                        else -> VisibilityChange.APPEAR_WITH_ANIMATION
                     }
                 }
                 .flowOn(bgDispatcher)
@@ -180,9 +200,9 @@
     }
 
     enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
-        HIDE_WITHOUT_ANIMATION(visible = false, canAnimate = false),
-        HIDE_WITH_ANIMATION(visible = false, canAnimate = true),
-        SHOW_WITH_ANIMATION(visible = true, canAnimate = true)
+        DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
+        DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
+        APPEAR_WITH_ANIMATION(visible = true, canAnimate = true)
     }
 
     // TODO(b/308591475): This should be tracked separately by the empty shade.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 138e1fa..c308a98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -130,35 +130,35 @@
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_trueWhenNoNotifs() =
+    fun testShouldIncludeEmptyShadeView_trueWhenNoNotifs() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
             runCurrent()
 
             // THEN empty shade is visible
-            assertThat(shouldShow).isTrue()
+            assertThat(shouldInclude).isTrue()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_falseWhenNotifs() =
+    fun testShouldIncludeEmptyShadeView_falseWhenNotifs() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldShow).isFalse()
+            assertThat(shouldInclude).isFalse()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_falseWhenQsExpandedDefault() =
+    fun testShouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -167,13 +167,13 @@
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldShow).isFalse()
+            assertThat(shouldInclude).isFalse()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
+    fun testShouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -185,13 +185,13 @@
             runCurrent()
 
             // THEN empty shade is visible
-            assertThat(shouldShow).isTrue()
+            assertThat(shouldInclude).isTrue()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_trueWhenLockedShade() =
+    fun testShouldIncludeEmptyShadeView_trueWhenLockedShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -200,13 +200,13 @@
             runCurrent()
 
             // THEN empty shade is visible
-            assertThat(shouldShow).isTrue()
+            assertThat(shouldInclude).isTrue()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_falseWhenKeyguard() =
+    fun testShouldIncludeEmptyShadeView_falseWhenKeyguard() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -215,13 +215,13 @@
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldShow).isFalse()
+            assertThat(shouldInclude).isFalse()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_falseWhenStartingToSleep() =
+    fun testShouldIncludeEmptyShadeView_falseWhenStartingToSleep() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -232,7 +232,7 @@
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldShow).isFalse()
+            assertThat(shouldInclude).isFalse()
         }
 
     @Test
@@ -282,9 +282,9 @@
         }
 
     @Test
-    fun testShouldShowFooterView_trueWhenShade() =
+    fun testShouldIncludeFooterView_trueWhenShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -294,13 +294,13 @@
             runCurrent()
 
             // THEN footer is visible
-            assertThat(shouldShow?.value).isTrue()
+            assertThat(shouldInclude?.value).isTrue()
         }
 
     @Test
-    fun testShouldShowFooterView_trueWhenLockedShade() =
+    fun testShouldIncludeFooterView_trueWhenLockedShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -310,13 +310,13 @@
             runCurrent()
 
             // THEN footer is visible
-            assertThat(shouldShow?.value).isTrue()
+            assertThat(shouldInclude?.value).isTrue()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenKeyguard() =
+    fun testShouldIncludeFooterView_falseWhenKeyguard() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -325,13 +325,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenUserNotSetUp() =
+    fun testShouldIncludeFooterView_falseWhenUserNotSetUp() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -343,13 +343,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenStartingToSleep() =
+    fun testShouldIncludeFooterView_falseWhenStartingToSleep() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -361,13 +361,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenQsExpandedDefault() =
+    fun testShouldIncludeFooterView_falseWhenQsExpandedDefault() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -380,13 +380,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_trueWhenQsExpandedSplitShade() =
+    fun testShouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -401,13 +401,13 @@
             runCurrent()
 
             // THEN footer is visible
-            assertThat(shouldShow?.value).isTrue()
+            assertThat(shouldInclude?.value).isTrue()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenRemoteInputActive() =
+    fun testShouldIncludeFooterView_falseWhenRemoteInputActive() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -419,29 +419,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenShadeIsClosed() =
+    fun testShouldIncludeFooterView_animatesWhenShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
-
-            // WHEN has notifs
-            activeNotificationListRepository.setActiveNotifs(count = 2)
-            // AND shade is closed
-            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(0f)
-            runCurrent()
-
-            // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
-        }
-
-    @Test
-    fun testShouldShowFooterView_animatesWhenShade() =
-        testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -451,13 +435,13 @@
             runCurrent()
 
             // THEN footer visibility animates
-            assertThat(shouldShow?.isAnimating).isTrue()
+            assertThat(shouldInclude?.isAnimating).isTrue()
         }
 
     @Test
-    fun testShouldShowFooterView_notAnimatingOnKeyguard() =
+    fun testShouldIncludeFooterView_notAnimatingOnKeyguard() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -467,7 +451,35 @@
             runCurrent()
 
             // THEN footer visibility does not animate
-            assertThat(shouldShow?.isAnimating).isFalse()
+            assertThat(shouldInclude?.isAnimating).isFalse()
+        }
+
+    @Test
+    fun testShouldHideFooterView_trueWhenShadeIsClosed() =
+        testScope.runTest {
+            val shouldHide by collectLastValue(underTest.shouldHideFooterView)
+
+            // WHEN shade is closed
+            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            fakeShadeRepository.setLegacyShadeExpansion(0f)
+            runCurrent()
+
+            // THEN footer is hidden
+            assertThat(shouldHide).isTrue()
+        }
+
+    @Test
+    fun testShouldHideFooterView_falseWhenShadeIsOpen() =
+        testScope.runTest {
+            val shouldHide by collectLastValue(underTest.shouldHideFooterView)
+
+            // WHEN shade is open
+            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            runCurrent()
+
+            // THEN footer is hidden
+            assertThat(shouldHide).isFalse()
         }
 
     @Test