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

Commit c245a427 authored by Lyn Han's avatar Lyn Han Committed by Android (Google) Code Review
Browse files

Merge "Fix bundle icon reshow after shade close" into main

parents 394a8f3f ed980e67
Loading
Loading
Loading
Loading
+97 −2
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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
    }

@@ -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)
        }
}
+25 −18
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
+23 −1
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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

@@ -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 {
@@ -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 {
+3 −0
Original line number Diff line number Diff line
@@ -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
@@ -32,6 +33,8 @@ interface BundleRowComponent {
    interface Builder {
        @BindsInstance fun bindBundleRepository(repository: BundleRepository): Builder

        @BindsInstance fun bindScope(scope: CoroutineScope): Builder

        fun build(): BundleRowComponent
    }
}
+37 −4
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
@@ -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>,
@@ -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