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

Commit cf87e386 authored by András Kurucz's avatar András Kurucz Committed by Android (Google) Code Review
Browse files

Merge "[Flexiglass] Create a SceneContainer based LockscreenNotificationDisplayConfig" into main

parents 106f4685 8590371f
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
@@ -5496,6 +5496,7 @@ public class NotificationStackScrollLayout
    }

    /** @see #isOnLockscreen() */
    @Override
    public void setOnLockscreen(boolean isOnLockscreen) {
        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
        if (mIsOnLockscreen != isOnLockscreen) {
@@ -5509,6 +5510,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