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

Commit 19796556 authored by Ahmed Mehfooz's avatar Ahmed Mehfooz
Browse files

[ROSP] Use Hydrator and AssistedInject for StatusBarPopupChipsViewModel

This change refactors `StatusBarPopupChipsViewModel` to utilize snapshot
state instead of exposing a flow.

Bug: b/394887130
Flag: com.android.systemui.status_bar_popup_chips

Test: Make sure popup chips show up in the status bar.

Change-Id: I89dc8971fcaa39992f7065526aa14a312f2d22c7
parent dc648413
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -57,7 +57,7 @@ class FakeHomeStatusBarViewModel(
    override val ongoingActivityChipsLegacy =
        MutableStateFlow(MultipleOngoingActivityChipsModelLegacy())

    override val statusBarPopupChips = MutableStateFlow(emptyList<PopupChipModel.Shown>())
    override val popupChips = emptyList<PopupChipModel.Shown>()

    override val mediaProjectionStopDialogDueToCallEndedState =
        MutableStateFlow(MediaProjectionStopDialogModel.Hidden)
+33 −29
Original line number Diff line number Diff line
@@ -16,49 +16,53 @@

package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.MediaControlChipViewModel
import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

/**
 * View model deciding which system process chips to show in the status bar. Emits a list of
 * PopupChipModels.
 */
@SysUISingleton
class StatusBarPopupChipsViewModel
@Inject
constructor(
    @Background scope: CoroutineScope,
    mediaControlChipViewModel: MediaControlChipViewModel,
) {
    private data class PopupChipBundle(
        val media: PopupChipModel = PopupChipModel.Hidden(chipId = PopupChipId.MediaControl)
    )
@AssistedInject
constructor(mediaControlChip: MediaControlChipViewModel) : ExclusiveActivatable() {
    private val hydrator: Hydrator = Hydrator("StatusBarPopupChipsViewModel.hydrator")

    private val incomingPopupChipBundle: StateFlow<PopupChipBundle?> =
        mediaControlChipViewModel.chip
            .map { chip -> PopupChipBundle(media = chip) }
            .stateIn(scope, SharingStarted.WhileSubscribed(), PopupChipBundle())
    private val incomingPopupChipBundle: PopupChipBundle by
        hydrator.hydratedStateOf(
            traceName = "incomingPopupChipBundle",
            initialValue = PopupChipBundle(),
            source = mediaControlChip.chip.map { chip -> PopupChipBundle(media = chip) },
        )

    val shownPopupChips: StateFlow<List<PopupChipModel.Shown>> =
    val shownPopupChips: List<PopupChipModel.Shown> by derivedStateOf {
        if (StatusBarPopupChips.isEnabled) {
            incomingPopupChipBundle
                .map { bundle ->
                    listOfNotNull(bundle?.media).filterIsInstance<PopupChipModel.Shown>()
                }
                .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
            val bundle = incomingPopupChipBundle
            listOfNotNull(bundle.media).filterIsInstance<PopupChipModel.Shown>()
        } else {
            MutableStateFlow(emptyList<PopupChipModel.Shown>()).asStateFlow()
            emptyList()
        }
    }

    override suspend fun onActivated(): Nothing {
        hydrator.activate()
    }

    private data class PopupChipBundle(
        val media: PopupChipModel = PopupChipModel.Hidden(chipId = PopupChipId.MediaControl)
    )

    @AssistedFactory
    interface Factory {
        fun create(): StatusBarPopupChipsViewModel
    }
}
+3 −4
Original line number Diff line number Diff line
@@ -255,10 +255,9 @@ fun StatusBarRoot(
                                )

                                setContent {
                                    val chips =
                                        statusBarViewModel.statusBarPopupChips
                                            .collectAsStateWithLifecycle()
                                    StatusBarPopupChipsContainer(chips = chips.value)
                                    StatusBarPopupChipsContainer(
                                        chips = statusBarViewModel.popupChips
                                    )
                                }
                            }
                        endSideContent.addView(composeView, 0)
+19 −5
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.annotation.ColorInt
import android.graphics.Rect
import android.view.View
import androidx.compose.runtime.getValue
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -29,6 +30,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.lifecycle.Activatable
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.log.table.TableLogBufferFactory
@@ -49,6 +51,7 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel
import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor
import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel
import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior
@@ -71,6 +74,8 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -94,7 +99,7 @@ import kotlinx.coroutines.flow.stateIn
 * [StatusBarHideIconsForBouncerManager]. We should move those pieces of logic to this class instead
 * so that it's all in one place and easily testable outside of the fragment.
 */
interface HomeStatusBarViewModel {
interface HomeStatusBarViewModel : Activatable {
    /** Factory to create the view model for the battery icon */
    val batteryViewModelFactory: BatteryViewModel.Factory

@@ -133,7 +138,7 @@ interface HomeStatusBarViewModel {
    val operatorNameViewModel: StatusBarOperatorNameViewModel

    /** The popup chips that should be shown on the right-hand side of the status bar. */
    val statusBarPopupChips: StateFlow<List<PopupChipModel.Shown>>
    val popupChips: List<PopupChipModel.Shown>

    /**
     * True if the current scene can show the home status bar (aka this status bar), and false if
@@ -208,7 +213,7 @@ constructor(
    shadeInteractor: ShadeInteractor,
    shareToAppChipViewModel: ShareToAppChipViewModel,
    ongoingActivityChipsViewModel: OngoingActivityChipsViewModel,
    statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel,
    statusBarPopupChipsViewModelFactory: StatusBarPopupChipsViewModel.Factory,
    animations: SystemStatusEventAnimationInteractor,
    statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore,
    @Background bgScope: CoroutineScope,
@@ -219,6 +224,8 @@ constructor(

    val tableLogger = tableLoggerFactory.getOrCreate(tableLogBufferName(thisDisplayId), 200)

    private val statusBarPopupChips by lazy { statusBarPopupChipsViewModelFactory.create() }

    override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> =
        keyguardTransitionInteractor
            .isInTransition(Edge.create(from = LOCKSCREEN, to = OCCLUDED))
@@ -246,7 +253,8 @@ constructor(

    override val ongoingActivityChipsLegacy = ongoingActivityChipsViewModel.chipsLegacy

    override val statusBarPopupChips = statusBarPopupChipsViewModel.shownPopupChips
    override val popupChips
        get() = statusBarPopupChips.shownPopupChips

    override val isHomeStatusBarAllowedByScene: StateFlow<Boolean> =
        combine(
@@ -495,7 +503,13 @@ constructor(
    private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE

    override suspend fun onActivated(): Nothing {
        hydrator.activate()
        coroutineScope {
            launch { hydrator.activate() }
            if (StatusBarPopupChips.isEnabled) {
                launch { statusBarPopupChips.activate() }
            }
            awaitCancellation()
        }
    }

    /** Inject this to create the display-dependent view model */
+21 −13
Original line number Diff line number Diff line
@@ -17,19 +17,23 @@
package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel

import android.platform.test.annotations.EnableFlags
import androidx.compose.runtime.snapshots.Snapshot
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
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.media.controls.shared.model.MediaDataLoadingModel
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@@ -37,30 +41,34 @@ import org.junit.runner.RunWith
@EnableFlags(StatusBarPopupChips.FLAG_NAME)
@RunWith(AndroidJUnit4::class)
class StatusBarPopupChipsViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val mediaFilterRepository = kosmos.mediaFilterRepository
    private val underTest = kosmos.statusBarPopupChipsViewModel
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val underTest = kosmos.statusBarPopupChipsViewModelFactory.create()

    @Before
    fun setUp() {
        underTest.activateIn(kosmos.testScope)
    }

    @Test
    fun shownPopupChips_allHidden_empty() =
        testScope.runTest {
            val shownPopupChips by collectLastValue(underTest.shownPopupChips)
        kosmos.runTest {
            val shownPopupChips = underTest.shownPopupChips
            assertThat(shownPopupChips).isEmpty()
        }

    @Test
    fun shownPopupChips_activeMedia_restHidden_mediaControlChipShown() =
        testScope.runTest {
            val shownPopupChips by collectLastValue(underTest.shownPopupChips)

        kosmos.runTest {
            val shownPopupChips = underTest.shownPopupChips
            val userMedia = MediaData(active = true, song = "test")
            val instanceId = userMedia.instanceId

            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))

            Snapshot.takeSnapshot {
                assertThat(shownPopupChips).hasSize(1)
            assertThat(shownPopupChips!!.first().chipId).isEqualTo(PopupChipId.MediaControl)
                assertThat(shownPopupChips.first().chipId).isEqualTo(PopupChipId.MediaControl)
            }
        }
}
Loading