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

Commit 393f5840 authored by Ahmed Mehfooz's avatar Ahmed Mehfooz Committed by Android (Google) Code Review
Browse files

Merge "[ROSP] Add MediaControlChipViewModel" into main

parents cce324c2 b1ec019b
Loading
Loading
Loading
Loading
+82 −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 androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
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.PopupChipModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class MediaControlChipViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val underTest = kosmos.mediaControlChipViewModel

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

            assertThat(chip).isInstanceOf(PopupChipModel.Hidden::class.java)
        }

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

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

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

            assertThat(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)
            val instanceId = userMedia.instanceId

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

            assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName)

            val updatedUserMedia = userMedia.copy(song = newSongName)
            mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)

            assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName)
        }
}
+3 −3
Original line number Diff line number Diff line
@@ -41,14 +41,14 @@ import kotlinx.coroutines.flow.stateIn
class MediaControlChipInteractor
@Inject
constructor(
    @Background private val applicationScope: CoroutineScope,
    @Background private val backgroundScope: CoroutineScope,
    mediaFilterRepository: MediaFilterRepository,
) {
    private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> =
        mediaFilterRepository.currentMedia
            .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() }
            .stateIn(
                scope = applicationScope,
                scope = backgroundScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = emptyList(),
            )
@@ -64,7 +64,7 @@ constructor(
                    ?.toMediaControlChipModel()
            }
            .stateIn(
                scope = applicationScope,
                scope = backgroundScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = null,
            )
+87 −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.content.Context
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.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.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 kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

/**
 * [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
constructor(
    @Background private val backgroundScope: CoroutineScope,
    @Application private val applicationContext: Context,
    mediaControlChipInteractor: MediaControlChipInteractor,
) : StatusBarPopupChipViewModel {

    /**
     * A [StateFlow] of the current [PopupChipModel]. This flow emits a new [PopupChipModel]
     * whenever the underlying [MediaControlChipModel] changes.
     */
    override val chip: StateFlow<PopupChipModel> =
        mediaControlChipInteractor.mediaControlModel
            .map { mediaControlModel -> toPopupChipModel(mediaControlModel, applicationContext) }
            .stateIn(
                backgroundScope,
                SharingStarted.WhileSubscribed(),
                PopupChipModel.Hidden(PopupChipId.MediaControls),
            )
}

private fun toPopupChipModel(model: MediaControlChipModel?, context: Context): PopupChipModel {
    if (model == null || model.songName.isNullOrEmpty()) {
        return PopupChipModel.Hidden(PopupChipId.MediaControls)
    }

    val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) }
    return PopupChipModel.Shown(
        chipId = PopupChipId.MediaControls,
        icon =
            model.appIcon?.loadDrawable(context)?.let {
                Icon.Loaded(drawable = it, contentDescription = contentDescription)
            }
                ?: Icon.Resource(
                    res = com.android.internal.R.drawable.ic_audio_media,
                    contentDescription = contentDescription,
                ),
        chipText = model.songName.toString(),
        // TODO(b/385202114): Show a popup containing the media carousal when the chip is toggled.
        onToggle = {},
        // TODO(b/385202193): Add support for clicking on the icon on a media chip.
        onIconPressed = {},
    )
}
+2 −2
Original line number Diff line number Diff line
@@ -17,13 +17,13 @@
package com.android.systemui.statusbar.featurepods.media.domain.interactor

import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.media.controls.data.repository.mediaFilterRepository

val Kosmos.mediaControlChipInteractor: MediaControlChipInteractor by
    Kosmos.Fixture {
        MediaControlChipInteractor(
            applicationScope = applicationCoroutineScope,
            backgroundScope = backgroundScope,
            mediaFilterRepository = mediaFilterRepository,
        )
    }
+31 −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.content.testableContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor

val Kosmos.mediaControlChipViewModel: MediaControlChipViewModel by
    Kosmos.Fixture {
        MediaControlChipViewModel(
            backgroundScope = applicationCoroutineScope,
            applicationContext = testableContext,
            mediaControlChipInteractor = mediaControlChipInteractor,
        )
    }