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

Commit e8371b94 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[ROSP] Use Hydrator and AssistedInject for MediaControlChipViewModel" into main

parents 4ac177fc a0b5f9bc
Loading
Loading
Loading
Loading
+10 −11
Original line number Diff line number Diff line
@@ -21,9 +21,10 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.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.domain.pipeline.MediaDataManager
import com.android.systemui.media.controls.shared.model.MediaData
@@ -47,7 +48,8 @@ import platform.test.runner.parameterized.Parameters
class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val mediaControlChipInteractor by lazy { kosmos.mediaControlChipInteractor }
    private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipViewModel }
    private val Kosmos.underTest by
        Kosmos.Fixture { kosmos.mediaControlChipViewModelFactory.create() }
    @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>

    companion object {
@@ -62,6 +64,7 @@ class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCas
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        mediaControlChipInteractor.initialize()
        kosmos.underTest.activateIn(kosmos.testScope)
    }

    init {
@@ -71,7 +74,7 @@ class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCas
    @Test
    fun chip_noActiveMedia_IsHidden() =
        kosmos.runTest {
            val chip by collectLastValue(underTest.chip)
            val chip = underTest.chip

            assertThat(chip).isInstanceOf(PopupChipModel.Hidden::class.java)
        }
@@ -79,30 +82,26 @@ class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCas
    @Test
    fun chip_activeMedia_IsShown() =
        kosmos.runTest {
            val chip by collectLastValue(underTest.chip)

            val userMedia = MediaData(active = true, song = "test")
            updateMedia(userMedia)

            assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java)
            assertThat(underTest.chip).isInstanceOf(PopupChipModel.Shown::class.java)
        }

    @Test
    fun chip_songNameChanges_chipTextUpdated() =
        kosmos.runTest {
            val chip by collectLastValue(underTest.chip)

            val initialSongName = "Initial Song"
            val newSongName = "New Song"
            val userMedia = MediaData(active = true, song = initialSongName)
            updateMedia(userMedia)
            assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java)
            assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName)
            assertThat(underTest.chip).isInstanceOf(PopupChipModel.Shown::class.java)
            assertThat((underTest.chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName)

            val updatedUserMedia = userMedia.copy(song = newSongName)
            updateMedia(updatedUserMedia)

            assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName)
            assertThat((underTest.chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName)
        }

    private fun updateMedia(mediaData: MediaData) {
+95 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.statusbar.featurepods.media.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.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.StatusBarPopupChips
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModelFactory
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
@EnableFlags(StatusBarPopupChips.FLAG_NAME)
@RunWith(AndroidJUnit4::class)
class StatusBarPopupChipsViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val underTest = kosmos.statusBarPopupChipsViewModelFactory.create()

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

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

    @Test
    fun shownPopupChips_activeMedia_restHidden_mediaControlChipShown() =
        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)
            }
        }

    @Test
    fun shownPopupChips_mediaChipToggled_popupShown() =
        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)
                val mediaChip = shownPopupChips.first()
                assertThat(mediaChip.isPopupShown).isFalse()

                mediaChip.showPopup.invoke()
                assertThat(shownPopupChips.first().isPopupShown).isTrue()
            }
        }
}
+28 −23
Original line number Diff line number Diff line
@@ -17,52 +17,52 @@
package com.android.systemui.statusbar.featurepods.media.ui.viewmodel

import android.content.Context
import androidx.compose.runtime.getValue
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor
import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel
import com.android.systemui.statusbar.featurepods.popups.shared.model.HoverBehavior
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/**
 * [StatusBarPopupChipViewModel] for a media control chip in the status bar. This view model is
 * responsible for converting the [MediaControlChipModel] to a [PopupChipModel] that can be used to
 * display a media control chip.
 */
@SysUISingleton
class MediaControlChipViewModel
@Inject
@AssistedInject
constructor(
    @Background private val backgroundScope: CoroutineScope,
    @Application private val applicationContext: Context,
    mediaControlChipInteractor: MediaControlChipInteractor,
) : StatusBarPopupChipViewModel {

) : StatusBarPopupChipViewModel, ExclusiveActivatable() {
    private val hydrator: Hydrator = Hydrator("MediaControlChipViewModel.hydrator")
    /**
     * A [StateFlow] of the current [PopupChipModel]. This flow emits a new [PopupChipModel]
     * A snapshot [State] of the current [PopupChipModel]. This emits a new [PopupChipModel]
     * whenever the underlying [MediaControlChipModel] changes.
     */
    override val chip: StateFlow<PopupChipModel> =
        mediaControlChipInteractor.mediaControlChipModel
            .map { mediaControlChipModel -> toPopupChipModel(mediaControlChipModel) }
            .stateIn(
                backgroundScope,
                SharingStarted.WhileSubscribed(),
                PopupChipModel.Hidden(PopupChipId.MediaControl),
    override val chip: PopupChipModel by
        hydrator.hydratedStateOf(
            traceName = "chip",
            initialValue = PopupChipModel.Hidden(PopupChipId.MediaControl),
            source =
                mediaControlChipInteractor.mediaControlChipModel.map { model ->
                    toPopupChipModel(model)
                },
        )

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

    private fun toPopupChipModel(model: MediaControlChipModel?): PopupChipModel {
        if (model == null || model.songName.isNullOrEmpty()) {
            return PopupChipModel.Hidden(PopupChipId.MediaControl)
@@ -96,7 +96,12 @@ constructor(

        return HoverBehavior.Button(
            icon = Icon.Loaded(drawable = icon, contentDescription = contentDescription),
            onIconPressed = { backgroundScope.launch { action.run() } },
            onIconPressed = { action.run() },
        )
    }

    @AssistedFactory
    interface Factory {
        fun create(): MediaControlChipViewModel
    }
}
+4 −4
Original line number Diff line number Diff line
@@ -16,14 +16,14 @@

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

import com.android.systemui.lifecycle.Activatable
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
import kotlinx.coroutines.flow.StateFlow

/**
 * Interface for a view model that knows the display requirements for a single type of status bar
 * popup chip.
 */
interface StatusBarPopupChipViewModel {
    /** A flow modeling the popup chip that should be shown (or not shown). */
    val chip: StateFlow<PopupChipModel>
interface StatusBarPopupChipViewModel : Activatable {
    /** A snapshot [State] modeling the popup chip that should be shown (or not shown). */
    val chip: PopupChipModel
}
+8 −10
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
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
@@ -36,18 +35,17 @@ import kotlinx.coroutines.flow.map
 */
class StatusBarPopupChipsViewModel
@AssistedInject
constructor(mediaControlChip: MediaControlChipViewModel) : ExclusiveActivatable() {
    private val hydrator: Hydrator = Hydrator("StatusBarPopupChipsViewModel.hydrator")
constructor(mediaControlChipFactory: MediaControlChipViewModel.Factory) : ExclusiveActivatable() {

    private val mediaControlChip by lazy { mediaControlChipFactory.create() }

    /** The ID of the current chip that is showing its popup, or `null` if no chip is shown. */
    private var currentShownPopupChipId by mutableStateOf<PopupChipId?>(null)

    private val incomingPopupChipBundle: PopupChipBundle by
        hydrator.hydratedStateOf(
            traceName = "incomingPopupChipBundle",
            initialValue = PopupChipBundle(),
            source = mediaControlChip.chip.map { chip -> PopupChipBundle(media = chip) },
        )
    private val incomingPopupChipBundle: PopupChipBundle by derivedStateOf {
        val mediaChip = mediaControlChip.chip
        PopupChipBundle(media = mediaChip)
    }

    val shownPopupChips: List<PopupChipModel.Shown> by derivedStateOf {
        if (StatusBarPopupChips.isEnabled) {
@@ -66,7 +64,7 @@ constructor(mediaControlChip: MediaControlChipViewModel) : ExclusiveActivatable(
    }

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

    private data class PopupChipBundle(
Loading