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

Commit 8590371f authored by András Kurucz's avatar András Kurucz
Browse files

[Flexiglass] Create a SceneContainer based LockscreenNotificationDisplayConfig

The SharedNotificationContainerViewModel maxDisplayedNotifications
calculation was based on legacy sources, that are not fully supported
with Flexiglass.

This CL creates a version of this calculation, that is based on
SceneInteractor transitions.

It also moves binding this config closer to the View, so we don't
need to reference the StackSizeCalculator outside the NSSL.

Fixes: 388473175
Fixes: 376676300
Fixes: 414327366
Bug: 361324188
Test: Check the Notifications behaviour over LockScreen
Flag: com.android.systemui.scene_container
Change-Id: Iafb36cdb7044fb97e05ef605d3b41d3453dbc269
parent 1f9783b7
Loading
Loading
Loading
Loading
+176 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.systemui.statusbar.notification.stack.domain.interactor

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.advanceTimeBy
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.scene.data.repository.sceneContainerRepository
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.enableSingleShade
import com.android.systemui.shade.domain.interactor.enableSplitShade
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
class LockscreenNotificationDisplayConfigInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()

    private lateinit var interactor: LockscreenNotificationDisplayConfigInteractor

    // something that's not -1
    private val limit = 5

    @Before
    fun setUp() {
        interactor = kosmos.lockscreenNotificationDisplayConfigInteractor
    }

    @Test
    fun singleShade_IdleOnLockScreen_showOnlyFullHeight() =
        kosmos.runTest {
            enableSingleShade()

            val lockScreenConfig by
                collectLastValue(interactor.getLockscreenDisplayConfig { _, _ -> limit })

            setTransitionState(ObservableTransitionState.Idle(Scenes.Lockscreen))

            assertShowOnlyFullHeight(lockScreenConfig)
        }

    @Test
    fun splitShade_IdleOnLockScreen_showOnlyFullHeight() =
        kosmos.runTest {
            enableSplitShade()

            val lockScreenConfig by
                collectLastValue(interactor.getLockscreenDisplayConfig { _, _ -> limit })

            setTransitionState(ObservableTransitionState.Idle(Scenes.Lockscreen))

            assertShowOnlyFullHeight(lockScreenConfig)
        }

    @Test
    fun dualShadeShade_IdleOnLockScreen_showOnlyFullHeight() =
        kosmos.runTest {
            enableDualShade()

            val lockScreenConfig by
                collectLastValue(interactor.getLockscreenDisplayConfig { _, _ -> limit })

            setTransitionState(ObservableTransitionState.Idle(Scenes.Lockscreen))

            assertShowOnlyFullHeight(lockScreenConfig)
        }

    @Test
    fun singleShade_IdleOnShade_noLimit() =
        kosmos.runTest {
            enableSingleShade()

            val lockScreenConfig by
                collectLastValue(interactor.getLockscreenDisplayConfig { _, _ -> limit })

            setTransitionState(ObservableTransitionState.Idle(Scenes.Shade))

            assertNoLimit(lockScreenConfig)
        }

    @Test
    fun splitShade_IdleOnShade_noLimit() =
        kosmos.runTest {
            enableSplitShade()

            val lockScreenConfig by
                collectLastValue(interactor.getLockscreenDisplayConfig { _, _ -> limit })

            setTransitionState(ObservableTransitionState.Idle(Scenes.Shade))

            assertNoLimit(lockScreenConfig)
        }

    @Test
    fun dualShadeShade_NotificationShadeOverLockScreen_noLimit() =
        kosmos.runTest {
            enableDualShade()

            val lockScreenConfig by
                collectLastValue(interactor.getLockscreenDisplayConfig { _, _ -> limit })

            setTransitionState(
                ObservableTransitionState.Idle(
                    currentScene = Scenes.Lockscreen,
                    currentOverlays = setOf(Overlays.NotificationsShade),
                )
            )

            assertNoLimit(lockScreenConfig)
        }

    @Test
    fun dualShadeShade_QuickSettingsOverLockScreen_showOnlyFullHeight() =
        kosmos.runTest {
            enableDualShade()

            val lockScreenConfig by
                collectLastValue(interactor.getLockscreenDisplayConfig { _, _ -> limit })

            setTransitionState(
                ObservableTransitionState.Idle(
                    currentScene = Scenes.Lockscreen,
                    currentOverlays = setOf(Overlays.QuickSettingsShade),
                )
            )

            assertShowOnlyFullHeight(lockScreenConfig)
        }

    private fun Kosmos.setTransitionState(transitionState: ObservableTransitionState) {
        sceneContainerRepository.setTransitionState(flowOf(transitionState))
        // workaround to wait for the transition
        advanceTimeBy(50)
    }

    private fun assertNoLimit(lockScreenConfig: LockscreenDisplayConfig?) {
        assertThat(lockScreenConfig).isNotNull()
        assertThat(lockScreenConfig!!.isOnLockscreen).isFalse()
        assertThat(lockScreenConfig.maxNotifications).isEqualTo(-1)
    }

    private fun assertShowOnlyFullHeight(lockScreenConfig: LockscreenDisplayConfig?) {
        assertThat(lockScreenConfig).isNotNull()
        assertThat(lockScreenConfig!!.isOnLockscreen).isTrue()
        assertThat(lockScreenConfig.maxNotifications).isEqualTo(limit)
    }
}
+13 −0
Original line number Diff line number Diff line
@@ -5292,6 +5292,7 @@ public class NotificationStackScrollLayout
    }

    /** @see #isOnLockscreen() */
    @Override
    public void setOnLockscreen(boolean isOnLockscreen) {
        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
        if (mIsOnLockscreen != isOnLockscreen) {
@@ -5305,6 +5306,18 @@ public class NotificationStackScrollLayout
        }
    }

    @Override
    public int calculateMaxNotifications(int availableSpace, boolean useExtraShelfSpace) {
        int shelfHeight = mShelf != null ? mShelf.getIntrinsicHeight() : 0;
        return mNotificationStackSizeCalculator.computeMaxKeyguardNotifications(
                /* stack = */ this,
                /* space = */ availableSpace,
                /* shelfHeight = */ shelfHeight,
                /* extraShelfSpace = */ useExtraShelfSpace ? shelfHeight : 0
        );
    }

    @Override
    public void setMaxDisplayedNotifications(int maxDisplayedNotifications) {
        if (mMaxDisplayedNotifications != maxDisplayedNotifications) {
            if (mLogger != null) {
+1 −1
Original line number Diff line number Diff line
@@ -135,7 +135,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController.Configurati
import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
import com.android.systemui.statusbar.policy.SplitShadeStateController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.Compile;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor;

@@ -1624,6 +1623,7 @@ public class NotificationStackScrollLayoutController implements Dumpable {
     * Set the maximum number of notifications that can currently be displayed
     */
    public void setMaxDisplayedNotifications(int maxNotifications) {
        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
        mNotificationListContainer.setMaxDisplayedNotifications(maxNotifications);
    }

+98 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.systemui.statusbar.notification.stack.domain.interactor

import com.android.compose.animation.scene.ObservableTransitionState.Idle
import com.android.compose.animation.scene.ObservableTransitionState.Transition
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map

/**
 * Data class representing a configuration for displaying Notifications on the Lockscreen.
 *
 * @param isOnLockscreen is the user on the lockscreen
 * @param maxNotifications Limit for the max number of top-level Notifications to be displayed. A
 *   value of -1 indicates no limit.
 */
data class LockscreenDisplayConfig(val isOnLockscreen: Boolean, val maxNotifications: Int)

@SysUISingleton
class LockscreenNotificationDisplayConfigInteractor
@Inject
constructor(
    private val sceneInteractor: SceneInteractor,
    private val sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
    private val notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor,
) {
    private val showOnlyFullHeightNotifications: Flow<Boolean> =
        sceneInteractor.transitionState.map { transitionState ->
            when (transitionState) {
                is Idle ->
                    transitionState.currentScene in keyguardScenes &&
                        Overlays.NotificationsShade !in transitionState.currentOverlays

                is Transition.ChangeScene -> transitionState.fromScene in keyguardScenes

                is Transition.OverlayTransition ->
                    transitionState.currentScene in keyguardScenes &&
                        transitionState.fromContent != Overlays.NotificationsShade
            }
        }

    /**
     * When on keyguard, there is limited space to display notifications so calculate how many could
     * be shown. Otherwise, there is no limit since the vertical space will be scrollable.
     *
     * When expanding or when the user is interacting with the shade, keep the count stable; do not
     * emit a value.
     */
    fun getLockscreenDisplayConfig(
        calculateMaxNotifications: (Int, Boolean) -> Int
    ): Flow<LockscreenDisplayConfig> {
        @Suppress("UNCHECKED_CAST")
        return combine(
                showOnlyFullHeightNotifications,
                notificationStackAppearanceInteractor.constrainedAvailableSpace,
                sharedNotificationContainerInteractor.useExtraShelfSpace,
                sharedNotificationContainerInteractor.notificationStackChanged,
            ) { showLimited, availableHeight, useExtraShelfSpace, _ ->
                if (showLimited) {
                    LockscreenDisplayConfig(
                        isOnLockscreen = true,
                        maxNotifications =
                            calculateMaxNotifications(availableHeight, useExtraShelfSpace),
                    )
                } else {
                    LockscreenDisplayConfig(isOnLockscreen = false, maxNotifications = -1)
                }
            }
            .distinctUntilChanged()
    }

    companion object {
        /** Scenes where only full height notifications are allowed to be shown. */
        val keyguardScenes: Set<SceneKey> = setOf(Scenes.Lockscreen, Scenes.Communal, Scenes.Dream)
    }
}
+12 −0
Original line number Diff line number Diff line
@@ -110,6 +110,18 @@ interface NotificationScrollView {
    /** sets the current QS expand fraction */
    fun setQsExpandFraction(expandFraction: Float)

    /**
     * Returns the number of max Notifications that can be fitted in the given space without
     * clipping their height.
     */
    fun calculateMaxNotifications(space: Int, useExtraShelfSpace: Boolean): Int

    /** Set the max number of notifications that can be displayed. */
    fun setMaxDisplayedNotifications(maxDisplayedNotifications: Int)

    /** TBD what is the diff here exactly? */
    fun setOnLockscreen(onLockScreen: Boolean)

    /** set whether we are idle on the lockscreen scene */
    fun setShowingStackOnLockscreen(showingStackOnLockscreen: Boolean)

Loading