Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit be87f2e4 authored by Ioana Alexandru's avatar Ioana Alexandru Committed by Android (Google) Code Review
Browse files

Merge "Model hiding the footer in the NotificationListViewModel" into main

parents fdbf8c14 feee784d
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -58,6 +58,7 @@ public class FooterView extends StackScrollerDecorView {

    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 @@ public class FooterView extends StackScrollerDecorView {
        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);
+10 −9
Original line number Diff line number Diff line
@@ -594,15 +594,16 @@ public class StackScrollAlgorithm {
        );
        if (view instanceof FooterView) {
            if (FooterViewRefactor.isEnabled()) {
                if (((FooterView) view).shouldBeHidden()) {
                    viewState.hidden = true;
                } else {
                    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));
                }

            } else {
                final boolean shadeClosed = !ambientState.isShadeExpanded();
+2 −1
Original line number Diff line number Diff line
@@ -193,13 +193,14 @@ constructor(
                },
            )
        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()
    }

+37 −17
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@ import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.Notificati
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 @@ constructor(
        }
    }

    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 @@ constructor(
                    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 @@ constructor(
    }

    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.
+77 −65
Original line number Diff line number Diff line
@@ -130,35 +130,35 @@ class NotificationListViewModelTest : SysuiTestCase() {
        }

    @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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            runCurrent()

            // THEN empty shade is not visible
            assertThat(shouldShow).isFalse()
            assertThat(shouldInclude).isFalse()
        }

    @Test
@@ -282,9 +282,9 @@ class NotificationListViewModelTest : SysuiTestCase() {
        }

    @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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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,55 +419,67 @@ class NotificationListViewModelTest : SysuiTestCase() {
            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)
            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)

            // WHEN has notifs
            activeNotificationListRepository.setActiveNotifs(count = 2)
            // AND shade is closed
            // AND shade is open and fully expanded
            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
            fakeShadeRepository.setLegacyShadeExpansion(0f)
            fakeShadeRepository.setLegacyShadeExpansion(1f)
            runCurrent()

            // THEN footer is not visible
            assertThat(shouldShow?.value).isFalse()
            // THEN footer visibility animates
            assertThat(shouldInclude?.isAnimating).isTrue()
        }

    @Test
    fun testShouldShowFooterView_animatesWhenShade() =
    fun testShouldIncludeFooterView_notAnimatingOnKeyguard() =
        testScope.runTest {
            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)

            // WHEN has notifs
            activeNotificationListRepository.setActiveNotifs(count = 2)
            // AND shade is open and fully expanded
            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
            // AND we are on the keyguard
            fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
            fakeShadeRepository.setLegacyShadeExpansion(1f)
            runCurrent()

            // THEN footer visibility animates
            assertThat(shouldShow?.isAnimating).isTrue()
            // THEN footer visibility does not animate
            assertThat(shouldInclude?.isAnimating).isFalse()
        }

    @Test
    fun testShouldShowFooterView_notAnimatingOnKeyguard() =
    fun testShouldHideFooterView_trueWhenShadeIsClosed() =
        testScope.runTest {
            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
            val shouldHide by collectLastValue(underTest.shouldHideFooterView)

            // WHEN has notifs
            activeNotificationListRepository.setActiveNotifs(count = 2)
            // AND we are on the keyguard
            fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
            // 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 visibility does not animate
            assertThat(shouldShow?.isAnimating).isFalse()
            // THEN footer is hidden
            assertThat(shouldHide).isFalse()
        }

    @Test