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

Commit 274c515f authored by Michael Mikhail's avatar Michael Mikhail Committed by Android (Google) Code Review
Browse files

Merge "Sort media when loaded or removed" into main

parents dd15c42d e3c06c2b
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