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

Commit cac75836 authored by Anton Potapov's avatar Anton Potapov
Browse files

Rework cast slider.

This CL improves Volume panel behaviour when casting. It makes currently active volume slider to be on top and simplifies the underlying API.

Flag: aconfig new_volume_panel TEAMFOOD
Test: manual on the phone
Fixes: 329641812
Fixes: 329561499
Change-Id: I7569e353d72ad8759e0b1367168b42e4e2a71d6f
parent 139426f7
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.settingslib.media.session

import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.MediaSessionManager
import android.os.UserHandle
import androidx.concurrent.futures.DirectExecutor
@@ -42,3 +43,24 @@ val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?>
                awaitClose { removeOnActiveSessionsChangedListener(listener) }
            }
            .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 Diff line number Diff line
@@ -15,14 +15,10 @@
 */
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.MediaDevice
import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.settingslib.volume.shared.model.AudioManagerEvent
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -30,35 +26,23 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext

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

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

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

    val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>

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

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

    private val devicesChanges =
@@ -94,18 +78,6 @@ class LocalMediaRepositoryImpl(
            }
            .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?> =
        merge(devicesChanges, mediaDevicesUpdates)
            .map { localMediaManager.currentConnectedDevice }
@@ -116,30 +88,6 @@ class LocalMediaRepositoryImpl(
                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 {

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

@@ -38,7 +40,7 @@ import kotlinx.coroutines.flow.stateIn
interface MediaControllerRepository {

    /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */
    val activeLocalMediaController: StateFlow<MediaController?>
    val activeMediaControllers: StateFlow<Collection<MediaController>>
}

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

    private val devicesChanges =
        audioManagerEventsReceiver.events.filterIsInstance(
            AudioManagerEvent.StreamDevicesChanged::class
    override val activeMediaControllers: StateFlow<Collection<MediaController>> =
        merge(
                mediaSessionManager.activeMediaChanges
                    .onStart { emit(mediaSessionManager.getActiveSessions(null)) }
                    .filterNotNull(),
                localBluetoothManager
                    ?.headsetAudioModeChanges
                    ?.onStart { emit(Unit) }
                    ?.map { mediaSessionManager.getActiveSessions(null) } ?: emptyFlow(),
                audioManagerEventsReceiver.events
                    .filterIsInstance(AudioManagerEvent.StreamDevicesChanged::class)
                    .onStart { emit(AudioManagerEvent.StreamDevicesChanged) }
                    .map { 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)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)

    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
    }
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
}
+0 −57
Original line number 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)
}
+2 −2
Original line number Diff line number Diff line
@@ -116,7 +116,7 @@ class MediaControllerRepositoryImplTest {
                    )
                )
            var mediaController: MediaController? = null
            underTest.activeLocalMediaController
            underTest.activeMediaController
                .onEach { mediaController = it }
                .launchIn(backgroundScope)
            runCurrent()
@@ -141,7 +141,7 @@ class MediaControllerRepositoryImplTest {
                    )
                )
            var mediaController: MediaController? = null
            underTest.activeLocalMediaController
            underTest.activeMediaController
                .onEach { mediaController = it }
                .launchIn(backgroundScope)
            runCurrent()
Loading