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

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

Merge "Add basic implementation for Media Interactor" into main

parents bbcf4ba0 c0e86554
Loading
Loading
Loading
Loading
+34 −20
Original line number Diff line number Diff line
@@ -16,32 +16,43 @@

package com.android.systemui.media.remedia.data.repository

import android.content.packageManager
import android.media.session.MediaController
import android.media.session.MediaSession
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.remedia.data.model.MediaDataModel
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MediaRepositoryTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val drawable = context.getDrawable(R.drawable.ic_music_note)!!
    private val kosmos =
        testKosmos().apply {
            whenever(packageManager.getApplicationIcon(anyString())).thenReturn(drawable)
            context.setMockPackageManager(packageManager)
        }
    private val testScope = kosmos.testScope
    private val session = MediaSession(context, "MediaRepositoryTestSession")

@@ -62,11 +73,11 @@ class MediaRepositoryTest : SysuiTestCase() {
                MediaData()
                    .copy(token = session.sessionToken, active = true, instanceId = instanceId)

            underTest.addCurrentUserMediaEntry(userMedia)
            addCurrentUserMediaEntry(userMedia)

            assertThat(currentUserEntries?.get(instanceId)).isEqualTo(userMedia)

            underTest.addCurrentUserMediaEntry(userMedia.copy(active = false))
            addCurrentUserMediaEntry(userMedia.copy(active = false))

            assertThat(currentUserEntries?.get(instanceId)).isNotEqualTo(userMedia)
            assertThat(currentUserEntries?.get(instanceId)?.active).isFalse()
@@ -80,7 +91,7 @@ class MediaRepositoryTest : SysuiTestCase() {
            val instanceId = InstanceId.fakeInstanceId(123)
            val userMedia = MediaData().copy(token = session.sessionToken, instanceId = instanceId)

            underTest.addCurrentUserMediaEntry(userMedia)
            addCurrentUserMediaEntry(userMedia)

            assertThat(currentUserEntries?.get(instanceId)).isEqualTo(userMedia)
            assertThat(underTest.removeCurrentUserMediaEntry(instanceId, userMedia)).isTrue()
@@ -94,7 +105,7 @@ class MediaRepositoryTest : SysuiTestCase() {
            val instanceId = InstanceId.fakeInstanceId(123)
            val userMedia = MediaData().copy(token = session.sessionToken, instanceId = instanceId)

            underTest.addCurrentUserMediaEntry(userMedia)
            addCurrentUserMediaEntry(userMedia)

            assertThat(currentUserEntries?.get(instanceId)).isEqualTo(userMedia)

@@ -140,9 +151,8 @@ class MediaRepositoryTest : SysuiTestCase() {
            val playingData = createMediaData("app1", true, LOCAL, false, playingInstanceId)
            val remoteData = createMediaData("app2", true, REMOTE, false, remoteInstanceId)

            underTest.addCurrentUserMediaEntry(playingData)
            underTest.addCurrentUserMediaEntry(remoteData)
            runCurrent()
            addCurrentUserMediaEntry(playingData)
            addCurrentUserMediaEntry(remoteData)

            assertThat(underTest.currentMedia.size).isEqualTo(2)
            assertThat(underTest.currentMedia)
@@ -161,9 +171,8 @@ class MediaRepositoryTest : SysuiTestCase() {
            var playingData1 = createMediaData("app1", true, LOCAL, false, playingInstanceId1)
            var playingData2 = createMediaData("app2", false, LOCAL, false, playingInstanceId2)

            underTest.addCurrentUserMediaEntry(playingData1)
            underTest.addCurrentUserMediaEntry(playingData2)
            runCurrent()
            addCurrentUserMediaEntry(playingData1)
            addCurrentUserMediaEntry(playingData2)

            assertThat(underTest.currentMedia.size).isEqualTo(2)
            assertThat(underTest.currentMedia)
@@ -176,9 +185,8 @@ class MediaRepositoryTest : SysuiTestCase() {
            playingData1 = createMediaData("app1", false, LOCAL, false, playingInstanceId1)
            playingData2 = createMediaData("app2", true, LOCAL, false, playingInstanceId2)

            underTest.addCurrentUserMediaEntry(playingData1)
            underTest.addCurrentUserMediaEntry(playingData2)
            runCurrent()
            addCurrentUserMediaEntry(playingData1)
            addCurrentUserMediaEntry(playingData2)

            assertThat(underTest.currentMedia.size).isEqualTo(2)
            assertThat(underTest.currentMedia)
@@ -214,15 +222,15 @@ class MediaRepositoryTest : SysuiTestCase() {
            val stoppedAndRemoteData = createMediaData("app4", false, REMOTE, false, instanceId4)
            val canResumeData = createMediaData("app5", false, LOCAL, true, instanceId5)

            underTest.addCurrentUserMediaEntry(stoppedAndLocalData)
            addCurrentUserMediaEntry(stoppedAndLocalData)

            underTest.addCurrentUserMediaEntry(stoppedAndRemoteData)
            addCurrentUserMediaEntry(stoppedAndRemoteData)

            underTest.addCurrentUserMediaEntry(canResumeData)
            addCurrentUserMediaEntry(canResumeData)

            underTest.addCurrentUserMediaEntry(playingAndLocalData)
            addCurrentUserMediaEntry(playingAndLocalData)

            underTest.addCurrentUserMediaEntry(playingAndRemoteData)
            addCurrentUserMediaEntry(playingAndRemoteData)

            underTest.reorderMedia()
            runCurrent()
@@ -239,6 +247,11 @@ class MediaRepositoryTest : SysuiTestCase() {
                .inOrder()
        }

    private fun TestScope.addCurrentUserMediaEntry(data: MediaData) {
        underTest.addCurrentUserMediaEntry(data)
        runCurrent()
    }

    private fun createMediaData(
        app: String,
        playing: Boolean,
@@ -248,6 +261,7 @@ class MediaRepositoryTest : SysuiTestCase() {
    ): MediaData {
        return MediaData(
            token = session.sessionToken,
            packageName = "packageName",
            playbackLocation = playbackLocation,
            resumption = isResume,
            notificationKey = "key: $app",
@@ -262,7 +276,7 @@ class MediaRepositoryTest : SysuiTestCase() {
            appUid = appUid,
            packageName = packageName,
            appName = app.toString(),
            appIcon = null,
            appIcon = Icon.Loaded(drawable, null),
            background = null,
            title = song.toString(),
            subtitle = artist.toString(),
+1 −1
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ data class MediaDataModel(
    /** Package name of the app that's posting the media, used for logging. */
    val packageName: String,
    val appName: String,
    val appIcon: Icon?,
    val appIcon: Icon,
    val background: Icon?,
    val title: String,
    val subtitle: String,
+54 −27
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.media.remedia.data.repository

import android.content.Context
import android.content.pm.PackageManager
import android.media.session.MediaController
import androidx.compose.runtime.getValue
import com.android.internal.logging.InstanceId
@@ -24,15 +25,21 @@ 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.Activatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.media.controls.data.model.MediaSortKeyModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.remedia.data.model.MediaDataModel
import com.android.systemui.res.R
import com.android.systemui.util.time.SystemClock
import java.util.TreeMap
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/** A repository that holds the state of current media on the device. */
interface MediaRepository : Activatable {
@@ -49,8 +56,12 @@ interface MediaRepository : Activatable {
@SysUISingleton
class MediaRepositoryImpl
@Inject
constructor(@Application private val context: Context, private val systemClock: SystemClock) :
    MediaRepository, MediaPipelineRepository() {
constructor(
    @Application private val applicationContext: Context,
    @Application private val applicationScope: CoroutineScope,
    @Background val backgroundDispatcher: CoroutineDispatcher,
    private val systemClock: SystemClock,
) : MediaRepository, MediaPipelineRepository() {

    private val hydrator = Hydrator(traceName = "MediaRepository.hydrator")
    private val mutableCurrentMedia: MutableStateFlow<List<MediaDataModel>> =
@@ -123,8 +134,10 @@ constructor(@Application private val context: Context, private val systemClock:
                    if (currentModel != null && currentModel.controller.sessionToken == token) {
                        currentModel.controller
                    } else {
                        MediaController(context, token!!)
                        MediaController(applicationContext, token!!)
                    }

                applicationScope.launch {
                    val mediaModel = toDataModel(controller)
                    sortedMap[sortKey] = mediaModel

@@ -151,6 +164,7 @@ constructor(@Application private val context: Context, private val systemClock:
                }
            }
        }
    }

    private fun removeFromSortedMedia(data: MediaData) {
        mutableCurrentMedia.value =
@@ -159,15 +173,17 @@ constructor(@Application private val context: Context, private val systemClock:
            TreeMap(sortedMedia.filter { (keyModel, _) -> keyModel.instanceId != data.instanceId })
    }

    private fun MediaData.toDataModel(controller: MediaController): MediaDataModel {
        val icon = appIcon?.loadDrawable(context)
        val background = artwork?.loadDrawable(context)
    private suspend fun MediaData.toDataModel(controller: MediaController): MediaDataModel {
        val icon = appIcon?.loadDrawable(applicationContext)
        val background = artwork?.loadDrawable(applicationContext)
        return MediaDataModel(
            instanceId = instanceId,
            appUid = appUid,
            packageName = packageName,
            appName = app.toString(),
            appIcon = icon?.let { Icon.Loaded(it, ContentDescription.Loaded(app)) },
            appIcon =
                icon?.let { Icon.Loaded(it, ContentDescription.Loaded(app)) }
                    ?: getAltIcon(packageName),
            background = background?.let { Icon.Loaded(background, null) },
            title = song.toString(),
            subtitle = artist.toString(),
@@ -183,4 +199,15 @@ constructor(@Application private val context: Context, private val systemClock:
            isExplicit = isExplicit,
        )
    }

    private suspend fun getAltIcon(packageName: String): Icon {
        return withContext(backgroundDispatcher) {
            try {
                val icon = applicationContext.packageManager.getApplicationIcon(packageName)
                Icon.Loaded(icon, null)
            } catch (exception: PackageManager.NameNotFoundException) {
                Icon.Resource(R.drawable.ic_music_note, null)
            }
        }
    }
}
+109 −0
Original line number Diff line number Diff line
@@ -16,7 +16,23 @@

package com.android.systemui.media.remedia.domain.interactor

import android.content.Context
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import com.android.systemui.biometrics.Utils.toBitmap
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.lifecycle.ExclusiveActivatable
import com.android.systemui.media.remedia.data.model.MediaDataModel
import com.android.systemui.media.remedia.data.repository.MediaRepository
import com.android.systemui.media.remedia.domain.model.MediaActionModel
import com.android.systemui.media.remedia.domain.model.MediaOutputDeviceModel
import com.android.systemui.media.remedia.domain.model.MediaSessionModel
import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout
import com.android.systemui.media.remedia.shared.model.MediaColorScheme
import com.android.systemui.media.remedia.shared.model.MediaSessionState
import javax.inject.Inject

/**
 * Defines interface for classes that can provide business logic in the domain of the media controls
@@ -36,3 +52,96 @@ interface MediaInteractor {
    /** Open media settings. */
    fun openMediaSettings()
}

@SysUISingleton
class MediaInteractorImpl
@Inject
constructor(@Application val applicationContext: Context, val repository: MediaRepository) :
    MediaInteractor, ExclusiveActivatable() {

    override val sessions: List<MediaSessionModel>
        get() = repository.currentMedia.map { toMediaSessionModel(it) }

    override fun seek(sessionKey: Any, to: Long) {
        TODO("Not yet implemented")
    }

    override fun hide(sessionKey: Any) {
        TODO("Not yet implemented")
    }

    override fun openMediaSettings() {
        TODO("Not yet implemented")
    }

    private fun toMediaSessionModel(dataModel: MediaDataModel): MediaSessionModel {
        return object : MediaSessionModel {
            override val key
                get() = dataModel.instanceId

            override val appName
                get() = dataModel.appName

            override val appIcon: Icon
                get() = dataModel.appIcon

            override val background: ImageBitmap?
                get() =
                    dataModel.background?.let {
                        (it as Icon.Loaded).drawable.toBitmap()?.asImageBitmap()
                    }

            override val colorScheme: MediaColorScheme
                get() = TODO("Not yet implemented")

            override val title: String
                get() = dataModel.title

            override val subtitle: String
                get() = dataModel.subtitle

            override val onClick: () -> Unit
                get() = TODO("Not yet implemented")

            override val isActive: Boolean
                get() = dataModel.isActive

            override val canBeHidden: Boolean
                get() = dataModel.canBeDismissed

            override val canBeScrubbed: Boolean
                get() = TODO("Not yet implemented")

            override val state: MediaSessionState
                get() = TODO("Not yet implemented")

            override val positionMs: Long
                get() = TODO("Not yet implemented")

            override val durationMs: Long
                get() = TODO("Not yet implemented")

            override val outputDevice: MediaOutputDeviceModel
                get() = TODO("Not yet implemented")

            override val actionButtonLayout: MediaCardActionButtonLayout
                get() = TODO("Not yet implemented")

            override val playPauseAction: MediaActionModel
                get() = TODO("Not yet implemented")

            override val leftAction: MediaActionModel
                get() = TODO("Not yet implemented")

            override val rightAction: MediaActionModel
                get() = TODO("Not yet implemented")

            override val additionalActions: List<MediaActionModel.Action>
                get() = TODO("Not yet implemented")
        }
    }

    override suspend fun onActivated(): Nothing {
        repository.activate()
    }
}
+10 −1
Original line number Diff line number Diff line
@@ -18,7 +18,16 @@ package com.android.systemui.media.remedia.data.repository

import android.content.applicationContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.util.time.systemClock

val Kosmos.mediaRepository by
    Kosmos.Fixture { MediaRepositoryImpl(context = applicationContext, systemClock = systemClock) }
    Kosmos.Fixture {
        MediaRepositoryImpl(
            applicationContext = applicationContext,
            applicationScope = applicationCoroutineScope,
            backgroundDispatcher = testDispatcher,
            systemClock = systemClock,
        )
    }