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

Commit ad87df88 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge changes from topic "volume_panel_cast_improvements" into main

* changes:
  Media output domain layer tests
  Rework cast slider.
parents aae2cb64 2765782f
Loading
Loading
Loading
Loading
+23 −1
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.settingslib.media.session
package com.android.settingslib.media.session


import android.media.session.MediaController
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.MediaSessionManager
import android.media.session.MediaSessionManager
import android.os.UserHandle
import android.os.UserHandle
import androidx.concurrent.futures.DirectExecutor
import androidx.concurrent.futures.DirectExecutor
@@ -28,7 +29,7 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch


/** [Flow] for [MediaSessionManager.OnActiveSessionsChangedListener]. */
/** [Flow] for [MediaSessionManager.OnActiveSessionsChangedListener]. */
val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?>
val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?>
    get() =
    get() =
        callbackFlow {
        callbackFlow {
                val listener =
                val listener =
@@ -42,3 +43,24 @@ val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?>
                awaitClose { removeOnActiveSessionsChangedListener(listener) }
                awaitClose { removeOnActiveSessionsChangedListener(listener) }
            }
            }
            .buffer(capacity = Channel.CONFLATED)
            .buffer(capacity = Channel.CONFLATED)

/** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */
val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?>
    get() =
        callbackFlow {
                val callback =
                    object : MediaSessionManager.RemoteSessionCallback {
                        override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) {
                            launch { send(sessionToken) }
                        }

                        override fun onDefaultRemoteSessionChanged(
                            sessionToken: MediaSession.Token?
                        ) {
                            launch { send(sessionToken) }
                        }
                    }
                registerRemoteSessionCallback(DirectExecutor.INSTANCE, callback)
                awaitClose { unregisterRemoteSessionCallback(callback) }
            }
            .buffer(capacity = Channel.CONFLATED)
+0 −52
Original line number Original line Diff line number Diff line
@@ -15,14 +15,10 @@
 */
 */
package com.android.settingslib.volume.data.repository
package com.android.settingslib.volume.data.repository


import android.media.MediaRouter2Manager
import android.media.RoutingSessionInfo
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.settingslib.volume.shared.model.AudioManagerEvent
import com.android.settingslib.volume.shared.model.AudioManagerEvent
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
@@ -30,35 +26,23 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext


/** Repository providing data about connected media devices. */
/** Repository providing data about connected media devices. */
interface LocalMediaRepository {
interface LocalMediaRepository {


    /** Available devices list */
    val mediaDevices: StateFlow<Collection<MediaDevice>>

    /** Currently connected media device */
    /** Currently connected media device */
    val currentConnectedDevice: StateFlow<MediaDevice?>
    val currentConnectedDevice: StateFlow<MediaDevice?>

    val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>

    suspend fun adjustSessionVolume(sessionId: String?, volume: Int)
}
}


class LocalMediaRepositoryImpl(
class LocalMediaRepositoryImpl(
    audioManagerEventsReceiver: AudioManagerEventsReceiver,
    audioManagerEventsReceiver: AudioManagerEventsReceiver,
    private val localMediaManager: LocalMediaManager,
    private val localMediaManager: LocalMediaManager,
    private val mediaRouter2Manager: MediaRouter2Manager,
    coroutineScope: CoroutineScope,
    coroutineScope: CoroutineScope,
    private val backgroundContext: CoroutineContext,
) : LocalMediaRepository {
) : LocalMediaRepository {


    private val devicesChanges =
    private val devicesChanges =
@@ -94,18 +78,6 @@ class LocalMediaRepositoryImpl(
            }
            }
            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)


    override val mediaDevices: StateFlow<Collection<MediaDevice>> =
        mediaDevicesUpdates
            .mapNotNull {
                if (it is DevicesUpdate.DeviceListUpdate) {
                    it.newDevices ?: emptyList()
                } else {
                    null
                }
            }
            .flowOn(backgroundContext)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())

    override val currentConnectedDevice: StateFlow<MediaDevice?> =
    override val currentConnectedDevice: StateFlow<MediaDevice?> =
        merge(devicesChanges, mediaDevicesUpdates)
        merge(devicesChanges, mediaDevicesUpdates)
            .map { localMediaManager.currentConnectedDevice }
            .map { localMediaManager.currentConnectedDevice }
@@ -116,30 +88,6 @@ class LocalMediaRepositoryImpl(
                localMediaManager.currentConnectedDevice
                localMediaManager.currentConnectedDevice
            )
            )


    override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> =
        merge(devicesChanges, mediaDevicesUpdates)
            .onStart { emit(Unit) }
            .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) }
            .flowOn(backgroundContext)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())

    override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
        withContext(backgroundContext) {
            if (sessionId == null) {
                localMediaManager.adjustSessionVolume(volume)
            } else {
                localMediaManager.adjustSessionVolume(sessionId, volume)
            }
        }
    }

    private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession =
        RoutingSession(
            info,
            isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(),
            isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info)
        )

    private sealed interface DevicesUpdate {
    private sealed interface DevicesUpdate {


        data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate
        data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate
+24 −50
Original line number Original line Diff line number Diff line
@@ -27,18 +27,26 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.stateIn


/** Provides controllers for currently active device media sessions. */
/** Provides controllers for currently active device media sessions. */
interface MediaControllerRepository {
interface MediaControllerRepository {


    /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */
    /**
    val activeLocalMediaController: StateFlow<MediaController?>
     * Get a list of controllers for all ongoing sessions. The controllers will be provided in
     * priority order with the most important controller at index 0.
     *
     * This requires the [android.Manifest.permission.MEDIA_CONTENT_CONTROL] permission be held by
     * the calling app.
     */
    val activeSessions: StateFlow<List<MediaController>>
}
}


class MediaControllerRepositoryImpl(
class MediaControllerRepositoryImpl(
@@ -49,51 +57,17 @@ class MediaControllerRepositoryImpl(
    backgroundContext: CoroutineContext,
    backgroundContext: CoroutineContext,
) : MediaControllerRepository {
) : MediaControllerRepository {


    private val devicesChanges =
    override val activeSessions: StateFlow<List<MediaController>> =
        audioManagerEventsReceiver.events.filterIsInstance(
        merge(
            AudioManagerEvent.StreamDevicesChanged::class
                mediaSessionManager.activeMediaChanges.filterNotNull(),
                localBluetoothManager?.headsetAudioModeChanges?.map {
                    mediaSessionManager.getActiveSessions(null)
                } ?: emptyFlow(),
                audioManagerEventsReceiver.events
                    .filterIsInstance(AudioManagerEvent.StreamDevicesChanged::class)
                    .map { mediaSessionManager.getActiveSessions(null) },
            )
            )

            .onStart { emit(mediaSessionManager.getActiveSessions(null)) }
    override val activeLocalMediaController: StateFlow<MediaController?> =
        combine(
                mediaSessionManager.activeMediaChanges.onStart {
                    emit(mediaSessionManager.getActiveSessions(null))
                },
                localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) }
                    ?: flowOf(null),
                devicesChanges.onStart { emit(AudioManagerEvent.StreamDevicesChanged) },
            ) { controllers, _, _ ->
                controllers?.let(::findLocalMediaController)
            }
            .flowOn(backgroundContext)
            .flowOn(backgroundContext)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())

    private fun findLocalMediaController(
        controllers: Collection<MediaController>,
    ): MediaController? {
        var localController: MediaController? = null
        val remoteMediaSessionLists: MutableList<String> = ArrayList()
        for (controller in controllers) {
            val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
            when (playbackInfo.playbackType) {
                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
                    if (localController?.packageName.equals(controller.packageName)) {
                        localController = null
                    }
                    if (!remoteMediaSessionLists.contains(controller.packageName)) {
                        remoteMediaSessionLists.add(controller.packageName)
                    }
                }
                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
                    if (
                        localController == null &&
                            !remoteMediaSessionLists.contains(controller.packageName)
                    ) {
                        localController = controller
                    }
                }
            }
        }
        return localController
    }
}
}
+0 −57
Original line number Original line 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.settingslib.volume.domain.interactor

import com.android.settingslib.media.MediaDevice
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import com.android.settingslib.volume.domain.model.RoutingSession
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

class LocalMediaInteractor(
    private val repository: LocalMediaRepository,
    coroutineScope: CoroutineScope,
) {

    /** Available devices list */
    val mediaDevices: StateFlow<Collection<MediaDevice>>
        get() = repository.mediaDevices

    /** Currently connected media device */
    val currentConnectedDevice: StateFlow<MediaDevice?>
        get() = repository.currentConnectedDevice

    val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
        repository.remoteRoutingSessions
            .map { sessions ->
                sessions.map {
                    RoutingSession(
                        routingSessionInfo = it.routingSessionInfo,
                        isMediaOutputDisabled = it.isMediaOutputDisabled,
                        isVolumeSeekBarEnabled =
                            it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0
                    )
                }
            }
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())

    suspend fun adjustSessionVolume(sessionId: String?, volume: Int) =
        repository.adjustSessionVolume(sessionId, volume)
}
+0 −103
Original line number Original line Diff line number Diff line
@@ -15,17 +15,12 @@
 */
 */
package com.android.settingslib.volume.data.repository
package com.android.settingslib.volume.data.repository


import android.media.MediaRoute2Info
import android.media.MediaRouter2Manager
import android.media.RoutingSessionInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.TestScope
@@ -37,15 +32,10 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyString
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import org.mockito.MockitoAnnotations


@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
@SmallTest
class LocalMediaRepositoryImplTest {
class LocalMediaRepositoryImplTest {
@@ -53,7 +43,6 @@ class LocalMediaRepositoryImplTest {
    @Mock private lateinit var localMediaManager: LocalMediaManager
    @Mock private lateinit var localMediaManager: LocalMediaManager
    @Mock private lateinit var mediaDevice1: MediaDevice
    @Mock private lateinit var mediaDevice1: MediaDevice
    @Mock private lateinit var mediaDevice2: MediaDevice
    @Mock private lateinit var mediaDevice2: MediaDevice
    @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager


    @Captor
    @Captor
    private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback>
    private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback>
@@ -71,28 +60,10 @@ class LocalMediaRepositoryImplTest {
            LocalMediaRepositoryImpl(
            LocalMediaRepositoryImpl(
                eventsReceiver,
                eventsReceiver,
                localMediaManager,
                localMediaManager,
                mediaRouter2Manager,
                testScope.backgroundScope,
                testScope.backgroundScope,
                testScope.testScheduler,
            )
            )
    }
    }


    @Test
    fun mediaDevices_areUpdated() {
        testScope.runTest {
            var mediaDevices: Collection<MediaDevice>? = null
            underTest.mediaDevices.onEach { mediaDevices = it }.launchIn(backgroundScope)
            runCurrent()
            verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture())
            deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2))
            runCurrent()

            assertThat(mediaDevices).hasSize(2)
            assertThat(mediaDevices).contains(mediaDevice1)
            assertThat(mediaDevices).contains(mediaDevice2)
        }
    }

    @Test
    @Test
    fun deviceListUpdated_currentConnectedDeviceUpdated() {
    fun deviceListUpdated_currentConnectedDeviceUpdated() {
        testScope.runTest {
        testScope.runTest {
@@ -110,78 +81,4 @@ class LocalMediaRepositoryImplTest {
            assertThat(currentConnectedDevice).isEqualTo(mediaDevice1)
            assertThat(currentConnectedDevice).isEqualTo(mediaDevice1)
        }
        }
    }
    }

    @Test
    fun kek() {
        testScope.runTest {
            `when`(localMediaManager.remoteRoutingSessions)
                .thenReturn(
                    listOf(
                        testRoutingSessionInfo1,
                        testRoutingSessionInfo2,
                        testRoutingSessionInfo3,
                    )
                )
            `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then {
                (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1
            }
            `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then {
                if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) {
                    return@then listOf(mock(MediaRoute2Info::class.java))
                }
                emptyList<MediaRoute2Info>()
            }
            var remoteRoutingSessions: Collection<RoutingSession>? = null
            underTest.remoteRoutingSessions
                .onEach { remoteRoutingSessions = it }
                .launchIn(backgroundScope)

            runCurrent()

            assertThat(remoteRoutingSessions)
                .containsExactlyElementsIn(
                    listOf(
                        RoutingSession(
                            routingSessionInfo = testRoutingSessionInfo1,
                            isVolumeSeekBarEnabled = true,
                            isMediaOutputDisabled = true,
                        ),
                        RoutingSession(
                            routingSessionInfo = testRoutingSessionInfo2,
                            isVolumeSeekBarEnabled = false,
                            isMediaOutputDisabled = false,
                        ),
                        RoutingSession(
                            routingSessionInfo = testRoutingSessionInfo3,
                            isVolumeSeekBarEnabled = false,
                            isMediaOutputDisabled = true,
                        )
                    )
                )
        }
    }

    @Test
    fun adjustSessionVolume_adjusts() {
        testScope.runTest {
            var volume = 0
            `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then {
                volume = it.arguments[1] as Int
                Unit
            }

            underTest.adjustSessionVolume("test_session", 10)

            assertThat(volume).isEqualTo(10)
        }
    }

    private companion object {
        val testRoutingSessionInfo1 =
            RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build()
        val testRoutingSessionInfo2 =
            RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build()
        val testRoutingSessionInfo3 =
            RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build()
    }
}
}
Loading