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

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

[Dual Shade] Add UMO (media controls) to the notifications shade.

Fix: 395616939
Test: Manually tested by adding active media playback and observing it
 is rendered in notifications shade.
Test: Added unit tests.
Test: Existing unit tests still pass.
Flag: com.android.systemui.scene_container
Change-Id: I75b64ceb3d25d90e030260258b289f7e1570f54e
parent 5e95d262
Loading
Loading
Loading
Loading
+31 −5
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.dimensionResource
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
@@ -34,6 +34,13 @@ import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.media.controls.ui.composable.MediaCarousel
import com.android.systemui.media.controls.ui.composable.isLandscape
import com.android.systemui.media.controls.ui.controller.MediaCarouselController
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.COLLAPSED
import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.EXPANDED
import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
import com.android.systemui.res.R
@@ -42,10 +49,11 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.ui.composable.Overlay
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.composable.OverlayShadeHeader
import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.util.Utils
import dagger.Lazy
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.flow.Flow

@SysUISingleton
@@ -58,6 +66,8 @@ constructor(
    private val stackScrollView: Lazy<NotificationScrollView>,
    private val clockSection: DefaultClockSection,
    private val keyguardClockViewModel: KeyguardClockViewModel,
    private val mediaCarouselController: MediaCarouselController,
    @Named(QUICK_QS_PANEL) private val mediaHost: Lazy<MediaHost>,
) : Overlay {
    override val key = Overlays.NotificationsShade

@@ -84,6 +94,11 @@ constructor(
                viewModel.notificationsPlaceholderViewModelFactory.create()
            }

        val usingCollapsedLandscapeMedia =
            Utils.useCollapsedMediaInLandscape(LocalResources.current)
        mediaHost.get().expansion =
            if (usingCollapsedLandscapeMedia && isLandscape()) COLLAPSED else EXPANDED

        OverlayShade(
            panelElement = NotificationsShade.Elements.Panel,
            alignmentOnWideScreens = Alignment.TopStart,
@@ -96,9 +111,7 @@ constructor(
                    }
                OverlayShadeHeader(
                    viewModel = headerViewModel,
                    modifier =
                        Modifier.element(NotificationsShade.Elements.StatusBar)
                            .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
                    modifier = Modifier.element(NotificationsShade.Elements.StatusBar),
                )
            },
        ) {
@@ -116,6 +129,19 @@ constructor(
                        }
                    }

                    MediaCarousel(
                        isVisible = viewModel.showMedia,
                        mediaHost = mediaHost.get(),
                        carouselController = mediaCarouselController,
                        usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia,
                        modifier =
                            Modifier.padding(
                                top = notificationStackPadding,
                                start = notificationStackPadding,
                                end = notificationStackPadding,
                            ),
                    )

                    NotificationScrollingStack(
                        shadeSession = shadeSession,
                        stackScrollView = stackScrollView.get(),
+37 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

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

import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,6 +29,8 @@ import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
@@ -39,10 +42,13 @@ import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel
import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
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
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -50,6 +56,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@@ -155,6 +162,36 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
            assertThat(underTest.showClock).isFalse()
        }

    @Test
    fun showMedia_activeMedia_true() =
        testScope.runTest {
            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true))
            runCurrent()

            assertThat(underTest.showMedia).isTrue()
        }

    @Test
    fun showMedia_noActiveMedia_false() =
        testScope.runTest {
            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = false))
            runCurrent()

            assertThat(underTest.showMedia).isFalse()
        }

    @Test
    fun showMedia_qsDisabled_false() =
        testScope.runTest {
            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true))
            kosmos.fakeDisableFlagsRepository.disableFlags.update {
                it.copy(disable2 = DISABLE2_QUICK_SETTINGS)
            }
            runCurrent()

            assertThat(underTest.showMedia).isFalse()
        }

    private fun TestScope.lockDevice() {
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        kosmos.powerInteractor.setAsleepForTest()
+22 −0
Original line number Diff line number Diff line
@@ -20,12 +20,15 @@ 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.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
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.disableflags.domain.interactor.DisableFlagsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation
@@ -33,6 +36,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOf

/**
 * Models UI state used to render the content of the notifications shade overlay.
@@ -47,6 +51,8 @@ constructor(
    val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
    val sceneInteractor: SceneInteractor,
    private val shadeInteractor: ShadeInteractor,
    disableFlagsInteractor: DisableFlagsInteractor,
    mediaCarouselInteractor: MediaCarouselInteractor,
    activeNotificationsInteractor: ActiveNotificationsInteractor,
) : ExclusiveActivatable() {

@@ -69,6 +75,22 @@ constructor(
                ),
        )

    val showMedia: Boolean by
        hydrator.hydratedStateOf(
            traceName = "showMedia",
            initialValue =
                disableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled() &&
                    mediaCarouselInteractor.hasActiveMediaOrRecommendation.value,
            source =
                disableFlagsInteractor.disableFlags.flatMapLatestConflated {
                    if (it.isQuickSettingsEnabled()) {
                        mediaCarouselInteractor.hasActiveMediaOrRecommendation
                    } else {
                        flowOf(false)
                    }
                },
        )

    override suspend fun onActivated(): Nothing {
        coroutineScope {
            launch { hydrator.activate() }
+4 −0
Original line number Diff line number Diff line
@@ -18,9 +18,11 @@ package com.android.systemui.shade.ui.viewmodel

import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory

@@ -31,6 +33,8 @@ val Kosmos.notificationsShadeOverlayContentViewModel:
        notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory,
        sceneInteractor = sceneInteractor,
        shadeInteractor = shadeInteractor,
        disableFlagsInteractor = disableFlagsInteractor,
        mediaCarouselInteractor = mediaCarouselInteractor,
        activeNotificationsInteractor = activeNotificationsInteractor,
    )
}