Loading packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt +7 −5 Original line number Original line Diff line number Diff line Loading @@ -19,12 +19,11 @@ package com.android.systemui.keyguard.ui.composable.section import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ContentScope import com.android.systemui.keyguard.ui.viewmodel.KeyguardMediaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardMediaViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.controller.MediaCarouselController 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.MediaHost Loading @@ -38,7 +37,7 @@ class MediaCarouselSection constructor( constructor( private val mediaCarouselController: MediaCarouselController, private val mediaCarouselController: MediaCarouselController, @param:Named(MediaModule.KEYGUARD) private val mediaHost: MediaHost, @param:Named(MediaModule.KEYGUARD) private val mediaHost: MediaHost, private val keyguardMediaViewModel: KeyguardMediaViewModel, private val keyguardMediaViewModelFactory: KeyguardMediaViewModel.Factory, ) { ) { @Composable @Composable Loading @@ -46,7 +45,10 @@ constructor( isShadeLayoutWide: Boolean, isShadeLayoutWide: Boolean, modifier: Modifier = Modifier, modifier: Modifier = Modifier, ) { ) { val isMediaVisible by keyguardMediaViewModel.isMediaVisible.collectAsStateWithLifecycle() val viewModel = rememberViewModel(traceName = "KeyguardMediaCarousel") { keyguardMediaViewModelFactory.create() } val horizontalPadding = val horizontalPadding = if (isShadeLayoutWide) { if (isShadeLayoutWide) { dimensionResource(id = R.dimen.notification_side_paddings) dimensionResource(id = R.dimen.notification_side_paddings) Loading @@ -55,7 +57,7 @@ constructor( dimensionResource(id = R.dimen.notification_panel_margin_horizontal) dimensionResource(id = R.dimen.notification_panel_margin_horizontal) } } MediaCarousel( MediaCarousel( isVisible = isMediaVisible, isVisible = viewModel.isMediaVisible, mediaHost = mediaHost, mediaHost = mediaHost, modifier = modifier.fillMaxWidth().padding(horizontal = horizontalPadding), modifier = modifier.fillMaxWidth().padding(horizontal = horizontalPadding), carouselController = mediaCarouselController, carouselController = mediaCarouselController, Loading packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt 0 → 100644 +84 −0 Original line number Original line 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.keyguard.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher 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.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class KeyguardMediaViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val underTest = kosmos.keyguardMediaViewModelFactory.create() @Before fun setUp() { underTest.activateIn(kosmos.testScope) } @Test fun onDozing_noActiveMedia_mediaIsHidden() = kosmos.runTest { keyguardRepository.setIsDozing(true) assertThat(underTest.isMediaVisible).isFalse() } @Test fun onDozing_activeMediaExists_mediaIsHidden() = kosmos.runTest { val userMedia = MediaData(active = true) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) keyguardRepository.setIsDozing(true) assertThat(underTest.isMediaVisible).isFalse() } @Test fun onDeviceAwake_activeMediaExists_mediaIsVisible() = kosmos.runTest { val userMedia = MediaData(active = true) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) keyguardRepository.setIsDozing(false) assertThat(underTest.isMediaVisible).isTrue() } @Test fun onDeviceAwake_noActiveMedia_mediaIsHidden() = kosmos.runTest { keyguardRepository.setIsDozing(false) assertThat(underTest.isMediaVisible).isFalse() } } packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt +44 −4 Original line number Original line Diff line number Diff line Loading @@ -16,10 +16,50 @@ package com.android.systemui.keyguard.ui.viewmodel package com.android.systemui.keyguard.ui.viewmodel import androidx.compose.runtime.getValue import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 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.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import javax.inject.Inject import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import kotlinx.coroutines.flow.StateFlow import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.flowOf class KeyguardMediaViewModel @Inject constructor(mediaCarouselInteractor: MediaCarouselInteractor) { class KeyguardMediaViewModel val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasActiveMediaOrRecommendation @AssistedInject constructor( mediaCarouselInteractor: MediaCarouselInteractor, keyguardInteractor: KeyguardInteractor, ) : ExclusiveActivatable() { private val hydrator = Hydrator("KeyguardMediaViewModel.hydrator") /** * Whether media carousel is visible on lockscreen. Media may be presented on lockscreen but * still hidden on certain surfaces like AOD */ val isMediaVisible: Boolean by hydrator.hydratedStateOf( traceName = "isMediaVisible", source = keyguardInteractor.isDozing.flatMapLatestConflated { isDozing -> if (isDozing) { flowOf(false) } else { mediaCarouselInteractor.hasActiveMediaOrRecommendation } }, initialValue = !keyguardInteractor.isDozing.value && mediaCarouselInteractor.hasActiveMediaOrRecommendation.value, ) override suspend fun onActivated(): Nothing { hydrator.activate() } @AssistedFactory interface Factory { fun create(): KeyguardMediaViewModel } } } packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt 0 → 100644 +30 −0 Original line number Original line 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.keyguard.ui.viewmodel import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor val Kosmos.keyguardMediaViewModelFactory by Kosmos.Fixture { object : KeyguardMediaViewModel.Factory { override fun create(): KeyguardMediaViewModel { return KeyguardMediaViewModel(mediaCarouselInteractor, keyguardInteractor) } } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt +7 −5 Original line number Original line Diff line number Diff line Loading @@ -19,12 +19,11 @@ package com.android.systemui.keyguard.ui.composable.section import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ContentScope import com.android.systemui.keyguard.ui.viewmodel.KeyguardMediaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardMediaViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.controller.MediaCarouselController 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.MediaHost Loading @@ -38,7 +37,7 @@ class MediaCarouselSection constructor( constructor( private val mediaCarouselController: MediaCarouselController, private val mediaCarouselController: MediaCarouselController, @param:Named(MediaModule.KEYGUARD) private val mediaHost: MediaHost, @param:Named(MediaModule.KEYGUARD) private val mediaHost: MediaHost, private val keyguardMediaViewModel: KeyguardMediaViewModel, private val keyguardMediaViewModelFactory: KeyguardMediaViewModel.Factory, ) { ) { @Composable @Composable Loading @@ -46,7 +45,10 @@ constructor( isShadeLayoutWide: Boolean, isShadeLayoutWide: Boolean, modifier: Modifier = Modifier, modifier: Modifier = Modifier, ) { ) { val isMediaVisible by keyguardMediaViewModel.isMediaVisible.collectAsStateWithLifecycle() val viewModel = rememberViewModel(traceName = "KeyguardMediaCarousel") { keyguardMediaViewModelFactory.create() } val horizontalPadding = val horizontalPadding = if (isShadeLayoutWide) { if (isShadeLayoutWide) { dimensionResource(id = R.dimen.notification_side_paddings) dimensionResource(id = R.dimen.notification_side_paddings) Loading @@ -55,7 +57,7 @@ constructor( dimensionResource(id = R.dimen.notification_panel_margin_horizontal) dimensionResource(id = R.dimen.notification_panel_margin_horizontal) } } MediaCarousel( MediaCarousel( isVisible = isMediaVisible, isVisible = viewModel.isMediaVisible, mediaHost = mediaHost, mediaHost = mediaHost, modifier = modifier.fillMaxWidth().padding(horizontal = horizontalPadding), modifier = modifier.fillMaxWidth().padding(horizontal = horizontalPadding), carouselController = mediaCarouselController, carouselController = mediaCarouselController, Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt 0 → 100644 +84 −0 Original line number Original line 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.keyguard.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher 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.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class KeyguardMediaViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val underTest = kosmos.keyguardMediaViewModelFactory.create() @Before fun setUp() { underTest.activateIn(kosmos.testScope) } @Test fun onDozing_noActiveMedia_mediaIsHidden() = kosmos.runTest { keyguardRepository.setIsDozing(true) assertThat(underTest.isMediaVisible).isFalse() } @Test fun onDozing_activeMediaExists_mediaIsHidden() = kosmos.runTest { val userMedia = MediaData(active = true) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) keyguardRepository.setIsDozing(true) assertThat(underTest.isMediaVisible).isFalse() } @Test fun onDeviceAwake_activeMediaExists_mediaIsVisible() = kosmos.runTest { val userMedia = MediaData(active = true) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) keyguardRepository.setIsDozing(false) assertThat(underTest.isMediaVisible).isTrue() } @Test fun onDeviceAwake_noActiveMedia_mediaIsHidden() = kosmos.runTest { keyguardRepository.setIsDozing(false) assertThat(underTest.isMediaVisible).isFalse() } }
packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt +44 −4 Original line number Original line Diff line number Diff line Loading @@ -16,10 +16,50 @@ package com.android.systemui.keyguard.ui.viewmodel package com.android.systemui.keyguard.ui.viewmodel import androidx.compose.runtime.getValue import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 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.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import javax.inject.Inject import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import kotlinx.coroutines.flow.StateFlow import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.flowOf class KeyguardMediaViewModel @Inject constructor(mediaCarouselInteractor: MediaCarouselInteractor) { class KeyguardMediaViewModel val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasActiveMediaOrRecommendation @AssistedInject constructor( mediaCarouselInteractor: MediaCarouselInteractor, keyguardInteractor: KeyguardInteractor, ) : ExclusiveActivatable() { private val hydrator = Hydrator("KeyguardMediaViewModel.hydrator") /** * Whether media carousel is visible on lockscreen. Media may be presented on lockscreen but * still hidden on certain surfaces like AOD */ val isMediaVisible: Boolean by hydrator.hydratedStateOf( traceName = "isMediaVisible", source = keyguardInteractor.isDozing.flatMapLatestConflated { isDozing -> if (isDozing) { flowOf(false) } else { mediaCarouselInteractor.hasActiveMediaOrRecommendation } }, initialValue = !keyguardInteractor.isDozing.value && mediaCarouselInteractor.hasActiveMediaOrRecommendation.value, ) override suspend fun onActivated(): Nothing { hydrator.activate() } @AssistedFactory interface Factory { fun create(): KeyguardMediaViewModel } } }
packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt 0 → 100644 +30 −0 Original line number Original line 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.keyguard.ui.viewmodel import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor val Kosmos.keyguardMediaViewModelFactory by Kosmos.Fixture { object : KeyguardMediaViewModel.Factory { override fun create(): KeyguardMediaViewModel { return KeyguardMediaViewModel(mediaCarouselInteractor, keyguardInteractor) } } }