Loading packages/SystemUI/compose/features/src/com/android/systemui/ambientcue/ui/compose/AmbientCueContainer.kt +2 −2 Original line number Diff line number Diff line Loading @@ -33,8 +33,8 @@ fun AmbientCueContainer( ) { val viewModel = rememberViewModel("AmbientCueContainer") { ambientCueViewModelFactory.create() } val visible = viewModel.isOverlayVisible val expanded = viewModel.isOverlayExpanded val visible = viewModel.isVisible val expanded = viewModel.isExpanded val actions = viewModel.actions // TODO: b/414507396 - Replace with the height of the navbar Loading packages/SystemUI/multivalentTests/src/com/android/systemui/ambientcue/ui/viewmodel/AmbientCueViewModelTest.kt 0 → 100644 +71 −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.ambientcue.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.ambientcue.domain.interactor.ambientCueInteractor import com.android.systemui.kosmos.advanceTimeBy import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn 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 @RunWith(AndroidJUnit4::class) @SmallTest class AmbientCueViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val viewModel = kosmos.ambientCueViewModelFactory.create() @Before fun setUp() { viewModel.activateIn(kosmos.testScope) } @Test fun isVisible_timesOut() = kosmos.runTest { ambientCueInteractor.setIsVisible(true) runCurrent() assertThat(viewModel.isVisible).isTrue() // Times out when there's no interaction advanceTimeBy(AmbientCueViewModel.AMBIENT_CUE_TIMEOUT_SEC) runCurrent() assertThat(viewModel.isVisible).isFalse() } @Test fun isVisible_whenExpanded_doesntTimeOut() = kosmos.runTest { ambientCueInteractor.setIsVisible(true) runCurrent() assertThat(viewModel.isVisible).isTrue() // Doesn't time out when expanded viewModel.expand() advanceTimeBy(AmbientCueViewModel.AMBIENT_CUE_TIMEOUT_SEC) runCurrent() assertThat(viewModel.isVisible).isTrue() } } packages/SystemUI/src/com/android/systemui/ambientcue/data/repository/AmbientCueRepository.kt +1 −0 Original line number Diff line number Diff line Loading @@ -145,6 +145,7 @@ constructor( @VisibleForTesting const val AMBIENT_ACTION_FEATURE = 72 // Surface that PCC wants to push cards into @VisibleForTesting const val AMBIENT_CUE_SURFACE = "ambientcue" // Timeout to hide cuebar if it wasn't interacted with private const val TAG = "AmbientCueRepository" private const val DEBUG = false private const val ACTION_CREATE_AMBIENT_CUE = Loading packages/SystemUI/src/com/android/systemui/ambientcue/ui/viewmodel/AmbientCueViewModel.kt +30 −9 Original line number Diff line number Diff line Loading @@ -16,29 +16,36 @@ package com.android.systemui.ambientcue.ui.viewmodel import androidx.annotation.VisibleForTesting import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.android.app.tracing.coroutines.coroutineScopeTraced import com.android.systemui.ambientcue.domain.interactor.AmbientCueInteractor import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class AmbientCueViewModel @AssistedInject constructor(private val ambientCueInteractor: AmbientCueInteractor) : ExclusiveActivatable() { private val hydrator = Hydrator("OverlayViewModel.hydrator") val isOverlayVisible: Boolean by val isVisible: Boolean by hydrator.hydratedStateOf( traceName = "isOverlayVisible", traceName = "isVisible", initialValue = false, source = ambientCueInteractor.isVisible, ) var isOverlayExpanded: Boolean by mutableStateOf(false) var isExpanded: Boolean by mutableStateOf(false) private set val actions: List<ActionViewModel> by Loading @@ -55,24 +62,37 @@ constructor(private val ambientCueInteractor: AmbientCueInteractor) : ExclusiveA fun show() { ambientCueInteractor.setIsVisible(true) isOverlayExpanded = false isExpanded = false } fun expand() { isOverlayExpanded = true isExpanded = true } fun collapse() { isOverlayExpanded = false isExpanded = false } fun hide() { ambientCueInteractor.setIsVisible(false) isOverlayExpanded = false isExpanded = false } override suspend fun onActivated(): Nothing { hydrator.activate() coroutineScopeTraced("AmbientCueViewModel") { launch { hydrator.activate() } launch { // Hide the UI if the user doesn't interact with it after N seconds ambientCueInteractor.isVisible.collectLatest { isVisible -> if (!isVisible) return@collectLatest delay(AMBIENT_CUE_TIMEOUT_SEC) if (!isExpanded) { ambientCueInteractor.setIsVisible(false) } } } awaitCancellation() } } @AssistedFactory Loading @@ -81,6 +101,7 @@ constructor(private val ambientCueInteractor: AmbientCueInteractor) : ExclusiveA } companion object { private const val TAG = "OverlayViewModel" private const val TAG = "AmbientCueViewModel" @VisibleForTesting val AMBIENT_CUE_TIMEOUT_SEC = 15.seconds } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/ambientcue/ui/compose/AmbientCueContainer.kt +2 −2 Original line number Diff line number Diff line Loading @@ -33,8 +33,8 @@ fun AmbientCueContainer( ) { val viewModel = rememberViewModel("AmbientCueContainer") { ambientCueViewModelFactory.create() } val visible = viewModel.isOverlayVisible val expanded = viewModel.isOverlayExpanded val visible = viewModel.isVisible val expanded = viewModel.isExpanded val actions = viewModel.actions // TODO: b/414507396 - Replace with the height of the navbar Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/ambientcue/ui/viewmodel/AmbientCueViewModelTest.kt 0 → 100644 +71 −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.ambientcue.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.ambientcue.domain.interactor.ambientCueInteractor import com.android.systemui.kosmos.advanceTimeBy import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn 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 @RunWith(AndroidJUnit4::class) @SmallTest class AmbientCueViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val viewModel = kosmos.ambientCueViewModelFactory.create() @Before fun setUp() { viewModel.activateIn(kosmos.testScope) } @Test fun isVisible_timesOut() = kosmos.runTest { ambientCueInteractor.setIsVisible(true) runCurrent() assertThat(viewModel.isVisible).isTrue() // Times out when there's no interaction advanceTimeBy(AmbientCueViewModel.AMBIENT_CUE_TIMEOUT_SEC) runCurrent() assertThat(viewModel.isVisible).isFalse() } @Test fun isVisible_whenExpanded_doesntTimeOut() = kosmos.runTest { ambientCueInteractor.setIsVisible(true) runCurrent() assertThat(viewModel.isVisible).isTrue() // Doesn't time out when expanded viewModel.expand() advanceTimeBy(AmbientCueViewModel.AMBIENT_CUE_TIMEOUT_SEC) runCurrent() assertThat(viewModel.isVisible).isTrue() } }
packages/SystemUI/src/com/android/systemui/ambientcue/data/repository/AmbientCueRepository.kt +1 −0 Original line number Diff line number Diff line Loading @@ -145,6 +145,7 @@ constructor( @VisibleForTesting const val AMBIENT_ACTION_FEATURE = 72 // Surface that PCC wants to push cards into @VisibleForTesting const val AMBIENT_CUE_SURFACE = "ambientcue" // Timeout to hide cuebar if it wasn't interacted with private const val TAG = "AmbientCueRepository" private const val DEBUG = false private const val ACTION_CREATE_AMBIENT_CUE = Loading
packages/SystemUI/src/com/android/systemui/ambientcue/ui/viewmodel/AmbientCueViewModel.kt +30 −9 Original line number Diff line number Diff line Loading @@ -16,29 +16,36 @@ package com.android.systemui.ambientcue.ui.viewmodel import androidx.annotation.VisibleForTesting import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.android.app.tracing.coroutines.coroutineScopeTraced import com.android.systemui.ambientcue.domain.interactor.AmbientCueInteractor import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class AmbientCueViewModel @AssistedInject constructor(private val ambientCueInteractor: AmbientCueInteractor) : ExclusiveActivatable() { private val hydrator = Hydrator("OverlayViewModel.hydrator") val isOverlayVisible: Boolean by val isVisible: Boolean by hydrator.hydratedStateOf( traceName = "isOverlayVisible", traceName = "isVisible", initialValue = false, source = ambientCueInteractor.isVisible, ) var isOverlayExpanded: Boolean by mutableStateOf(false) var isExpanded: Boolean by mutableStateOf(false) private set val actions: List<ActionViewModel> by Loading @@ -55,24 +62,37 @@ constructor(private val ambientCueInteractor: AmbientCueInteractor) : ExclusiveA fun show() { ambientCueInteractor.setIsVisible(true) isOverlayExpanded = false isExpanded = false } fun expand() { isOverlayExpanded = true isExpanded = true } fun collapse() { isOverlayExpanded = false isExpanded = false } fun hide() { ambientCueInteractor.setIsVisible(false) isOverlayExpanded = false isExpanded = false } override suspend fun onActivated(): Nothing { hydrator.activate() coroutineScopeTraced("AmbientCueViewModel") { launch { hydrator.activate() } launch { // Hide the UI if the user doesn't interact with it after N seconds ambientCueInteractor.isVisible.collectLatest { isVisible -> if (!isVisible) return@collectLatest delay(AMBIENT_CUE_TIMEOUT_SEC) if (!isExpanded) { ambientCueInteractor.setIsVisible(false) } } } awaitCancellation() } } @AssistedFactory Loading @@ -81,6 +101,7 @@ constructor(private val ambientCueInteractor: AmbientCueInteractor) : ExclusiveA } companion object { private const val TAG = "OverlayViewModel" private const val TAG = "AmbientCueViewModel" @VisibleForTesting val AMBIENT_CUE_TIMEOUT_SEC = 15.seconds } }