Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/domain/interactor/BundleInteractorTest.kt +97 −2 Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ import com.android.systemui.controls.dagger.ControlsComponentTest.Companion.eq import com.android.systemui.controls.ui.ControlActionCoordinatorImplTest.Companion.any import com.android.systemui.kosmos.testScope import com.android.systemui.notifications.ui.composable.row.BundleHeader import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.row.data.model.AppData import com.android.systemui.statusbar.notification.row.data.repository.BundleRepository import com.android.systemui.statusbar.notification.row.data.repository.testBundleRepository Loading @@ -41,9 +43,11 @@ import com.android.systemui.statusbar.notification.row.icon.appIconProvider import com.android.systemui.statusbar.notification.row.icon.mockAppIconProvider import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.testKosmos import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.systemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest Loading @@ -58,6 +62,7 @@ import org.mockito.junit.MockitoRule import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import platform.test.motion.compose.runMonotonicClockTest import kotlin.test.Test @OptIn(ExperimentalCoroutinesApi::class) @SmallTest Loading @@ -69,9 +74,11 @@ class BundleInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val fakeSystemClock = FakeSystemClock() private val testBundleRepository: BundleRepository = kosmos.testBundleRepository private val mockShadeInteractor = mock<ShadeInteractor>() private val isShadeFullyCollapsedFlow = MutableStateFlow(false) private lateinit var underTest: BundleInteractor private val drawable1: Drawable = ColorDrawable(Color.RED) Loading @@ -81,6 +88,9 @@ class BundleInteractorTest : SysuiTestCase() { @Before fun setUp() { kosmos.appIconProvider = kosmos.mockAppIconProvider whenever(mockShadeInteractor.isShadeFullyCollapsed).thenReturn(isShadeFullyCollapsedFlow) kosmos.shadeInteractor = mockShadeInteractor kosmos.systemClock = fakeSystemClock underTest = kosmos.bundleInteractor } Loading Loading @@ -293,4 +303,89 @@ class BundleInteractorTest : SysuiTestCase() { // Assert assertThat(underTest.state?.currentScene).isEqualTo(BundleHeader.Scenes.Collapsed) } @Test fun setTargetScene_whenCollapsing_updatesLastCollapseTime() = runMonotonicClockTest { // Arrange val testTime = 20000L fakeSystemClock.setUptimeMillis(testTime) underTest.state = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Expanded, motionScheme = MotionScheme.standard(), ) underTest.composeScope = this // Act underTest.setTargetScene(BundleHeader.Scenes.Collapsed) testScope.runCurrent() // Assert assertThat(testBundleRepository.lastCollapseTime).isEqualTo(testTime) } @Test fun setTargetScene_whenExpanding_doesNotUpdateLastCollapseTime() = runMonotonicClockTest { // Arrange val initialTime = 11000L testBundleRepository.lastCollapseTime = initialTime fakeSystemClock.setUptimeMillis(20000L) underTest.state = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Collapsed, motionScheme = MotionScheme.standard(), ) underTest.composeScope = this // Act underTest.setTargetScene(BundleHeader.Scenes.Expanded) testScope.runCurrent() // Assert assertThat(testBundleRepository.lastCollapseTime).isEqualTo(initialTime) } @Test fun observeShadeState_whenShadeCollapsesOnExpandedBundle_updatesState() = testScope.runTest { // Arrange val shadeState = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Expanded, motionScheme = MotionScheme.standard(), ) testBundleRepository.state = shadeState val testTime = 20000L fakeSystemClock.setUptimeMillis(testTime) // Act isShadeFullyCollapsedFlow.value = true runCurrent() // Assert assertThat(testBundleRepository.lastCollapseTime).isEqualTo(testTime) assertThat(shadeState.currentScene).isEqualTo(BundleHeader.Scenes.Collapsed) } @Test fun observeShadeState_whenShadeCollapsesOnCollapsedBundle_doesNothing() = testScope.runTest { // Arrange val shadeState = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Collapsed, motionScheme = MotionScheme.standard(), ) testBundleRepository.state = shadeState val initialTime = 11000L testBundleRepository.lastCollapseTime = initialTime fakeSystemClock.setUptimeMillis(20000L) // Act isShadeFullyCollapsedFlow.value = true runCurrent() // Assert assertThat(testBundleRepository.lastCollapseTime).isEqualTo(initialTime) } } packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/BundleHeaderViewModelTest.kt +25 −18 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.row.ui.viewmodel import android.platform.test.annotations.EnableFlags import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MotionScheme import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.MutableSceneTransitionLayoutState Loading @@ -24,9 +26,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.notifications.ui.composable.row.BundleHeader import com.android.systemui.statusbar.notification.row.domain.interactor.BundleInteractor import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.testKosmos import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Rule import org.junit.Test Loading @@ -39,52 +42,56 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class) @EnableFlags(NotificationBundleUi.FLAG_NAME) class BundleHeaderViewModelTest : SysuiTestCase() { @get:Rule val rule: MockitoRule = MockitoJUnit.rule() private val kosmos = testKosmos() @Mock lateinit var mockSceneTransitionLayoutState: MutableSceneTransitionLayoutState @Mock lateinit var mockComposeScope: CoroutineScope @Mock private lateinit var mockBundleInteractor: BundleInteractor private lateinit var underTest: BundleHeaderViewModel @Before fun setup() { underTest = kosmos.bundleHeaderViewModelFactory.create() whenever(mockBundleInteractor.previewIcons).thenReturn(MutableStateFlow(emptyList())) underTest = BundleHeaderViewModel(mockBundleInteractor) underTest.activateIn(kosmos.testScope) underTest.state = mockSceneTransitionLayoutState underTest.composeScope = mockComposeScope } @Test fun onHeaderClicked_toggles_expansion_state_to_expanded() { // Arrange whenever(mockSceneTransitionLayoutState.currentScene) .thenReturn(BundleHeader.Scenes.Collapsed) val state = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Collapsed, motionScheme = MotionScheme.standard(), ) underTest.state = state whenever(mockBundleInteractor.state).thenReturn(state) // Act underTest.onHeaderClicked() // Assert verify(mockSceneTransitionLayoutState) .setTargetScene(BundleHeader.Scenes.Expanded, mockComposeScope) verify(mockBundleInteractor).setTargetScene(BundleHeader.Scenes.Expanded) } @Test fun onHeaderClicked_toggles_expansion_state_to_collapsed() { // Arrange whenever(mockSceneTransitionLayoutState.currentScene) .thenReturn(BundleHeader.Scenes.Expanded) val state = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Expanded, motionScheme = MotionScheme.standard(), ) underTest.state = state whenever(mockBundleInteractor.state).thenReturn(state) // Act underTest.onHeaderClicked() // Assert verify(mockSceneTransitionLayoutState) .setTargetScene(BundleHeader.Scenes.Collapsed, mockComposeScope) verify(mockBundleInteractor).setTargetScene(BundleHeader.Scenes.Collapsed) } } No newline at end of file packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/BundleBarn.kt +23 −1 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection.render import android.content.Context import android.view.View import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect Loading @@ -25,6 +26,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.initOnBackPressedDispatcherOwner import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.lifecycle.repeatWhenAttached Loading @@ -47,6 +49,10 @@ import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeader import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.util.time.SystemClock import dagger.Lazy import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import javax.inject.Inject import javax.inject.Provider Loading @@ -63,6 +69,7 @@ constructor( val systemClock: SystemClock, val logger: RowInflaterTaskLogger, val userTracker: UserTracker, @Main private val mainDispatcher: CoroutineDispatcher, private val presenterLazy: Lazy<NotificationPresenter?>? = null, private val iconManager: IconManager, ) : PipelineDumpable { Loading Loading @@ -114,8 +121,23 @@ constructor( } private fun initBundleHeaderView(bundleEntry: BundleEntry, row: ExpandableNotificationRow) { val scope = CoroutineScope(SupervisorJob() + mainDispatcher) row.addOnAttachStateChangeListener( object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) {} override fun onViewDetachedFromWindow(v: View) { scope.cancel() row.removeOnAttachStateChangeListener(this) } } ) val bundleRowComponent = bundleRowComponentBuilder.bindBundleRepository(bundleEntry.bundleRepository).build() bundleRowComponentBuilder .bindBundleRepository(bundleEntry.bundleRepository) .bindScope(scope) .build() val headerComposeView = ComposeView(context) row.setBundleHeaderView(headerComposeView) headerComposeView.repeatWhenAttached { Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/BundleRowComponent.kt +3 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import com.android.systemui.statusbar.notification.row.data.repository.BundleRep import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderViewModel import dagger.BindsInstance import dagger.Subcomponent import kotlinx.coroutines.CoroutineScope /** This dagger component is used to init the ViewModel and Interactors needed for a bundle row */ @Subcomponent Loading @@ -32,6 +33,8 @@ interface BundleRowComponent { interface Builder { @BindsInstance fun bindBundleRepository(repository: BundleRepository): Builder @BindsInstance fun bindScope(scope: CoroutineScope): Builder fun build(): BundleRowComponent } } packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/BundleInteractor.kt +37 −4 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import com.android.compose.animation.scene.SceneKey import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.notifications.ui.composable.row.BundleHeader import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.row.dagger.BundleRowScope import com.android.systemui.statusbar.notification.row.data.model.AppData import com.android.systemui.statusbar.notification.row.data.repository.BundleRepository Loading @@ -35,12 +36,15 @@ import com.android.systemui.statusbar.notification.row.icon.AppIconProvider import com.android.systemui.util.icuMessageFormat import com.android.systemui.util.time.SystemClock import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject /** Provides functionality for UI to interact with a Notification Bundle. */ @BundleRowScope Loading @@ -52,6 +56,8 @@ constructor( private val context: Context, @Background private val backgroundDispatcher: CoroutineDispatcher, private val systemClock: SystemClock, private val shadeInteractor: ShadeInteractor, @BundleRowScope private val scope: CoroutineScope, ) { @get:StringRes val titleText: Int Loading @@ -72,6 +78,27 @@ constructor( numberOfChildren ?: 0, ) private var sceneTargetJob: Job? = null init { observeShadeState() } private fun observeShadeState() { scope.launch { shadeInteractor.isShadeFullyCollapsed .filter { isCollapsed -> isCollapsed } // Only act when it becomes true .collect { if (repository.state?.currentScene == BundleHeader.Scenes.Expanded) { repository.lastCollapseTime = systemClock.uptimeMillis() // Use snapTo() since the UI is already gone and no animation is needed. repository.state?.snapTo(BundleHeader.Scenes.Collapsed) } } } } /** Filters the list of AppData based on time of last collapse by user. */ private fun filterByCollapseTime( rawAppDataList: List<AppData>, Loading Loading @@ -123,11 +150,17 @@ constructor( } fun setTargetScene(scene: SceneKey) { sceneTargetJob?.cancel() sceneTargetJob = scope.launch { state?.setTargetScene(scene, composeScope!!) if (state?.currentScene == BundleHeader.Scenes.Collapsed) { // [setTargetScene] does not immediately update [currentScene] so we must check [scene] if (scene == BundleHeader.Scenes.Collapsed) { repository.lastCollapseTime = systemClock.uptimeMillis() } } } private fun fetchAppIcon(appData: AppData): Drawable? { return try { Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/domain/interactor/BundleInteractorTest.kt +97 −2 Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ import com.android.systemui.controls.dagger.ControlsComponentTest.Companion.eq import com.android.systemui.controls.ui.ControlActionCoordinatorImplTest.Companion.any import com.android.systemui.kosmos.testScope import com.android.systemui.notifications.ui.composable.row.BundleHeader import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.row.data.model.AppData import com.android.systemui.statusbar.notification.row.data.repository.BundleRepository import com.android.systemui.statusbar.notification.row.data.repository.testBundleRepository Loading @@ -41,9 +43,11 @@ import com.android.systemui.statusbar.notification.row.icon.appIconProvider import com.android.systemui.statusbar.notification.row.icon.mockAppIconProvider import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.testKosmos import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.systemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest Loading @@ -58,6 +62,7 @@ import org.mockito.junit.MockitoRule import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import platform.test.motion.compose.runMonotonicClockTest import kotlin.test.Test @OptIn(ExperimentalCoroutinesApi::class) @SmallTest Loading @@ -69,9 +74,11 @@ class BundleInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val fakeSystemClock = FakeSystemClock() private val testBundleRepository: BundleRepository = kosmos.testBundleRepository private val mockShadeInteractor = mock<ShadeInteractor>() private val isShadeFullyCollapsedFlow = MutableStateFlow(false) private lateinit var underTest: BundleInteractor private val drawable1: Drawable = ColorDrawable(Color.RED) Loading @@ -81,6 +88,9 @@ class BundleInteractorTest : SysuiTestCase() { @Before fun setUp() { kosmos.appIconProvider = kosmos.mockAppIconProvider whenever(mockShadeInteractor.isShadeFullyCollapsed).thenReturn(isShadeFullyCollapsedFlow) kosmos.shadeInteractor = mockShadeInteractor kosmos.systemClock = fakeSystemClock underTest = kosmos.bundleInteractor } Loading Loading @@ -293,4 +303,89 @@ class BundleInteractorTest : SysuiTestCase() { // Assert assertThat(underTest.state?.currentScene).isEqualTo(BundleHeader.Scenes.Collapsed) } @Test fun setTargetScene_whenCollapsing_updatesLastCollapseTime() = runMonotonicClockTest { // Arrange val testTime = 20000L fakeSystemClock.setUptimeMillis(testTime) underTest.state = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Expanded, motionScheme = MotionScheme.standard(), ) underTest.composeScope = this // Act underTest.setTargetScene(BundleHeader.Scenes.Collapsed) testScope.runCurrent() // Assert assertThat(testBundleRepository.lastCollapseTime).isEqualTo(testTime) } @Test fun setTargetScene_whenExpanding_doesNotUpdateLastCollapseTime() = runMonotonicClockTest { // Arrange val initialTime = 11000L testBundleRepository.lastCollapseTime = initialTime fakeSystemClock.setUptimeMillis(20000L) underTest.state = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Collapsed, motionScheme = MotionScheme.standard(), ) underTest.composeScope = this // Act underTest.setTargetScene(BundleHeader.Scenes.Expanded) testScope.runCurrent() // Assert assertThat(testBundleRepository.lastCollapseTime).isEqualTo(initialTime) } @Test fun observeShadeState_whenShadeCollapsesOnExpandedBundle_updatesState() = testScope.runTest { // Arrange val shadeState = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Expanded, motionScheme = MotionScheme.standard(), ) testBundleRepository.state = shadeState val testTime = 20000L fakeSystemClock.setUptimeMillis(testTime) // Act isShadeFullyCollapsedFlow.value = true runCurrent() // Assert assertThat(testBundleRepository.lastCollapseTime).isEqualTo(testTime) assertThat(shadeState.currentScene).isEqualTo(BundleHeader.Scenes.Collapsed) } @Test fun observeShadeState_whenShadeCollapsesOnCollapsedBundle_doesNothing() = testScope.runTest { // Arrange val shadeState = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Collapsed, motionScheme = MotionScheme.standard(), ) testBundleRepository.state = shadeState val initialTime = 11000L testBundleRepository.lastCollapseTime = initialTime fakeSystemClock.setUptimeMillis(20000L) // Act isShadeFullyCollapsedFlow.value = true runCurrent() // Assert assertThat(testBundleRepository.lastCollapseTime).isEqualTo(initialTime) } }
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/BundleHeaderViewModelTest.kt +25 −18 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.row.ui.viewmodel import android.platform.test.annotations.EnableFlags import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MotionScheme import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.MutableSceneTransitionLayoutState Loading @@ -24,9 +26,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.notifications.ui.composable.row.BundleHeader import com.android.systemui.statusbar.notification.row.domain.interactor.BundleInteractor import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.testKosmos import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Rule import org.junit.Test Loading @@ -39,52 +42,56 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class) @EnableFlags(NotificationBundleUi.FLAG_NAME) class BundleHeaderViewModelTest : SysuiTestCase() { @get:Rule val rule: MockitoRule = MockitoJUnit.rule() private val kosmos = testKosmos() @Mock lateinit var mockSceneTransitionLayoutState: MutableSceneTransitionLayoutState @Mock lateinit var mockComposeScope: CoroutineScope @Mock private lateinit var mockBundleInteractor: BundleInteractor private lateinit var underTest: BundleHeaderViewModel @Before fun setup() { underTest = kosmos.bundleHeaderViewModelFactory.create() whenever(mockBundleInteractor.previewIcons).thenReturn(MutableStateFlow(emptyList())) underTest = BundleHeaderViewModel(mockBundleInteractor) underTest.activateIn(kosmos.testScope) underTest.state = mockSceneTransitionLayoutState underTest.composeScope = mockComposeScope } @Test fun onHeaderClicked_toggles_expansion_state_to_expanded() { // Arrange whenever(mockSceneTransitionLayoutState.currentScene) .thenReturn(BundleHeader.Scenes.Collapsed) val state = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Collapsed, motionScheme = MotionScheme.standard(), ) underTest.state = state whenever(mockBundleInteractor.state).thenReturn(state) // Act underTest.onHeaderClicked() // Assert verify(mockSceneTransitionLayoutState) .setTargetScene(BundleHeader.Scenes.Expanded, mockComposeScope) verify(mockBundleInteractor).setTargetScene(BundleHeader.Scenes.Expanded) } @Test fun onHeaderClicked_toggles_expansion_state_to_collapsed() { // Arrange whenever(mockSceneTransitionLayoutState.currentScene) .thenReturn(BundleHeader.Scenes.Expanded) val state = MutableSceneTransitionLayoutState( initialScene = BundleHeader.Scenes.Expanded, motionScheme = MotionScheme.standard(), ) underTest.state = state whenever(mockBundleInteractor.state).thenReturn(state) // Act underTest.onHeaderClicked() // Assert verify(mockSceneTransitionLayoutState) .setTargetScene(BundleHeader.Scenes.Collapsed, mockComposeScope) verify(mockBundleInteractor).setTargetScene(BundleHeader.Scenes.Collapsed) } } No newline at end of file
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/BundleBarn.kt +23 −1 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection.render import android.content.Context import android.view.View import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect Loading @@ -25,6 +26,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.initOnBackPressedDispatcherOwner import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.lifecycle.repeatWhenAttached Loading @@ -47,6 +49,10 @@ import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeader import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.util.time.SystemClock import dagger.Lazy import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import javax.inject.Inject import javax.inject.Provider Loading @@ -63,6 +69,7 @@ constructor( val systemClock: SystemClock, val logger: RowInflaterTaskLogger, val userTracker: UserTracker, @Main private val mainDispatcher: CoroutineDispatcher, private val presenterLazy: Lazy<NotificationPresenter?>? = null, private val iconManager: IconManager, ) : PipelineDumpable { Loading Loading @@ -114,8 +121,23 @@ constructor( } private fun initBundleHeaderView(bundleEntry: BundleEntry, row: ExpandableNotificationRow) { val scope = CoroutineScope(SupervisorJob() + mainDispatcher) row.addOnAttachStateChangeListener( object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) {} override fun onViewDetachedFromWindow(v: View) { scope.cancel() row.removeOnAttachStateChangeListener(this) } } ) val bundleRowComponent = bundleRowComponentBuilder.bindBundleRepository(bundleEntry.bundleRepository).build() bundleRowComponentBuilder .bindBundleRepository(bundleEntry.bundleRepository) .bindScope(scope) .build() val headerComposeView = ComposeView(context) row.setBundleHeaderView(headerComposeView) headerComposeView.repeatWhenAttached { Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/BundleRowComponent.kt +3 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import com.android.systemui.statusbar.notification.row.data.repository.BundleRep import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderViewModel import dagger.BindsInstance import dagger.Subcomponent import kotlinx.coroutines.CoroutineScope /** This dagger component is used to init the ViewModel and Interactors needed for a bundle row */ @Subcomponent Loading @@ -32,6 +33,8 @@ interface BundleRowComponent { interface Builder { @BindsInstance fun bindBundleRepository(repository: BundleRepository): Builder @BindsInstance fun bindScope(scope: CoroutineScope): Builder fun build(): BundleRowComponent } }
packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/BundleInteractor.kt +37 −4 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import com.android.compose.animation.scene.SceneKey import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.notifications.ui.composable.row.BundleHeader import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.row.dagger.BundleRowScope import com.android.systemui.statusbar.notification.row.data.model.AppData import com.android.systemui.statusbar.notification.row.data.repository.BundleRepository Loading @@ -35,12 +36,15 @@ import com.android.systemui.statusbar.notification.row.icon.AppIconProvider import com.android.systemui.util.icuMessageFormat import com.android.systemui.util.time.SystemClock import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject /** Provides functionality for UI to interact with a Notification Bundle. */ @BundleRowScope Loading @@ -52,6 +56,8 @@ constructor( private val context: Context, @Background private val backgroundDispatcher: CoroutineDispatcher, private val systemClock: SystemClock, private val shadeInteractor: ShadeInteractor, @BundleRowScope private val scope: CoroutineScope, ) { @get:StringRes val titleText: Int Loading @@ -72,6 +78,27 @@ constructor( numberOfChildren ?: 0, ) private var sceneTargetJob: Job? = null init { observeShadeState() } private fun observeShadeState() { scope.launch { shadeInteractor.isShadeFullyCollapsed .filter { isCollapsed -> isCollapsed } // Only act when it becomes true .collect { if (repository.state?.currentScene == BundleHeader.Scenes.Expanded) { repository.lastCollapseTime = systemClock.uptimeMillis() // Use snapTo() since the UI is already gone and no animation is needed. repository.state?.snapTo(BundleHeader.Scenes.Collapsed) } } } } /** Filters the list of AppData based on time of last collapse by user. */ private fun filterByCollapseTime( rawAppDataList: List<AppData>, Loading Loading @@ -123,11 +150,17 @@ constructor( } fun setTargetScene(scene: SceneKey) { sceneTargetJob?.cancel() sceneTargetJob = scope.launch { state?.setTargetScene(scene, composeScope!!) if (state?.currentScene == BundleHeader.Scenes.Collapsed) { // [setTargetScene] does not immediately update [currentScene] so we must check [scene] if (scene == BundleHeader.Scenes.Collapsed) { repository.lastCollapseTime = systemClock.uptimeMillis() } } } private fun fetchAppIcon(appData: AppData): Drawable? { return try { Loading