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

Commit 0b91e28c authored by Anton Potapov's avatar Anton Potapov
Browse files

Add MediaControllerInteractor to fake Looper usage in tests

Flag: aconfig new_volume_panel NEXTFOOD
Test: atest MediaDeviceSessionInteractorTest
Test: atest MediaOutputInteractorTest
Bug: 338223243
Change-Id: I7e0804e1081f43c7970ad28a8aa52e58667dd669
parent cdecb49e
Loading
Loading
Loading
Loading
+2 −8
Original line number Diff line number Diff line
@@ -16,17 +16,16 @@

package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor

import android.os.Handler
import android.testing.TestableLooper
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.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.volume.localMediaController
import com.android.systemui.volume.mediaControllerRepository
import com.android.systemui.volume.mediaDeviceSessionInteractor
import com.android.systemui.volume.mediaOutputInteractor
import com.android.systemui.volume.panel.shared.model.filterData
import com.android.systemui.volume.remoteMediaController
@@ -55,12 +54,7 @@ class MediaDeviceSessionInteractorTest : SysuiTestCase() {
                listOf(localMediaController, remoteMediaController)
            )

            underTest =
                MediaDeviceSessionInteractor(
                    testScope.testScheduler,
                    Handler(TestableLooper.get(kosmos.testCase).looper),
                    mediaControllerRepository,
                )
            underTest = mediaDeviceSessionInteractor
        }
    }

+7 −0
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactoryImpl
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaControllerInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaControllerInteractorImpl
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -41,6 +43,11 @@ interface MediaDevicesModule {
        impl: LocalMediaRepositoryFactoryImpl
    ): LocalMediaRepositoryFactory

    @Binds
    fun bindMediaControllerInteractor(
        impl: MediaControllerInteractorImpl
    ): MediaControllerInteractor

    companion object {

        @Provides
+34 −38
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@
 * limitations under the License.
 */

package com.android.settingslib.volume.data.repository
package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor

import android.media.MediaMetadata
import android.media.session.MediaController
@@ -22,79 +22,75 @@ import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.os.Bundle
import android.os.Handler
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaControllerChangeModel
import javax.inject.Inject
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch

interface MediaControllerInteractor {

    /** [MediaController.Callback] flow representation. */
fun MediaController.stateChanges(handler: Handler): Flow<MediaControllerChange> {
    return callbackFlow {
        val callback = MediaControllerCallbackProducer(this)
        registerCallback(callback, handler)
        awaitClose { unregisterCallback(callback) }
    fun stateChanges(mediaController: MediaController): Flow<MediaControllerChangeModel>
}
}

/** Models particular change event received by [MediaController.Callback]. */
sealed interface MediaControllerChange {

    data object SessionDestroyed : MediaControllerChange

    data class SessionEvent(val event: String, val extras: Bundle?) : MediaControllerChange

    data class PlaybackStateChanged(val state: PlaybackState?) : MediaControllerChange

    data class MetadataChanged(val metadata: MediaMetadata?) : MediaControllerChange
@SysUISingleton
class MediaControllerInteractorImpl
@Inject
constructor(
    @Background private val backgroundHandler: Handler,
) : MediaControllerInteractor {

    data class QueueChanged(val queue: MutableList<MediaSession.QueueItem>?) :
        MediaControllerChange

    data class QueueTitleChanged(val title: CharSequence?) : MediaControllerChange

    data class ExtrasChanged(val extras: Bundle?) : MediaControllerChange

    data class AudioInfoChanged(val info: MediaController.PlaybackInfo?) : MediaControllerChange
    override fun stateChanges(mediaController: MediaController): Flow<MediaControllerChangeModel> {
        return conflatedCallbackFlow {
            val callback = MediaControllerCallbackProducer(this)
            mediaController.registerCallback(callback, backgroundHandler)
            awaitClose { mediaController.unregisterCallback(callback) }
        }
    }
}

private class MediaControllerCallbackProducer(
    private val producingScope: ProducerScope<MediaControllerChange>
    private val producingScope: ProducerScope<MediaControllerChangeModel>
) : MediaController.Callback() {

    override fun onSessionDestroyed() {
        send(MediaControllerChange.SessionDestroyed)
        send(MediaControllerChangeModel.SessionDestroyed)
    }

    override fun onSessionEvent(event: String, extras: Bundle?) {
        send(MediaControllerChange.SessionEvent(event, extras))
        send(MediaControllerChangeModel.SessionEvent(event, extras))
    }

    override fun onPlaybackStateChanged(state: PlaybackState?) {
        send(MediaControllerChange.PlaybackStateChanged(state))
        send(MediaControllerChangeModel.PlaybackStateChanged(state))
    }

    override fun onMetadataChanged(metadata: MediaMetadata?) {
        send(MediaControllerChange.MetadataChanged(metadata))
        send(MediaControllerChangeModel.MetadataChanged(metadata))
    }

    override fun onQueueChanged(queue: MutableList<MediaSession.QueueItem>?) {
        send(MediaControllerChange.QueueChanged(queue))
        send(MediaControllerChangeModel.QueueChanged(queue))
    }

    override fun onQueueTitleChanged(title: CharSequence?) {
        send(MediaControllerChange.QueueTitleChanged(title))
        send(MediaControllerChangeModel.QueueTitleChanged(title))
    }

    override fun onExtrasChanged(extras: Bundle?) {
        send(MediaControllerChange.ExtrasChanged(extras))
        send(MediaControllerChangeModel.ExtrasChanged(extras))
    }

    override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) {
        send(MediaControllerChange.AudioInfoChanged(info))
        send(MediaControllerChangeModel.AudioInfoChanged(info))
    }

    private fun send(change: MediaControllerChange) {
    private fun send(change: MediaControllerChangeModel) {
        producingScope.launch { producingScope.send(change) }
    }
}
+10 −11
Original line number Diff line number Diff line
@@ -18,11 +18,9 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interacto

import android.media.session.MediaController
import android.media.session.PlaybackState
import android.os.Handler
import com.android.settingslib.volume.data.repository.MediaControllerChange
import com.android.settingslib.volume.data.repository.MediaControllerRepository
import com.android.settingslib.volume.data.repository.stateChanges
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaControllerChangeModel
import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import javax.inject.Inject
@@ -45,38 +43,39 @@ class MediaDeviceSessionInteractor
@Inject
constructor(
    @Background private val backgroundCoroutineContext: CoroutineContext,
    @Background private val backgroundHandler: Handler,
    private val mediaControllerInteractor: MediaControllerInteractor,
    private val mediaControllerRepository: MediaControllerRepository,
) {

    /** [PlaybackState] changes for the [MediaDeviceSession]. */
    fun playbackState(session: MediaDeviceSession): Flow<PlaybackState?> {
        return stateChanges(session) {
                emit(MediaControllerChange.PlaybackStateChanged(it.playbackState))
                emit(MediaControllerChangeModel.PlaybackStateChanged(it.playbackState))
            }
            .filterIsInstance(MediaControllerChange.PlaybackStateChanged::class)
            .filterIsInstance(MediaControllerChangeModel.PlaybackStateChanged::class)
            .map { it.state }
    }

    /** [MediaController.PlaybackInfo] changes for the [MediaDeviceSession]. */
    fun playbackInfo(session: MediaDeviceSession): Flow<MediaController.PlaybackInfo?> {
        return stateChanges(session) {
                emit(MediaControllerChange.AudioInfoChanged(it.playbackInfo))
                emit(MediaControllerChangeModel.AudioInfoChanged(it.playbackInfo))
            }
            .filterIsInstance(MediaControllerChange.AudioInfoChanged::class)
            .filterIsInstance(MediaControllerChangeModel.AudioInfoChanged::class)
            .map { it.info }
    }

    private fun stateChanges(
        session: MediaDeviceSession,
        onStart: suspend FlowCollector<MediaControllerChange>.(controller: MediaController) -> Unit,
    ): Flow<MediaControllerChange?> =
        onStart:
            suspend FlowCollector<MediaControllerChangeModel>.(controller: MediaController) -> Unit,
    ): Flow<MediaControllerChangeModel?> =
        mediaControllerRepository.activeSessions
            .flatMapLatest { controllers ->
                val controller: MediaController =
                    findControllerForSession(controllers, session)
                        ?: return@flatMapLatest flowOf(null)
                controller.stateChanges(backgroundHandler).onStart { onStart(controller) }
                mediaControllerInteractor.stateChanges(controller).onStart { onStart(controller) }
            }
            .flowOn(backgroundCoroutineContext)

+5 −4
Original line number Diff line number Diff line
@@ -19,12 +19,10 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interacto
import android.content.pm.PackageManager
import android.media.VolumeProvider
import android.media.session.MediaController
import android.os.Handler
import android.util.Log
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import com.android.settingslib.volume.data.repository.MediaControllerRepository
import com.android.settingslib.volume.data.repository.stateChanges
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions
@@ -61,7 +59,7 @@ constructor(
    @VolumePanelScope private val coroutineScope: CoroutineScope,
    @Background private val backgroundCoroutineContext: CoroutineContext,
    mediaControllerRepository: MediaControllerRepository,
    @Background private val backgroundHandler: Handler,
    private val mediaControllerInteractor: MediaControllerInteractor,
) {

    private val activeMediaControllers: Flow<MediaControllers> =
@@ -194,7 +192,10 @@ constructor(
            return flowOf(null)
        }

        return stateChanges(backgroundHandler).map { this }.onStart { emit(this@stateChanges) }
        return mediaControllerInteractor
            .stateChanges(this)
            .map { this }
            .onStart { emit(this@stateChanges) }
    }

    private data class MediaControllers(
Loading