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

Commit e3c06c2b authored by Michael Mikhail's avatar Michael Mikhail
Browse files

Sort media when loaded or removed

Flag: ACONFIG media_controls_refactor DISABLED
Bug: 326281896
Test: atest SystemUiRoboTests:MediaFilterRepositoryTest
Test: atest SystemUiRoboTests:MediaCarouselInteractorTest
Change-Id: I840c2fa506cf2e2de22fff5c49e7b126563c7723
parent 3473ec33
Loading
Loading
Loading
Loading
+137 −1
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.MediaTestHelper
import com.android.systemui.media.controls.shared.model.MediaCommonModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -172,12 +173,147 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
            val recommendationsLoadingModel =
                SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE)

            underTest.setRecommedationsLoadingState(recommendationsLoadingModel)
            underTest.setRecommendationsLoadingState(recommendationsLoadingModel)

            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
        }

    @Test
    fun addMediaControlPlayingThenRemote() =
        testScope.runTest {
            val sortedMedia by collectLastValue(underTest.sortedMedia)
            val playingInstanceId = InstanceId.fakeInstanceId(123)
            val remoteInstanceId = InstanceId.fakeInstanceId(321)
            val playingData = createMediaData("app1", true, LOCAL, false, playingInstanceId)
            val remoteData = createMediaData("app2", true, REMOTE, false, remoteInstanceId)

            underTest.addSelectedUserMediaEntry(playingData)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId))
            underTest.addSelectedUserMediaEntry(remoteData)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(remoteInstanceId))

            assertThat(sortedMedia?.size).isEqualTo(2)
            assertThat(sortedMedia?.values)
                .containsExactly(
                    MediaCommonModel.MediaControl(playingInstanceId),
                    MediaCommonModel.MediaControl(remoteInstanceId)
                )
        }

    @Test
    fun switchMediaControlsPlaying() =
        testScope.runTest {
            val sortedMedia by collectLastValue(underTest.sortedMedia)
            val playingInstanceId1 = InstanceId.fakeInstanceId(123)
            val playingInstanceId2 = InstanceId.fakeInstanceId(321)
            var playingData1 = createMediaData("app1", true, LOCAL, false, playingInstanceId1)
            var playingData2 = createMediaData("app2", false, LOCAL, false, playingInstanceId2)

            underTest.addSelectedUserMediaEntry(playingData1)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId1))
            underTest.addSelectedUserMediaEntry(playingData2)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId2))

            assertThat(sortedMedia?.size).isEqualTo(2)
            assertThat(sortedMedia?.values)
                .containsExactly(
                    MediaCommonModel.MediaControl(playingInstanceId1),
                    MediaCommonModel.MediaControl(playingInstanceId2)
                )
                .inOrder()

            playingData1 = createMediaData("app1", false, LOCAL, false, playingInstanceId1)
            playingData2 = createMediaData("app2", true, LOCAL, false, playingInstanceId2)

            underTest.addSelectedUserMediaEntry(playingData1)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId1))
            underTest.addSelectedUserMediaEntry(playingData2)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId2))

            assertThat(sortedMedia?.size).isEqualTo(2)
            assertThat(sortedMedia?.values)
                .containsExactly(
                    MediaCommonModel.MediaControl(playingInstanceId2),
                    MediaCommonModel.MediaControl(playingInstanceId1)
                )
                .inOrder()
        }

    @Test
    fun fullOrderTest() =
        testScope.runTest {
            val sortedMedia by collectLastValue(underTest.sortedMedia)
            val instanceId1 = InstanceId.fakeInstanceId(123)
            val instanceId2 = InstanceId.fakeInstanceId(456)
            val instanceId3 = InstanceId.fakeInstanceId(321)
            val instanceId4 = InstanceId.fakeInstanceId(654)
            val instanceId5 = InstanceId.fakeInstanceId(124)
            val playingAndLocalData = createMediaData("app1", true, LOCAL, false, instanceId1)
            val playingAndRemoteData = createMediaData("app2", true, REMOTE, false, instanceId2)
            val stoppedAndLocalData = createMediaData("app3", false, LOCAL, false, instanceId3)
            val stoppedAndRemoteData = createMediaData("app4", false, REMOTE, false, instanceId4)
            val canResumeData = createMediaData("app5", false, LOCAL, true, instanceId5)

            val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
            val mediaRecommendations =
                SmartspaceMediaData(
                    targetId = KEY_MEDIA_SMARTSPACE,
                    isActive = true,
                    recommendations = MediaTestHelper.getValidRecommendationList(icon),
                )

            underTest.addSelectedUserMediaEntry(stoppedAndLocalData)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId3))

            underTest.addSelectedUserMediaEntry(stoppedAndRemoteData)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId4))

            underTest.addSelectedUserMediaEntry(canResumeData)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId5))

            underTest.addSelectedUserMediaEntry(playingAndLocalData)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId1))

            underTest.addSelectedUserMediaEntry(playingAndRemoteData)
            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId2))

            underTest.setRecommendation(mediaRecommendations)
            underTest.setRecommendationsLoadingState(
                SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
            )

            assertThat(sortedMedia?.size).isEqualTo(6)
            assertThat(sortedMedia?.values)
                .containsExactly(
                    MediaCommonModel.MediaControl(instanceId1),
                    MediaCommonModel.MediaControl(instanceId2),
                    MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE),
                    MediaCommonModel.MediaControl(instanceId4),
                    MediaCommonModel.MediaControl(instanceId3),
                    MediaCommonModel.MediaControl(instanceId5),
                )
                .inOrder()
        }

    private fun createMediaData(
        app: String,
        playing: Boolean,
        playbackLocation: Int,
        isResume: Boolean,
        instanceId: InstanceId,
    ): MediaData {
        return MediaData(
            playbackLocation = playbackLocation,
            resumption = isResume,
            notificationKey = "key: $app",
            isPlaying = playing,
            instanceId = instanceId
        )
    }

    companion object {
        private const val LOCAL = MediaData.PLAYBACK_LOCAL
        private const val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
        private const val KEY = "KEY"
        private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
    }
+23 −0
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
import com.android.systemui.media.controls.shared.model.MediaCommonModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -94,22 +95,29 @@ class MediaCarouselInteractorTest : SysuiTestCase() {
                collectLastValue(underTest.hasActiveMediaOrRecommendation)
            val hasActiveMedia by collectLastValue(underTest.hasActiveMedia)
            val hasAnyMedia by collectLastValue(underTest.hasAnyMedia)
            val sortedMedia by collectLastValue(underTest.sortedMedia)

            val userMedia = MediaData(active = false)
            val instanceId = userMedia.instanceId

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

            assertThat(hasActiveMediaOrRecommendation).isFalse()
            assertThat(hasActiveMedia).isFalse()
            assertThat(hasAnyMedia).isTrue()
            assertThat(sortedMedia).containsExactly(MediaCommonModel.MediaControl(instanceId))

            assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia))
                .isTrue()
            mediaFilterRepository.addMediaDataLoadingState(
                MediaDataLoadingModel.Removed(instanceId)
            )

            assertThat(hasActiveMediaOrRecommendation).isFalse()
            assertThat(hasActiveMedia).isFalse()
            assertThat(hasAnyMedia).isFalse()
            assertThat(sortedMedia).isEmpty()
        }

    @Test
@@ -119,6 +127,7 @@ class MediaCarouselInteractorTest : SysuiTestCase() {
                collectLastValue(underTest.hasActiveMediaOrRecommendation)
            val hasAnyMediaOrRecommendation by
                collectLastValue(underTest.hasAnyMediaOrRecommendation)
            val sortedMedia by collectLastValue(underTest.sortedMedia)
            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)

            val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
@@ -131,14 +140,28 @@ class MediaCarouselInteractorTest : SysuiTestCase() {
            val userMedia = MediaData(active = false)

            mediaFilterRepository.setRecommendation(userMediaRecommendation)
            mediaFilterRepository.setRecommendationsLoadingState(
                SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
            )

            assertThat(hasActiveMediaOrRecommendation).isTrue()
            assertThat(hasAnyMediaOrRecommendation).isTrue()
            assertThat(sortedMedia)
                .containsExactly(MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE))

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

            assertThat(hasActiveMediaOrRecommendation).isTrue()
            assertThat(hasAnyMediaOrRecommendation).isTrue()
            assertThat(sortedMedia)
                .containsExactly(
                    MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE),
                    MediaCommonModel.MediaControl(userMedia.instanceId)
                )
                .inOrder()
        }

    @Test
+33 −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.media.controls.data.model

import com.android.internal.logging.InstanceId
import com.android.systemui.media.controls.shared.model.MediaData.Companion.PLAYBACK_LOCAL

data class MediaSortKeyModel(
    /** Whether the item represents a Smartspace media recommendation that should be prioritized. */
    val isPrioritizedRec: Boolean = false,
    val isPlaying: Boolean? = null,
    val playbackLocation: Int = PLAYBACK_LOCAL,
    val active: Boolean = true,
    val isResume: Boolean = false,
    val lastActive: Long = 0L,
    val notificationKey: String? = null,
    val updateTime: Long = 0,
    val instanceId: InstanceId? = null,
)
+108 −2
Original line number Diff line number Diff line
@@ -18,10 +18,14 @@ package com.android.systemui.media.controls.data.repository

import com.android.internal.logging.InstanceId
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.media.controls.data.model.MediaSortKeyModel
import com.android.systemui.media.controls.shared.model.MediaCommonModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
import com.android.systemui.util.time.SystemClock
import java.util.TreeMap
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -29,7 +33,7 @@ import kotlinx.coroutines.flow.asStateFlow

/** A repository that holds the state of filtered media data on the device. */
@SysUISingleton
class MediaFilterRepository @Inject constructor() {
class MediaFilterRepository @Inject constructor(private val systemClock: SystemClock) {

    /** Instance id of media control that recommendations card reactivated. */
    private val _reactivatedId: MutableStateFlow<InstanceId?> = MutableStateFlow(null)
@@ -58,6 +62,26 @@ class MediaFilterRepository @Inject constructor() {
    val recommendationsLoadingState: StateFlow<SmartspaceMediaLoadingModel> =
        _recommendationsLoadingState.asStateFlow()

    private val comparator =
        compareByDescending<MediaSortKeyModel> {
                it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_LOCAL
            }
            .thenByDescending {
                it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL
            }
            .thenByDescending { it.active }
            .thenByDescending { it.isPrioritizedRec }
            .thenByDescending { !it.isResume }
            .thenByDescending { it.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
            .thenByDescending { it.lastActive }
            .thenByDescending { it.updateTime }
            .thenByDescending { it.notificationKey }

    private val _sortedMedia: MutableStateFlow<TreeMap<MediaSortKeyModel, MediaCommonModel>> =
        MutableStateFlow(TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator))
    val sortedMedia: StateFlow<Map<MediaSortKeyModel, MediaCommonModel>> =
        _sortedMedia.asStateFlow()

    fun addMediaEntry(key: String, data: MediaData) {
        val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
        entries[key] = data
@@ -138,9 +162,91 @@ class MediaFilterRepository @Inject constructor() {
                } else {
                    emptyList()
                }

        addMediaLoadingToSortedMap(mediaDataLoadingModel)
    }

    fun setRecommedationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) {
    fun setRecommendationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) {
        _recommendationsLoadingState.value = smartspaceMediaLoadingModel

        addRecsLoadingToSortedMap(smartspaceMediaLoadingModel)
    }

    private fun addMediaLoadingToSortedMap(mediaDataLoadingModel: MediaDataLoadingModel) {
        val instanceId =
            when (mediaDataLoadingModel) {
                is MediaDataLoadingModel.Loaded -> mediaDataLoadingModel.instanceId
                is MediaDataLoadingModel.Removed -> mediaDataLoadingModel.instanceId
                MediaDataLoadingModel.Unknown -> null
            }
        val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
        sortedMap.putAll(
            _sortedMedia.value.filter { (_, commonModel) ->
                commonModel !is MediaCommonModel.MediaControl ||
                    commonModel.instanceId != instanceId
            }
        )

        _selectedUserEntries.value[instanceId]?.let {
            val sortKey =
                MediaSortKeyModel(
                    isPrioritizedRec = false,
                    it.isPlaying,
                    it.playbackLocation,
                    it.active,
                    it.resumption,
                    it.lastActive,
                    it.notificationKey,
                    systemClock.currentTimeMillis(),
                    it.instanceId,
                )

            if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) {
                sortedMap[sortKey] = MediaCommonModel.MediaControl(it.instanceId)
            }
        }

        _sortedMedia.value = sortedMap
    }

    private fun addRecsLoadingToSortedMap(
        smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel
    ) {
        val isPrioritized: Boolean
        val key: String?
        when (smartspaceMediaLoadingModel) {
            is SmartspaceMediaLoadingModel.Loaded -> {
                isPrioritized = smartspaceMediaLoadingModel.isPrioritized
                key = smartspaceMediaLoadingModel.key
            }
            is SmartspaceMediaLoadingModel.Removed -> {
                isPrioritized = false
                key = smartspaceMediaLoadingModel.key
            }
            SmartspaceMediaLoadingModel.Unknown -> {
                isPrioritized = false
                key = null
            }
        }
        val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
        sortedMap.putAll(
            _sortedMedia.value.filter { (_, commonModel) ->
                commonModel !is MediaCommonModel.MediaRecommendations || commonModel.key != key
            }
        )

        key?.let {
            val sortKey =
                MediaSortKeyModel(
                    isPrioritizedRec = isPrioritized,
                    isPlaying = false,
                    active = _smartspaceMediaData.value.isActive,
                )
            if (smartspaceMediaLoadingModel is SmartspaceMediaLoadingModel.Loaded) {
                sortedMap[sortKey] = MediaCommonModel.MediaRecommendations(key)
            }
        }

        _sortedMedia.value = sortedMap
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -186,7 +186,7 @@ constructor(
            smartspaceMediaData.packageName,
            smartspaceMediaData.instanceId
        )
        mediaFilterRepository.setRecommedationsLoadingState(
        mediaFilterRepository.setRecommendationsLoadingState(
            SmartspaceMediaLoadingModel.Loaded(key, shouldPrioritizeMutable)
        )
    }
@@ -224,7 +224,7 @@ constructor(
                )
            )
        }
        mediaFilterRepository.setRecommedationsLoadingState(
        mediaFilterRepository.setRecommendationsLoadingState(
            SmartspaceMediaLoadingModel.Removed(key, immediately)
        )
    }
Loading