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

Commit 58d531db authored by burakov's avatar burakov Committed by Danny Burakov
Browse files

[Dual Shade] Show the notifications shade footer when the shade is empty

BONUS: Update `ShadeTestUtil` to work correctly with Dual Shade as well.

Fix: 404798547
Test: Added unit tests.
Test: Manually by opening the notifications shade with and without
 notifications present, and observing that the footer is shown in both
 cases.
Flag: com.android.systemui.scene_container
Change-Id: I493183bc46b975871ea728f83bd8917017990b40
parent 709cff4f
Loading
Loading
Loading
Loading
+58 −0
Original line number Diff line number Diff line
@@ -32,6 +32,8 @@ import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.enableSingleShade
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
@@ -46,6 +48,7 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.ui.isAnimating
import com.android.systemui.util.ui.value
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -56,6 +59,7 @@ import org.junit.runner.RunWith
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -451,6 +455,60 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
            assertThat(shouldShow?.value).isFalse()
        }

    @Test
    @EnableSceneContainer
    fun shouldShowFooterView_dualShade_trueWhenShadeIsExpanded() =
        testScope.runTest {
            kosmos.enableDualShade()
            runCurrent()

            val shouldShow by collectLastValue(underTest.shouldShowFooterView)

            // WHEN shade is open
            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
            shadeTestUtil.setShadeExpansion(1f)
            runCurrent()

            // THEN footer is shown
            assertThat(shouldShow?.value).isTrue()
        }

    @Test
    @EnableSceneContainer
    fun shouldShowFooterView_singleShade_falseWhenNoNotifs() =
        testScope.runTest {
            kosmos.enableSingleShade()
            val shouldShow by collectLastValue(underTest.shouldShowFooterView)

            // WHEN shade is open, has no notifs
            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
            shadeTestUtil.setShadeExpansion(1f)
            activeNotificationListRepository.setActiveNotifs(count = 0)
            runCurrent()

            // THEN footer is hidden
            assertThat(shouldShow?.value).isFalse()
        }

    @Test
    @EnableSceneContainer
    fun shouldShowFooterView_dualShade_trueWhenNoNotifs() =
        testScope.runTest {
            kosmos.enableDualShade()
            runCurrent()

            val shouldShow by collectLastValue(underTest.shouldShowFooterView)

            // WHEN shade is open, has no notifs
            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
            shadeTestUtil.setShadeExpansion(1f)
            activeNotificationListRepository.setActiveNotifs(count = 0)
            runCurrent()

            // THEN footer is shown
            assertThat(shouldShow?.value).isTrue()
        }

    @Test
    @DisableSceneContainer
    fun shouldHideFooterView_trueWhenShadeIsClosed() =
+9 −2
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel
import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
@@ -67,6 +69,7 @@ constructor(
    private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
    remoteInputInteractor: RemoteInputInteractor,
    shadeInteractor: ShadeInteractor,
    shadeModeInteractor: ShadeModeInteractor,
    userSetupInteractor: UserSetupInteractor,
    @Background bgDispatcher: CoroutineDispatcher,
    dumpManager: DumpManager,
@@ -243,6 +246,7 @@ constructor(
            flowOf(AnimatedValue.NotAnimating(false))
        } else {
            combine(
                    shadeModeInteractor.shadeMode,
                    activeNotificationsInteractor.areAnyNotificationsPresent,
                    userSetupInteractor.isUserSetUp,
                    notificationStackInteractor.isShowingOnLockscreen,
@@ -250,6 +254,7 @@ constructor(
                    remoteInputInteractor.isRemoteInputActive,
                    shadeInteractor.shadeExpansion.map { it < 0.5f }.distinctUntilChanged(),
                ) {
                    shadeMode,
                    hasNotifications,
                    isUserSetUp,
                    isShowingOnLockscreen,
@@ -257,14 +262,16 @@ constructor(
                    isRemoteInputActive,
                    shadeLessThanHalfwayExpanded ->
                    when {
                        !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
                        // Hide the footer when there are no notifications, unless it's Dual Shade.
                        shadeMode != ShadeMode.Dual && !hasNotifications ->
                            VisibilityChange.DISAPPEAR_WITH_ANIMATION
                        // Hide the footer until the user setup is complete, to prevent access
                        // to settings (b/193149550).
                        !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.
                        // transitioning between the shade and lockscreen.
                        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.
+119 −39
Original line number Diff line number Diff line
@@ -14,18 +14,25 @@
 * limitations under the License.
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

package com.android.systemui.shade

import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestableContext
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.FakeShadeRepository
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
@@ -208,42 +215,51 @@ class ShadeTestUtilSceneImpl(
    val shadeRepository: ShadeRepository,
    val context: SysuiTestableContext,
    val shadeInteractor: ShadeInteractor,
    val shadeModeInteractor: ShadeModeInteractor,
) : ShadeTestUtilDelegate {
    val isUserInputOngoing = MutableStateFlow(true)

    private val notificationsShade: ContentKey
        get() = if (shadeModeInteractor.isDualShade) Overlays.NotificationsShade else Scenes.Shade

    private val quickSettingsShade: ContentKey
        get() =
            if (shadeModeInteractor.isDualShade) {
                Overlays.QuickSettingsShade
            } else {
                Scenes.QuickSettings
            }

    override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
        shadeRepository.setLegacyIsQsExpanded(qsExpansion > 0f)
        if (shadeExpansion == 1f) {
            setIdleScene(Scenes.Shade)
        } else if (qsExpansion == 1f) {
            setIdleScene(Scenes.QuickSettings)
        } else if (shadeExpansion == 0f && qsExpansion == 0f) {
            setIdleScene(Scenes.Lockscreen)
        } else if (shadeExpansion == 0f) {
            setTransitionProgress(Scenes.Lockscreen, Scenes.QuickSettings, qsExpansion)
        } else if (qsExpansion == 0f) {
            setTransitionProgress(Scenes.Lockscreen, Scenes.Shade, shadeExpansion)
        } else {
            setTransitionProgress(Scenes.Shade, Scenes.QuickSettings, qsExpansion)
        when {
            shadeExpansion == 1f -> setIdleContent(notificationsShade)
            qsExpansion == 1f -> setIdleContent(quickSettingsShade)
            shadeExpansion == 0f && qsExpansion == 0f -> setIdleScene(Scenes.Lockscreen)
            shadeExpansion == 0f ->
                setTransitionProgress(Scenes.Lockscreen, quickSettingsShade, qsExpansion)
            qsExpansion == 0f ->
                setTransitionProgress(Scenes.Lockscreen, notificationsShade, shadeExpansion)
            else -> setTransitionProgress(notificationsShade, quickSettingsShade, qsExpansion)
        }

        // Requesting a value will cause the stateIn to begin flowing, otherwise incorrect values
        // may not flow fast enough to the stateIn
        // may not flow fast enough to the stateIn.
        shadeInteractor.isAnyFullyExpanded.value
    }

    /** Sets shade expansion to a value between 0-1. */
    override fun setShadeExpansion(shadeExpansion: Float) {
        setShadeAndQsExpansion(shadeExpansion, 0f)
        setShadeAndQsExpansion(shadeExpansion, qsExpansion = 0f)
    }

    /** Sets QS expansion to a value between 0-1. */
    override fun setQsExpansion(qsExpansion: Float) {
        setShadeAndQsExpansion(0f, qsExpansion)
        setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion)
    }

    override fun programmaticCollapseShade() {
        setTransitionProgress(Scenes.Shade, Scenes.Lockscreen, .5f, false)
        setTransitionProgress(notificationsShade, Scenes.Lockscreen, .5f, false)
    }

    override fun setQsFullscreen(qsFullscreen: Boolean) {
@@ -257,12 +273,16 @@ class ShadeTestUtilSceneImpl(
    }

    override fun setLockscreenShadeExpansion(lockscreenShadeExpansion: Float) {
        if (lockscreenShadeExpansion == 0f) {
            setIdleScene(Scenes.Lockscreen)
        } else if (lockscreenShadeExpansion == 1f) {
            setIdleScene(Scenes.Shade)
        } else {
            setTransitionProgress(Scenes.Lockscreen, Scenes.Shade, lockscreenShadeExpansion)
        when (lockscreenShadeExpansion) {
            0f -> setIdleScene(Scenes.Lockscreen)
            1f ->
                setIdleContent(contentKey = notificationsShade, backgroundScene = Scenes.Lockscreen)
            else ->
                setTransitionProgress(
                    Scenes.Lockscreen,
                    notificationsShade,
                    lockscreenShadeExpansion,
                )
        }
    }

@@ -274,33 +294,93 @@ class ShadeTestUtilSceneImpl(
        isUserInputOngoing.value = tracking
    }

    private fun setIdleContent(
        contentKey: ContentKey,
        backgroundScene: SceneKey = sceneInteractor.currentScene.value,
    ) {
        when (contentKey) {
            is SceneKey -> setIdleScene(contentKey)
            is OverlayKey -> setIdleOverlay(contentKey, backgroundScene)
        }
    }

    private fun setIdleScene(scene: SceneKey) {
        sceneInteractor.changeScene(scene, "ShadeTestUtil.setIdleScene")
        val transitionState =
            MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(scene))
        sceneInteractor.setTransitionState(transitionState)
        sceneInteractor.setTransitionState(flowOf(ObservableTransitionState.Idle(scene)))
        testScope.runCurrent()
    }

    private fun setIdleOverlay(overlay: OverlayKey, currentScene: SceneKey) {
        sceneInteractor.showOverlay(overlay, "ShadeTestUtil.setIdleOnOverlay")
        sceneInteractor.setTransitionState(
            flowOf(
                ObservableTransitionState.Idle(
                    currentScene = currentScene,
                    currentOverlays = setOf(overlay),
                )
            )
        )
        testScope.runCurrent()
    }

    private fun setTransitionProgress(
        from: SceneKey,
        to: SceneKey,
        from: ContentKey,
        to: ContentKey,
        progress: Float,
        isInitiatedByUserInput: Boolean = true,
    ) {
        sceneInteractor.changeScene(from, "ShadeTestUtil.setTransitionProgress")
        val loggingReason = "ShadeTestUtil.setTransitionProgress"
        // Set the initial state
        when (from) {
            is SceneKey -> sceneInteractor.changeScene(from, loggingReason)
            is OverlayKey -> sceneInteractor.showOverlay(from, loggingReason)
        }

        val transitionState =
            MutableStateFlow<ObservableTransitionState>(
            when {
                from is SceneKey && to is SceneKey ->
                    ObservableTransitionState.Transition(
                        fromScene = from,
                        toScene = to,
                        currentScene = flowOf(to),
                    progress = MutableStateFlow(progress),
                        progress = flowOf(progress),
                        isInitiatedByUserInput = isInitiatedByUserInput,
                        isUserInputOngoing = isUserInputOngoing,
                    )
                from is SceneKey && to is OverlayKey ->
                    ObservableTransitionState.Transition.showOverlay(
                        overlay = to,
                        fromScene = from,
                        currentOverlays = flowOf(emptySet()),
                        progress = flowOf(progress),
                        isInitiatedByUserInput = isInitiatedByUserInput,
                        isUserInputOngoing = isUserInputOngoing,
                    )
        sceneInteractor.setTransitionState(transitionState)
                from is OverlayKey && to is SceneKey ->
                    ObservableTransitionState.Transition.hideOverlay(
                        overlay = from,
                        toScene = to,
                        currentOverlays = flowOf(emptySet()),
                        progress = flowOf(progress),
                        isInitiatedByUserInput = isInitiatedByUserInput,
                        isUserInputOngoing = isUserInputOngoing,
                    )
                from is OverlayKey && to is OverlayKey ->
                    ObservableTransitionState.Transition.ReplaceOverlay(
                        fromOverlay = from,
                        toOverlay = to,
                        currentScene = sceneInteractor.currentScene.value,
                        currentOverlays = flowOf(emptySet()),
                        progress = flowOf(progress),
                        isInitiatedByUserInput = isInitiatedByUserInput,
                        isUserInputOngoing = isUserInputOngoing,
                        previewProgress = flowOf(0f),
                        isInPreviewStage = flowOf(false),
                    )
                else -> error("Invalid content keys for transition: from=$from, to=$to")
            }

        sceneInteractor.setTransitionState(flowOf(transitionState))
        testScope.runCurrent()
    }

+2 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.domain.interactor.shadeModeInteractor

var Kosmos.shadeTestUtil: ShadeTestUtil by
    Kosmos.Fixture {
@@ -34,6 +35,7 @@ var Kosmos.shadeTestUtil: ShadeTestUtil by
                    fakeShadeRepository,
                    testableContext,
                    shadeInteractor,
                    shadeModeInteractor,
                )
            } else {
                ShadeTestUtilLegacyImpl(
+2 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
@@ -45,6 +46,7 @@ val Kosmos.notificationListViewModel by Fixture {
        headsUpNotificationInteractor,
        remoteInputInteractor,
        shadeInteractor,
        shadeModeInteractor,
        userSetupInteractor,
        testDispatcher,
        dumpManager,