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

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

Add basic implementation for Media Interactor

Flag: com.android.systemui.media_controls_in_compose
Bug: 397989775
Test: build.
Change-Id: Ief9476662fb4926855dea282686803c446f86de8
parent 2378d9ae
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,
        )
    }