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

Commit f6cf8332 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[Dual Shade] Introduce a shade header that is closer to the UX mocks." into main

parents 22dbd88d 3ada7e1c
Loading
Loading
Loading
Loading
+37 −10
Original line number Diff line number Diff line
@@ -18,23 +18,27 @@ package com.android.systemui.notifications.ui.composable

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.layout.layoutId
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
import com.android.systemui.scene.session.ui.composable.SaveableSession
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.ui.composable.Overlay
import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
import com.android.systemui.shade.ui.composable.CollapsedShadeHeader
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.statusbar.phone.ui.StatusBarIconController
import com.android.systemui.statusbar.phone.ui.TintedIconManager
@@ -53,6 +57,8 @@ constructor(
    private val statusBarIconController: StatusBarIconController,
    private val shadeSession: SaveableSession,
    private val stackScrollView: Lazy<NotificationScrollView>,
    private val clockSection: DefaultClockSection,
    private val clockInteractor: KeyguardClockInteractor,
) : Overlay {

    override val key = Overlays.NotificationsShade
@@ -80,14 +86,29 @@ constructor(

        OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) {
            Column {
                ExpandedShadeHeader(
                if (viewModel.showHeader) {
                    val burnIn = rememberBurnIn(clockInteractor)

                    CollapsedShadeHeader(
                        viewModelFactory = viewModel.shadeHeaderViewModelFactory,
                        createTintedIconManager = tintedIconManagerFactory::create,
                    createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
                        createBatteryMeterViewController =
                            batteryMeterViewControllerFactory::create,
                        statusBarIconController = statusBarIconController,
                    modifier = Modifier.padding(horizontal = 16.dp),
                        modifier =
                            Modifier.element(NotificationsShade.Elements.StatusBar)
                                .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
                    )

                    with(clockSection) {
                        SmallClock(
                            burnInParams = burnIn.parameters,
                            onTopChanged = burnIn.onSmallClockTopChanged,
                            modifier = Modifier.fillMaxWidth(),
                        )
                    }
                }

                NotificationScrollingStack(
                    shadeSession = shadeSession,
                    stackScrollView = stackScrollView.get(),
@@ -110,3 +131,9 @@ constructor(
        }
    }
}

object NotificationsShade {
    object Elements {
        val StatusBar = ElementKey("NotificationsShadeStatusBar")
    }
}
+5 −1
Original line number Diff line number Diff line
@@ -105,13 +105,17 @@ val SceneContainerTransitions = transitions {

    // Overlay transitions

    // TODO(b/376659778): Remove this transition once nested STLs are supported.
    from(Scenes.Gone, to = Overlays.NotificationsShade) {
        toNotificationsShadeTransition(translateClock = true)
    }
    to(Overlays.NotificationsShade) { toNotificationsShadeTransition() }
    to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() }
    from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) {
        notificationsShadeToQuickSettingsShadeTransition()
    }
    from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) {
        toNotificationsShadeTransition(durationScale = 0.9)
        toNotificationsShadeTransition(translateClock = true, durationScale = 0.9)
    }
    from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) {
        toQuickSettingsShadeTransition(durationScale = 0.9)
+21 −9
Original line number Diff line number Diff line
@@ -21,30 +21,42 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.notifications.ui.composable.NotificationsShade
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.composable.Shade
import com.android.systemui.shade.ui.composable.ShadeHeader
import kotlin.time.Duration.Companion.milliseconds

fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) {
fun TransitionBuilder.toNotificationsShadeTransition(
    translateClock: Boolean = false,
    durationScale: Double = 1.0,
) {
    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
    swipeSpec =
        spring(
            stiffness = Spring.StiffnessMediumLow,
            visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
        )
    // Ensure the clock isn't clipped by the shade outline during the transition from lockscreen.
    sharedElement(
        ClockElementKeys.smallClockElementKey,
        elevateInContent = Overlays.NotificationsShade,
    )
    scaleSize(OverlayShade.Elements.Panel, height = 0f)
    // TODO(b/376659778): This is a temporary hack to have a shared element transition with the
    //  lockscreen clock. Remove once nested STLs are supported.
    if (!translateClock) {
        translate(ClockElementKeys.smallClockElementKey)
    }
    // Avoid translating the status bar with the shade panel.
    translate(NotificationsShade.Elements.StatusBar)
    // Slide in the shade panel from the top edge.
    translate(OverlayShade.Elements.Panel, Edge.Top)

    fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) }

    fractionRange(start = .5f) {
        fade(ShadeHeader.Elements.Clock)
        fade(ShadeHeader.Elements.ExpandedContent)
        fade(ShadeHeader.Elements.PrivacyChip)
        fade(Notifications.Elements.NotificationScrim)
    }
    fractionRange(start = .5f) { fade(Notifications.Elements.NotificationScrim) }
}

private val DefaultDuration = 300.milliseconds
+35 −0
Original line number Diff line number Diff line
@@ -35,9 +35,12 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -121,6 +124,38 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
            assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
        }

    @Test
    fun showHeader_showsOnNarrowScreen() =
        testScope.runTest {
            kosmos.shadeRepository.setShadeLayoutWide(false)

            // Shown when notifications are present.
            kosmos.activeNotificationListRepository.setActiveNotifs(1)
            runCurrent()
            assertThat(underTest.showHeader).isTrue()

            // Hidden when notifications are not present.
            kosmos.activeNotificationListRepository.setActiveNotifs(0)
            runCurrent()
            assertThat(underTest.showHeader).isFalse()
        }

    @Test
    fun showHeader_hidesOnWideScreen() =
        testScope.runTest {
            kosmos.shadeRepository.setShadeLayoutWide(true)

            // Hidden when notifications are present.
            kosmos.activeNotificationListRepository.setActiveNotifs(1)
            runCurrent()
            assertThat(underTest.showHeader).isFalse()

            // Hidden when notifications are not present.
            kosmos.activeNotificationListRepository.setActiveNotifs(0)
            runCurrent()
            assertThat(underTest.showHeader).isFalse()
        }

    private fun TestScope.lockDevice() {
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        kosmos.powerInteractor.setAsleepForTest()
+34 −1
Original line number Diff line number Diff line
@@ -16,19 +16,23 @@

package com.android.systemui.notifications.ui.viewmodel

import androidx.compose.runtime.getValue
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import com.android.app.tracing.coroutines.launchTraced as launch

/**
 * Models UI state used to render the content of the notifications shade overlay.
@@ -43,10 +47,32 @@ constructor(
    val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
    val sceneInteractor: SceneInteractor,
    private val shadeInteractor: ShadeInteractor,
    activeNotificationsInteractor: ActiveNotificationsInteractor,
) : ExclusiveActivatable() {

    private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator")

    val showHeader: Boolean by
        hydrator.hydratedStateOf(
            traceName = "showHeader",
            initialValue =
                shouldShowHeader(
                    isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value,
                    areAnyNotificationsPresent =
                        activeNotificationsInteractor.areAnyNotificationsPresentValue,
                ),
            source =
                combine(
                    shadeInteractor.isShadeLayoutWide,
                    activeNotificationsInteractor.areAnyNotificationsPresent,
                    this::shouldShowHeader,
                ),
        )

    override suspend fun onActivated(): Nothing {
        coroutineScope {
            launch { hydrator.activate() }

            launch {
                sceneInteractor.currentScene.collect { currentScene ->
                    when (currentScene) {
@@ -77,6 +103,13 @@ constructor(
        shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked")
    }

    private fun shouldShowHeader(
        isShadeLayoutWide: Boolean,
        areAnyNotificationsPresent: Boolean,
    ): Boolean {
        return !isShadeLayoutWide && areAnyNotificationsPresent
    }

    @AssistedFactory
    interface Factory {
        fun create(): NotificationsShadeOverlayContentViewModel
Loading