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

Commit 9d79df7a authored by Michael Mikhail's avatar Michael Mikhail
Browse files

Apply media mutex to avoid race condition

Flag: com.android.systemui.media_controls_in_compose
Bug: 397989775
Test: Checked UI, no crashes.
Test: atest SystemUiRoboTests:MediaRepositoryTest
Change-Id: Ief78f80ca979c9d0761460eaea6fcd3561030d46
parent 31f04d4d
Loading
Loading
Loading
Loading
+103 −70
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.graphics.Color
import com.android.internal.annotations.GuardedBy
import com.android.internal.logging.InstanceId
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -56,6 +57,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext

/** A repository that holds the state of current media on the device. */
@@ -97,6 +100,7 @@ constructor(

    override var shouldScrollToFirst by mutableStateOf(false)

    @GuardedBy("mediaMutex")
    private var sortedMedia = TreeMap<MediaSortKeyModel, MediaDataModel>(comparator)

    // To store active controllers and their callbacks
@@ -104,21 +108,28 @@ constructor(
    private val mediaCallbacks = mutableMapOf<InstanceId, MediaController.Callback>()
    // To store active polling jobs
    private val positionPollers = mutableMapOf<InstanceId, Job>()
    private val mediaMutex = Mutex()

    override fun addCurrentUserMediaEntry(data: MediaData): UpdateArtInfoModel? {
        return super.addCurrentUserMediaEntry(data).also { updateModel ->
            addToSortedMedia(data, updateModel)
            applicationScope.launch {
                mediaMutex.withLock { addToSortedMediaLocked(data, updateModel) }
            }
        }
    }

    override fun removeCurrentUserMediaEntry(key: InstanceId): MediaData? {
        return super.removeCurrentUserMediaEntry(key)?.also { removeFromSortedMedia(it) }
        return super.removeCurrentUserMediaEntry(key)?.also {
            applicationScope.launch { mediaMutex.withLock { removeFromSortedMediaLocked(it) } }
        }
    }

    override fun removeCurrentUserMediaEntry(key: InstanceId, data: MediaData): Boolean {
        return super.removeCurrentUserMediaEntry(key, data).also {
            if (it) {
                removeFromSortedMedia(data)
                applicationScope.launch {
                    mediaMutex.withLock { removeFromSortedMediaLocked(data) }
                }
            }
        }
    }
@@ -126,23 +137,33 @@ constructor(
    override fun clearCurrentUserMedia() {
        val userEntries = LinkedHashMap<InstanceId, MediaData>(mutableUserEntries.value)
        mutableUserEntries.value = LinkedHashMap()
        userEntries.forEach { removeFromSortedMedia(it.value) }
        applicationScope.launch {
            mediaMutex.withLock { userEntries.forEach { removeFromSortedMediaLocked(it.value) } }
        }
    }

    override fun seek(sessionKey: InstanceId, to: Long) {
        activeControllers[sessionKey]?.let { controller ->
            controller.transportControls.seekTo(to)
            applicationScope.launch {
                mediaMutex.withLock {
                    currentMedia
                        .find { it.instanceId == sessionKey }
                        ?.let { latestModel ->
                    updateMediaModelInState(latestModel) { it.copy(positionMs = to) }
                            updateMediaModelInStateLocked(latestModel) { it.copy(positionMs = to) }
                        }
                }
            }
        }
    }

    override fun reorderMedia() {
        applicationScope.launch {
            mediaMutex.withLock {
                currentMedia.clear()
                currentMedia.addAll(sortedMedia.values.toList())
            }
        }
        currentCarouselIndex = 0
    }

@@ -154,7 +175,8 @@ constructor(
        shouldScrollToFirst = false
    }

    private fun addToSortedMedia(data: MediaData, updateModel: UpdateArtInfoModel?) {
    @GuardedBy("mediaMutex")
    private suspend fun addToSortedMediaLocked(data: MediaData, updateModel: UpdateArtInfoModel?) {
        val sortedMap = TreeMap<MediaSortKeyModel, MediaDataModel>(comparator)
        val currentModel = sortedMedia.values.find { it.instanceId == data.instanceId }

@@ -173,8 +195,6 @@ constructor(
                        systemClock.currentTimeMillis(),
                        instanceId,
                    )

                applicationScope.launch {
                val controller =
                    if (currentModel != null && currentModel.token == token) {
                        activeControllers[currentModel.instanceId]
@@ -183,8 +203,7 @@ constructor(
                        currentModel?.instanceId?.let { clearControllerState(it) }
                        token?.let { MediaController(applicationContext, it) }
                    }
                    val (icon, background) =
                        getIconAndBackground(mediaData, currentModel, updateModel)
                val (icon, background) = getIconAndBackground(mediaData, currentModel, updateModel)
                val mediaModel = toDataModel(controller, icon, background)
                sortedMap[sortKey] = mediaModel
                controller?.let { setupController(mediaModel, it) }
@@ -215,9 +234,9 @@ constructor(
            }
        }
    }
    }

    private fun removeFromSortedMedia(data: MediaData) {
    @GuardedBy("mediaMutex")
    private fun removeFromSortedMediaLocked(data: MediaData) {
        currentMedia.removeIf { model -> data.instanceId == model.instanceId }
        sortedMedia =
            TreeMap<MediaSortKeyModel, MediaDataModel>(comparator).apply {
@@ -390,15 +409,23 @@ constructor(
                }

                override fun onMetadataChanged(metadata: MediaMetadata?) {
                    val duration = metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L
                    applicationScope.launch {
                        mediaMutex.withLock {
                            val duration =
                                metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L
                            currentMedia
                                .find { it.instanceId == dataModel.instanceId }
                                ?.let { latestModel ->
                            updateMediaModelInState(latestModel) { model ->
                                    updateMediaModelInStateLocked(latestModel) { model ->
                                        val canBeScrubbed =
                                    controller.playbackState?.state != PlaybackState.STATE_NONE &&
                                        duration > 0L
                                model.copy(canBeScrubbed = canBeScrubbed, durationMs = duration)
                                            controller.playbackState?.state !=
                                                PlaybackState.STATE_NONE && duration > 0L
                                        model.copy(
                                            canBeScrubbed = canBeScrubbed,
                                            durationMs = duration,
                                        )
                                    }
                                }
                        }
                    }
                }
@@ -465,11 +492,14 @@ constructor(
    }

    private fun checkPlaybackPosition(instanceId: InstanceId, playbackState: PlaybackState?) {
        applicationScope.launch {
            mediaMutex.withLock {
                currentMedia
                    .find { it.instanceId == instanceId }
                    ?.let { latestModel ->
                val newPosition = playbackState?.computeActualPosition(latestModel.durationMs)
                updateMediaModelInState(latestModel) {
                        val newPosition =
                            playbackState?.computeActualPosition(latestModel.durationMs)
                        updateMediaModelInStateLocked(latestModel) {
                            if (newPosition != null && newPosition <= latestModel.durationMs) {
                                it.copy(positionMs = newPosition)
                            } else {
@@ -478,6 +508,8 @@ constructor(
                        }
                    }
            }
        }
    }

    private fun clearControllerState(instanceId: InstanceId) {
        positionPollers[instanceId]?.cancel()
@@ -487,7 +519,8 @@ constructor(
        mediaCallbacks.remove(instanceId)
    }

    private fun updateMediaModelInState(
    @GuardedBy("mediaMutex")
    private fun updateMediaModelInStateLocked(
        oldModel: MediaDataModel,
        updateBlock: (MediaDataModel) -> MediaDataModel,
    ) {