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

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

Merge "Introduce pipeline interface and data layer" into main

parents 5ff50ebe ab5dc479
Loading
Loading
Loading
Loading
+43 −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

import android.R
import android.app.smartspace.SmartspaceAction
import android.content.Context
import android.graphics.drawable.Icon
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever

class MediaTestHelper {
    companion object {
        /** Returns a list of three mocked recommendations */
        fun getValidRecommendationList(context: Context): List<SmartspaceAction> {
            val mediaRecommendationItem =
                mock<SmartspaceAction> {
                    whenever(icon)
                        .thenReturn(
                            Icon.createWithResource(
                                context,
                                R.drawable.ic_media_play,
                            )
                        )
                }
            return listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem)
        }
    }
}
+124 −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.repository

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.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.MediaTestHelper
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class MediaDataRepositoryTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private val underTest: MediaDataRepository = kosmos.mediaDataRepository

    @Test
    fun setRecommendation() =
        testScope.runTest {
            val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
            val recommendation = SmartspaceMediaData(isActive = true)

            underTest.setRecommendation(recommendation)

            assertThat(smartspaceData).isEqualTo(recommendation)
        }

    @Test
    fun addAndRemoveMediaData() =
        testScope.runTest {
            val entries by collectLastValue(underTest.mediaEntries)

            val firstKey = "key1"
            val firstData = MediaData().copy(isPlaying = true)

            val secondKey = "key2"
            val secondData = MediaData().copy(resumption = true)

            underTest.addMediaEntry(firstKey, firstData)
            underTest.addMediaEntry(secondKey, secondData)
            underTest.addMediaEntry(firstKey, firstData.copy(isPlaying = false))

            assertThat(entries!!.size).isEqualTo(2)
            assertThat(entries!![firstKey]).isNotEqualTo(firstData)

            underTest.removeMediaEntry(firstKey)

            assertThat(entries!!.size).isEqualTo(1)
            assertThat(entries!![secondKey]).isEqualTo(secondData)
        }

    @Test
    fun setRecommendationInactive() =
        testScope.runTest {
            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, true)
            val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
            val recommendation =
                SmartspaceMediaData(
                    targetId = KEY_MEDIA_SMARTSPACE,
                    isActive = true,
                    recommendations = MediaTestHelper.getValidRecommendationList(context),
                )

            underTest.setRecommendation(recommendation)

            assertThat(smartspaceData).isEqualTo(recommendation)

            underTest.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)

            assertThat(smartspaceData).isNotEqualTo(recommendation)
            assertThat(smartspaceData!!.isActive).isFalse()
        }

    @Test
    fun dismissRecommendation() =
        testScope.runTest {
            val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
            val recommendation =
                SmartspaceMediaData(
                    targetId = KEY_MEDIA_SMARTSPACE,
                    isActive = true,
                    recommendations = MediaTestHelper.getValidRecommendationList(context),
                )

            underTest.setRecommendation(recommendation)

            assertThat(smartspaceData).isEqualTo(recommendation)

            underTest.dismissSmartspaceRecommendation(KEY_MEDIA_SMARTSPACE)

            assertThat(smartspaceData!!.isActive).isFalse()
        }

    companion object {
        private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
    }
}
+144 −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.repository

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.testScope
import com.android.systemui.media.controls.MediaTestHelper
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class MediaFilterRepositoryTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private val underTest: MediaFilterRepository = kosmos.mediaFilterRepository

    @Test
    fun addSelectedUserMediaEntry_activeThenInactivate() =
        testScope.runTest {
            val selectedUserEntries by collectLastValue(underTest.selectedUserEntries)

            val userMedia = MediaData().copy(active = true)

            underTest.addSelectedUserMediaEntry(KEY, userMedia)

            assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia)

            underTest.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false))

            assertThat(selectedUserEntries?.get(KEY)).isNotEqualTo(userMedia)
            assertThat(selectedUserEntries?.get(KEY)?.active).isFalse()
        }

    @Test
    fun addSelectedUserMediaEntry_thenRemove_returnsBoolean() =
        testScope.runTest {
            val selectedUserEntries by collectLastValue(underTest.selectedUserEntries)

            val userMedia = MediaData()

            underTest.addSelectedUserMediaEntry(KEY, userMedia)

            assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia)

            assertThat(underTest.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue()
        }

    @Test
    fun addSelectedUserMediaEntry_thenRemove_returnsValue() =
        testScope.runTest {
            val selectedUserEntries by collectLastValue(underTest.selectedUserEntries)

            val userMedia = MediaData()

            underTest.addSelectedUserMediaEntry(KEY, userMedia)

            assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia)

            assertThat(underTest.removeSelectedUserMediaEntry(KEY)).isEqualTo(userMedia)
        }

    @Test
    fun addAllUserMediaEntry_activeThenInactivate() =
        testScope.runTest {
            val allUserEntries by collectLastValue(underTest.allUserEntries)

            val userMedia = MediaData().copy(active = true)

            underTest.addMediaEntry(KEY, userMedia)

            assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia)

            underTest.addMediaEntry(KEY, userMedia.copy(active = false))

            assertThat(allUserEntries?.get(KEY)).isNotEqualTo(userMedia)
            assertThat(allUserEntries?.get(KEY)?.active).isFalse()
        }

    @Test
    fun addAllUserMediaEntry_thenRemove_returnsValue() =
        testScope.runTest {
            val allUserEntries by collectLastValue(underTest.allUserEntries)

            val userMedia = MediaData()

            underTest.addMediaEntry(KEY, userMedia)

            assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia)

            assertThat(underTest.removeMediaEntry(KEY)).isEqualTo(userMedia)
        }

    @Test
    fun addActiveRecommendation_thenInactive() =
        testScope.runTest {
            val smartspaceMediaData by collectLastValue(underTest.smartspaceMediaData)

            val mediaRecommendation =
                SmartspaceMediaData(
                    targetId = KEY_MEDIA_SMARTSPACE,
                    isActive = true,
                    recommendations = MediaTestHelper.getValidRecommendationList(context),
                )

            underTest.setRecommendation(mediaRecommendation)

            assertThat(smartspaceMediaData).isEqualTo(mediaRecommendation)

            underTest.setRecommendation(mediaRecommendation.copy(isActive = false))

            assertThat(smartspaceMediaData).isNotEqualTo(mediaRecommendation)
            assertThat(smartspaceMediaData?.isActive).isFalse()
        }

    companion object {
        private const val KEY = "KEY"
        private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
    }
}
+199 −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.domain.interactor

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.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.MediaTestHelper
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
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.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class MediaCarouselInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository
    private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor

    @Test
    fun addUserMediaEntry_activeThenInactivate() =
        testScope.runTest {
            val hasActiveMediaOrRecommendation by
                collectLastValue(underTest.hasActiveMediaOrRecommendation)
            val hasActiveMedia by collectLastValue(underTest.hasActiveMedia)
            val hasAnyMedia by collectLastValue(underTest.hasAnyMedia)

            val userMedia = MediaData().copy(active = true)

            mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia)

            assertThat(hasActiveMediaOrRecommendation).isTrue()
            assertThat(hasActiveMedia).isTrue()
            assertThat(hasAnyMedia).isTrue()

            mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false))

            assertThat(hasActiveMediaOrRecommendation).isFalse()
            assertThat(hasActiveMedia).isFalse()
            assertThat(hasAnyMedia).isTrue()
        }

    @Test
    fun addInactiveUserMediaEntry_thenRemove() =
        testScope.runTest {
            val hasActiveMediaOrRecommendation by
                collectLastValue(underTest.hasActiveMediaOrRecommendation)
            val hasActiveMedia by collectLastValue(underTest.hasActiveMedia)
            val hasAnyMedia by collectLastValue(underTest.hasAnyMedia)

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

            mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia)

            assertThat(hasActiveMediaOrRecommendation).isFalse()
            assertThat(hasActiveMedia).isFalse()
            assertThat(hasAnyMedia).isTrue()

            assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue()

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

    @Test
    fun addActiveRecommendation_inactiveMedia() =
        testScope.runTest {
            val hasActiveMediaOrRecommendation by
                collectLastValue(underTest.hasActiveMediaOrRecommendation)
            val hasAnyMediaOrRecommendation by
                collectLastValue(underTest.hasAnyMediaOrRecommendation)
            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)

            val userMediaRecommendation =
                SmartspaceMediaData(
                    targetId = KEY_MEDIA_SMARTSPACE,
                    isActive = true,
                    recommendations = MediaTestHelper.getValidRecommendationList(context),
                )
            val userMedia = MediaData().copy(active = false)

            mediaFilterRepository.setRecommendation(userMediaRecommendation)

            assertThat(hasActiveMediaOrRecommendation).isTrue()
            assertThat(hasAnyMediaOrRecommendation).isTrue()

            mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia)

            assertThat(hasActiveMediaOrRecommendation).isTrue()
            assertThat(hasAnyMediaOrRecommendation).isTrue()
        }

    @Test
    fun addActiveRecommendation_thenInactive() =
        testScope.runTest {
            val hasActiveMediaOrRecommendation by
                collectLastValue(underTest.hasActiveMediaOrRecommendation)
            val hasAnyMediaOrRecommendation by
                collectLastValue(underTest.hasAnyMediaOrRecommendation)
            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)

            val mediaRecommendation =
                SmartspaceMediaData(
                    targetId = KEY_MEDIA_SMARTSPACE,
                    isActive = true,
                    recommendations = MediaTestHelper.getValidRecommendationList(context),
                )

            mediaFilterRepository.setRecommendation(mediaRecommendation)

            assertThat(hasActiveMediaOrRecommendation).isTrue()
            assertThat(hasAnyMediaOrRecommendation).isTrue()

            mediaFilterRepository.setRecommendation(mediaRecommendation.copy(isActive = false))

            assertThat(hasActiveMediaOrRecommendation).isFalse()
            assertThat(hasAnyMediaOrRecommendation).isFalse()
        }

    @Test
    fun addActiveRecommendation_thenInvalid() =
        testScope.runTest {
            val hasActiveMediaOrRecommendation by
                collectLastValue(underTest.hasActiveMediaOrRecommendation)
            val hasAnyMediaOrRecommendation by
                collectLastValue(underTest.hasAnyMediaOrRecommendation)
            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)

            val mediaRecommendation =
                SmartspaceMediaData(
                    targetId = KEY_MEDIA_SMARTSPACE,
                    isActive = true,
                    recommendations = MediaTestHelper.getValidRecommendationList(context),
                )

            mediaFilterRepository.setRecommendation(mediaRecommendation)

            assertThat(hasActiveMediaOrRecommendation).isTrue()
            assertThat(hasAnyMediaOrRecommendation).isTrue()

            mediaFilterRepository.setRecommendation(
                mediaRecommendation.copy(recommendations = listOf())
            )

            assertThat(hasActiveMediaOrRecommendation).isFalse()
            assertThat(hasAnyMediaOrRecommendation).isFalse()
        }

    @Test
    fun hasAnyMedia_noMediaSet_returnsFalse() =
        testScope.runTest { assertThat(underTest.hasAnyMedia.value).isFalse() }

    @Test
    fun hasAnyMediaOrRecommendation_noMediaSet_returnsFalse() =
        testScope.runTest { assertThat(underTest.hasAnyMediaOrRecommendation.value).isFalse() }

    @Test
    fun hasActiveMedia_noMediaSet_returnsFalse() =
        testScope.runTest { assertThat(underTest.hasActiveMedia.value).isFalse() }

    @Test
    fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() =
        testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() }

    companion object {
        private const val KEY = "KEY"
        private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
    }
}
+123 −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.repository

import android.util.Log
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaFlags
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

private const val TAG = "MediaDataRepository"
private const val DEBUG = true

/** A repository that holds the state of all media controls in carousel. */
@SysUISingleton
class MediaDataRepository
@Inject
constructor(
    private val mediaFlags: MediaFlags,
    dumpManager: DumpManager,
) : Dumpable {

    private val _mediaEntries: MutableStateFlow<Map<String, MediaData>> =
        MutableStateFlow(LinkedHashMap())
    val mediaEntries: StateFlow<Map<String, MediaData>> = _mediaEntries.asStateFlow()

    private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> =
        MutableStateFlow(SmartspaceMediaData())
    val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow()

    init {
        dumpManager.registerNormalDumpable(TAG, this)
    }

    /** Updates the recommendation data with a new smartspace media data. */
    fun setRecommendation(recommendation: SmartspaceMediaData) {
        _smartspaceMediaData.value = recommendation
    }

    /**
     * Marks the recommendation data as inactive.
     *
     * @return true if the recommendation was actually marked as inactive, false otherwise.
     */
    fun setRecommendationInactive(key: String): Boolean {
        if (!mediaFlags.isPersistentSsCardEnabled()) {
            Log.e(TAG, "Only persistent recommendation can be inactive!")
            return false
        }
        if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")

        if (smartspaceMediaData.value.targetId != key || !smartspaceMediaData.value.isValid()) {
            // If this doesn't match, or we've already invalidated the data, no action needed
            return false
        }

        setRecommendation(smartspaceMediaData.value.copy(isActive = false))
        return true
    }

    /**
     * Marks the recommendation data as dismissed.
     *
     * @return true if the recommendation was dismissed or already inactive, false otherwise.
     */
    fun dismissSmartspaceRecommendation(key: String): Boolean {
        val data = smartspaceMediaData.value
        if (data.targetId != key || !data.isValid()) {
            // If this doesn't match, or we've already invalidated the data, no action needed
            return false
        }

        if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
        if (data.isActive) {
            setRecommendation(
                SmartspaceMediaData(
                    targetId = smartspaceMediaData.value.targetId,
                    instanceId = smartspaceMediaData.value.instanceId
                )
            )
        }
        return true
    }

    fun removeMediaEntry(key: String): MediaData? {
        val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value)
        val mediaData = entries.remove(key)
        _mediaEntries.value = entries
        return mediaData
    }

    fun addMediaEntry(key: String, data: MediaData): MediaData? {
        val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value)
        val mediaData = entries.put(key, data)
        _mediaEntries.value = entries
        return mediaData
    }

    override fun dump(pw: PrintWriter, args: Array<out String>) {
        pw.apply { println("mediaEntries: ${mediaEntries.value}") }
    }
}
Loading