diff options
| author | 2024-03-15 13:11:42 +0000 | |
|---|---|---|
| committer | 2024-03-19 11:33:44 +0000 | |
| commit | cac758367539e069b6118cac9963cf51a8b69a55 (patch) | |
| tree | 69ebb75d3ee71d6ea3129757ebee7cbd8bf0630c | |
| parent | 139426f7e53c5efecbe992c06656a9f1ed790d34 (diff) | |
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
19 files changed, 402 insertions, 375 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt index cda6b8bb36be..12da85915b96 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt +++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt @@ -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) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt index 298dd71e555e..724dd51b8fe4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt @@ -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 diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt index 7c231d1fad4e..656f4fd0d7fa 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt @@ -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 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) - } + 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) }, + ) .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()) } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt deleted file mode 100644 index f6213351ae0d..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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) -} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt index f3d17141334e..5ac3b439c32c 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt @@ -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() diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt index d134e60ef72f..155102c9b9a7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt @@ -21,7 +21,6 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.MediaControllerRepository import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl -import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -52,13 +51,6 @@ interface MediaDevicesModule { @Provides @SysUISingleton - fun provideLocalMediaInteractor( - repository: LocalMediaRepository, - @Application scope: CoroutineScope, - ): LocalMediaInteractor = LocalMediaInteractor(repository, scope) - - @Provides - @SysUISingleton fun provideMediaDeviceSessionRepository( intentsReceiver: AudioManagerEventsReceiver, mediaSessionManager: MediaSessionManager, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt index 11b4690e59ee..e052f243f7ea 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt @@ -15,15 +15,12 @@ */ package com.android.systemui.volume.panel.component.mediaoutput.data.repository -import android.media.MediaRouter2Manager import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.controls.util.LocalMediaManagerFactory import javax.inject.Inject -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope interface LocalMediaRepositoryFactory { @@ -35,18 +32,14 @@ class LocalMediaRepositoryFactoryImpl @Inject constructor( private val eventsReceiver: AudioManagerEventsReceiver, - private val mediaRouter2Manager: MediaRouter2Manager, private val localMediaManagerFactory: LocalMediaManagerFactory, @Application private val coroutineScope: CoroutineScope, - @Background private val backgroundCoroutineContext: CoroutineContext, ) : LocalMediaRepositoryFactory { override fun create(packageName: String?): LocalMediaRepository = LocalMediaRepositoryImpl( eventsReceiver, localMediaManagerFactory.create(packageName), - mediaRouter2Manager, coroutineScope, - backgroundCoroutineContext, ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt new file mode 100644 index 000000000000..6e39dd6602d8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt @@ -0,0 +1,108 @@ +/* + * 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.systemui.volume.panel.component.mediaoutput.domain.interactor + +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.MediaDeviceSession +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Allows to observe and change [MediaDeviceSession] state. */ +@OptIn(ExperimentalCoroutinesApi::class) +@VolumePanelScope +class MediaDeviceSessionInteractor +@Inject +constructor( + @Background private val backgroundCoroutineContext: CoroutineContext, + @Background private val backgroundHandler: Handler, + private val mediaControllerRepository: MediaControllerRepository, +) { + + /** [PlaybackState] changes for the [MediaDeviceSession]. */ + fun playbackState(session: MediaDeviceSession): Flow<PlaybackState?> { + return stateChanges(session) { + emit(MediaControllerChange.PlaybackStateChanged(it.playbackState)) + } + .filterIsInstance(MediaControllerChange.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)) + } + .filterIsInstance(MediaControllerChange.AudioInfoChanged::class) + .map { it.info } + } + + private fun stateChanges( + session: MediaDeviceSession, + onStart: suspend FlowCollector<MediaControllerChange>.(controller: MediaController) -> Unit, + ): Flow<MediaControllerChange?> = + mediaControllerRepository.activeMediaControllers + .flatMapLatest { controllers -> + val controller: MediaController = + findControllerForSession(controllers, session) + ?: return@flatMapLatest flowOf(null) + controller.stateChanges(backgroundHandler).onStart { onStart(controller) } + } + .flowOn(backgroundCoroutineContext) + + /** Set [MediaDeviceSession] volume to [volume]. */ + suspend fun setSessionVolume(mediaDeviceSession: MediaDeviceSession, volume: Int): Boolean { + if (!mediaDeviceSession.canAdjustVolume) { + return false + } + return withContext(backgroundCoroutineContext) { + val controller = + findControllerForSession( + mediaControllerRepository.activeMediaControllers.value, + mediaDeviceSession, + ) + if (controller == null) { + false + } else { + controller.setVolumeTo(volume, 0) + true + } + } + } + + private fun findControllerForSession( + controllers: Collection<MediaController>, + mediaDeviceSession: MediaDeviceSession, + ): MediaController? = + controllers.firstOrNull { it.sessionToken == mediaDeviceSession.sessionToken } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt index cb16abe7e575..ea4c082f4660 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt @@ -33,23 +33,15 @@ constructor( private val mediaOutputDialogManager: MediaOutputDialogManager, ) { - fun onBarClick(session: MediaDeviceSession, expandable: Expandable) { - when (session) { - is MediaDeviceSession.Active -> { - mediaOutputDialogManager.createAndShowWithController( - session.packageName, - false, - expandable.dialogController() - ) - } - is MediaDeviceSession.Inactive -> { - mediaOutputDialogManager.createAndShowForSystemRouting( - expandable.dialogController() - ) - } - else -> { - /* do nothing */ - } + fun onBarClick(session: MediaDeviceSession, isPlaybackActive: Boolean, expandable: Expandable) { + if (isPlaybackActive) { + mediaOutputDialogManager.createAndShowWithController( + session.packageName, + false, + expandable.dialogController() + ) + } else { + mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController()) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt index 0f5343701ac6..11c981fd61a0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -17,17 +17,16 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor 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.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.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -38,12 +37,9 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -58,35 +54,40 @@ constructor( private val packageManager: PackageManager, @VolumePanelScope private val coroutineScope: CoroutineScope, @Background private val backgroundCoroutineContext: CoroutineContext, - @Background private val backgroundHandler: Handler, - mediaControllerRepository: MediaControllerRepository + mediaControllerRepository: MediaControllerRepository, ) { - /** Current [MediaDeviceSession]. Emits when the session playback changes. */ - val mediaDeviceSession: StateFlow<MediaDeviceSession> = - mediaControllerRepository.activeLocalMediaController - .flatMapLatest { it?.mediaDeviceSession() ?: flowOf(MediaDeviceSession.Inactive) } - .flowOn(backgroundCoroutineContext) - .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSession.Inactive) + private val activeMediaControllers: Flow<MediaControllers> = + mediaControllerRepository.activeMediaControllers + .map { getMediaControllers(it) } + .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + + /** [MediaDeviceSessions] that contains currently active sessions. */ + val activeMediaDeviceSessions: Flow<MediaDeviceSessions> = + activeMediaControllers.map { + MediaDeviceSessions( + local = it.local?.mediaDeviceSession(), + remote = it.remote?.mediaDeviceSession() + ) + } - private fun MediaController.mediaDeviceSession(): Flow<MediaDeviceSession> { - return stateChanges(backgroundHandler) - .onStart { emit(MediaControllerChange.PlaybackStateChanged(playbackState)) } - .filterIsInstance<MediaControllerChange.PlaybackStateChanged>() + /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */ + val defaultActiveMediaSession: StateFlow<MediaDeviceSession?> = + activeMediaControllers .map { - MediaDeviceSession.Active( - appLabel = getApplicationLabel(packageName) - ?: return@map MediaDeviceSession.Inactive, - packageName = packageName, - sessionToken = sessionToken, - playbackState = playbackState, - ) + when { + it.local?.playbackState?.isActive == true -> it.local.mediaDeviceSession() + it.remote?.playbackState?.isActive == true -> it.remote.mediaDeviceSession() + it.local != null -> it.local.mediaDeviceSession() + else -> null + } } - } + .flowOn(backgroundCoroutineContext) + .stateIn(coroutineScope, SharingStarted.Eagerly, null) private val localMediaRepository: SharedFlow<LocalMediaRepository> = - mediaDeviceSession - .map { (it as? MediaDeviceSession.Active)?.packageName } + defaultActiveMediaSession + .map { it?.packageName } .distinctUntilChanged() .map { localMediaRepositoryFactory.create(it) } .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) @@ -111,6 +112,54 @@ constructor( } } + /** Finds local and remote media controllers. */ + private fun getMediaControllers( + controllers: Collection<MediaController>, + ): MediaControllers { + var localController: MediaController? = null + var remoteController: MediaController? = null + val remoteMediaSessions: MutableSet<String> = mutableSetOf() + for (controller in controllers) { + val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue + when (playbackInfo.playbackType) { + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { + // MediaController can't be local if there is a remote one for the same package + if (localController?.packageName.equals(controller.packageName)) { + localController = null + } + if (!remoteMediaSessions.contains(controller.packageName)) { + remoteMediaSessions.add(controller.packageName) + if (remoteController == null) { + remoteController = controller + } + } + } + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { + if (controller.packageName in remoteMediaSessions) continue + if (localController != null) continue + localController = controller + } + } + } + return MediaControllers(local = localController, remote = remoteController) + } + + private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? { + return MediaDeviceSession( + packageName = packageName, + sessionToken = sessionToken, + canAdjustVolume = + playbackInfo != null && + playbackInfo?.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED, + appLabel = getApplicationLabel(packageName) ?: return null + ) + } + + private data class MediaControllers( + val local: MediaController?, + val remote: MediaController?, + ) + private companion object { const val TAG = "MediaOutputInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt index 1bceee9b2d34..2a2ce796a2b7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt @@ -17,26 +17,15 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.model import android.media.session.MediaSession -import android.media.session.PlaybackState /** Represents media playing on the connected device. */ -sealed interface MediaDeviceSession { +data class MediaDeviceSession( + val appLabel: CharSequence, + val packageName: String, + val sessionToken: MediaSession.Token, + val canAdjustVolume: Boolean, +) - /** Media is playing. */ - data class Active( - val appLabel: CharSequence, - val packageName: String, - val sessionToken: MediaSession.Token, - val playbackState: PlaybackState?, - ) : MediaDeviceSession - - /** Media is not playing. */ - data object Inactive : MediaDeviceSession - - /** Current media state is unknown yet. */ - data object Unknown : MediaDeviceSession -} - -/** Returns true when the audio is playing for the [MediaDeviceSession]. */ -fun MediaDeviceSession.isPlaying(): Boolean = - this is MediaDeviceSession.Active && playbackState?.isActive == true +/** Returns true when [other] controls the same sessions as [this]. */ +fun MediaDeviceSession.isTheSameSession(other: MediaDeviceSession?): Boolean = + sessionToken == other?.sessionToken diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt new file mode 100644 index 000000000000..ddc078421b9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt @@ -0,0 +1,31 @@ +/* + * 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.systemui.volume.panel.component.mediaoutput.domain.model + +/** Models a pair of local and remote [MediaDeviceSession]s. */ +data class MediaDeviceSessions( + val local: MediaDeviceSession?, + val remote: MediaDeviceSession?, +) { + + companion object { + /** Returns [MediaDeviceSessions.local]. */ + val Local: (MediaDeviceSessions) -> MediaDeviceSession? = { it.local } + /** Returns [MediaDeviceSessions.remote]. */ + val Remote: (MediaDeviceSessions) -> MediaDeviceSession? = { it.remote } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt index d49cb1ea6958..2530a3a46384 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt @@ -17,24 +17,30 @@ package com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel import android.content.Context +import android.media.session.PlaybackState import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Color import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession -import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** Models the UI of the Media Output Volume Panel component. */ +@OptIn(ExperimentalCoroutinesApi::class) @VolumePanelScope class MediaOutputViewModel @Inject @@ -43,25 +49,36 @@ constructor( @VolumePanelScope private val coroutineScope: CoroutineScope, private val volumePanelViewModel: VolumePanelViewModel, private val actionsInteractor: MediaOutputActionsInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, interactor: MediaOutputInteractor, ) { - private val mediaDeviceSession: StateFlow<MediaDeviceSession> = - interactor.mediaDeviceSession.stateIn( - coroutineScope, - SharingStarted.Eagerly, - MediaDeviceSession.Unknown, - ) + private val sessionWithPlayback: StateFlow<SessionWithPlayback?> = + interactor.defaultActiveMediaSession + .flatMapLatest { session -> + if (session == null) { + flowOf(null) + } else { + mediaDeviceSessionInteractor.playbackState(session).map { playback -> + playback?.let { SessionWithPlayback(session, it) } + } + } + } + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + null, + ) val connectedDeviceViewModel: StateFlow<ConnectedDeviceViewModel?> = - combine(mediaDeviceSession, interactor.currentConnectedDevice) { + combine(sessionWithPlayback, interactor.currentConnectedDevice) { mediaDeviceSession, currentConnectedDevice -> ConnectedDeviceViewModel( - if (mediaDeviceSession.isPlaying()) { + if (mediaDeviceSession?.playback?.isActive == true) { context.getString( R.string.media_output_label_title, - (mediaDeviceSession as MediaDeviceSession.Active).appLabel + mediaDeviceSession.session.appLabel ) } else { context.getString(R.string.media_output_title_without_playing) @@ -76,10 +93,10 @@ constructor( ) val deviceIconViewModel: StateFlow<DeviceIconViewModel?> = - combine(mediaDeviceSession, interactor.currentConnectedDevice) { + combine(sessionWithPlayback, interactor.currentConnectedDevice) { mediaDeviceSession, currentConnectedDevice -> - if (mediaDeviceSession.isPlaying()) { + if (mediaDeviceSession?.playback?.isActive == true) { val icon = currentConnectedDevice?.icon?.let { Icon.Loaded(it, null) } ?: Icon.Resource( @@ -112,7 +129,14 @@ constructor( ) fun onBarClick(expandable: Expandable) { - actionsInteractor.onBarClick(mediaDeviceSession.value, expandable) + sessionWithPlayback.value?.let { + actionsInteractor.onBarClick(it.session, it.playback.isActive, expandable) + } volumePanelViewModel.dismissPanel() } + + private data class SessionWithPlayback( + val session: MediaDeviceSession, + val playback: PlaybackState, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt deleted file mode 100644 index 6b62074e023d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.systemui.volume.panel.component.volume.domain.interactor - -import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor -import com.android.settingslib.volume.domain.model.RoutingSession -import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -/** Provides a remote media casting state. */ -@VolumePanelScope -class CastVolumeInteractor -@Inject -constructor( - @VolumePanelScope private val coroutineScope: CoroutineScope, - private val localMediaInteractor: LocalMediaInteractor, -) { - - /** Returns a list of [RoutingSession] to show in the UI. */ - val remoteRoutingSessions: StateFlow<List<RoutingSession>> = - localMediaInteractor.remoteRoutingSessions - .map { it.filter { routingSession -> routingSession.isVolumeSeekBarEnabled } } - .stateIn(coroutineScope, SharingStarted.Eagerly, emptyList()) - - /** Sets [routingSession] volume to [volume]. */ - suspend fun setVolume(routingSession: RoutingSession, volume: Int) { - localMediaInteractor.adjustSessionVolume(routingSession.routingSessionInfo.id, volume) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index 1b732081a12a..d49442c149ee 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -80,7 +80,7 @@ constructor( ) { model, isEnabled, ringerMode -> model.toState(isEnabled, ringerMode) } - .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState) + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) override fun onValueChanged(state: SliderState, newValue: Float) { val audioViewModel = state as? State @@ -163,17 +163,6 @@ constructor( val audioStreamModel: AudioStreamModel, ) : SliderState - private data object EmptyState : SliderState { - override val value: Float = 0f - override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f - override val icon: Icon? = null - override val valueText: String = "" - override val label: String = "" - override val disabledMessage: String? = null - override val a11yStep: Int = 0 - override val isEnabled: Boolean = true - } - @AssistedFactory interface Factory { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index 86b2d73de3e3..0f240b37f02e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -17,11 +17,11 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context -import com.android.settingslib.volume.domain.model.RoutingSession +import android.media.session.MediaController.PlaybackInfo import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R -import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor -import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -30,30 +30,29 @@ import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch class CastVolumeSliderViewModel @AssistedInject constructor( - @Assisted private val routingSession: RoutingSession, + @Assisted private val session: MediaDeviceSession, @Assisted private val coroutineScope: CoroutineScope, private val context: Context, - mediaOutputInteractor: MediaOutputInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, private val volumeSliderInteractor: VolumeSliderInteractor, - private val castVolumeInteractor: CastVolumeInteractor, ) : SliderViewModel { - private val volumeRange = 0..routingSession.routingSessionInfo.volumeMax - override val slider: StateFlow<SliderState> = - combine(mediaOutputInteractor.currentConnectedDevice) { _ -> getCurrentState() } - .stateIn(coroutineScope, SharingStarted.Eagerly, getCurrentState()) + mediaDeviceSessionInteractor + .playbackInfo(session) + .mapNotNull { it?.getCurrentState() } + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) override fun onValueChanged(state: SliderState, newValue: Float) { coroutineScope.launch { - castVolumeInteractor.setVolume(routingSession, newValue.roundToInt()) + mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt()) } } @@ -61,15 +60,16 @@ constructor( // do nothing because this action isn't supported for Cast sliders. } - private fun getCurrentState(): State = - State( - value = routingSession.routingSessionInfo.volume.toFloat(), + private fun PlaybackInfo.getCurrentState(): State { + val volumeRange = 0..maxVolume + return State( + value = currentVolume.toFloat(), valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(), icon = Icon.Resource(R.drawable.ic_cast, null), valueText = SliderViewModel.formatValue( volumeSliderInteractor.processVolumeToValue( - volume = routingSession.routingSessionInfo.volume, + volume = currentVolume, volumeRange = volumeRange, ) ), @@ -77,6 +77,7 @@ constructor( isEnabled = true, a11yStep = 1 ) + } private data class State( override val value: Float, @@ -95,7 +96,7 @@ constructor( interface Factory { fun create( - routingSession: RoutingSession, + session: MediaDeviceSession, coroutineScope: CoroutineScope, ): CastVolumeSliderViewModel } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt index b87d0a786740..3dca2724b095 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt @@ -36,4 +36,15 @@ sealed interface SliderState { */ val a11yStep: Int val disabledMessage: String? + + data object Empty : SliderState { + override val value: Float = 0f + override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f + override val icon: Icon? = null + override val valueText: String = "" + override val label: String = "" + override val disabledMessage: String? = null + override val a11yStep: Int = 0 + override val isEnabled: Boolean = true + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt index aaee24b9357f..4e9a45635f7b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt @@ -18,9 +18,10 @@ package com.android.systemui.volume.panel.component.volume.ui.viewmodel import android.media.AudioManager import com.android.settingslib.volume.shared.model.AudioStream +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor -import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying -import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isTheSameSession import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel @@ -29,17 +30,15 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch /** @@ -52,50 +51,34 @@ class AudioVolumeComponentViewModel @Inject constructor( @VolumePanelScope private val scope: CoroutineScope, - castVolumeInteractor: CastVolumeInteractor, mediaOutputInteractor: MediaOutputInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory, private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory, ) { - private val remoteSessionsViewModels: Flow<List<SliderViewModel>> = - castVolumeInteractor.remoteRoutingSessions.transformLatest { routingSessions -> - coroutineScope { - emit( - routingSessions.map { routingSession -> - castVolumeSliderViewModelFactory.create(routingSession, this) - } - ) - } - } - private val streamViewModels: Flow<List<SliderViewModel>> = - flowOf( - listOf( - AudioStream(AudioManager.STREAM_MUSIC), - AudioStream(AudioManager.STREAM_VOICE_CALL), - AudioStream(AudioManager.STREAM_RING), - AudioStream(AudioManager.STREAM_NOTIFICATION), - AudioStream(AudioManager.STREAM_ALARM), - ) - ) - .transformLatest { streams -> + val sliderViewModels: StateFlow<List<SliderViewModel>> = + combineTransform( + mediaOutputInteractor.activeMediaDeviceSessions, + mediaOutputInteractor.defaultActiveMediaSession, + ) { activeSessions, defaultSession -> coroutineScope { - emit( - streams.map { stream -> - streamSliderViewModelFactory.create( - AudioStreamSliderViewModel.FactoryAudioStreamWrapper(stream), - this, - ) + val viewModels = buildList { + if (defaultSession?.isTheSameSession(activeSessions.remote) == true) { + addRemoteViewModelIfNeeded(this, activeSessions.remote) + addStreamViewModel(this, AudioManager.STREAM_MUSIC) + } else { + addStreamViewModel(this, AudioManager.STREAM_MUSIC) + addRemoteViewModelIfNeeded(this, activeSessions.remote) } - ) - } - } - val sliderViewModels: StateFlow<List<SliderViewModel>> = - combine(remoteSessionsViewModels, streamViewModels) { - remoteSessionsViewModels, - streamViewModels -> - remoteSessionsViewModels + streamViewModels + addStreamViewModel(this, AudioManager.STREAM_VOICE_CALL) + addStreamViewModel(this, AudioManager.STREAM_RING) + addStreamViewModel(this, AudioManager.STREAM_NOTIFICATION) + addStreamViewModel(this, AudioManager.STREAM_ALARM) + } + emit(viewModels) + } } .stateIn(scope, SharingStarted.Eagerly, emptyList()) @@ -103,12 +86,41 @@ constructor( val isExpanded: StateFlow<Boolean> = merge( - mutableIsExpanded.onStart { emit(false) }, - mediaOutputInteractor.mediaDeviceSession.map { !it.isPlaying() }, + mutableIsExpanded, + mediaOutputInteractor.defaultActiveMediaSession.flatMapLatest { + if (it == null) flowOf(true) + else mediaDeviceSessionInteractor.playbackState(it).map { it?.isActive != true } + }, ) .stateIn(scope, SharingStarted.Eagerly, false) fun onExpandedChanged(isExpanded: Boolean) { scope.launch { mutableIsExpanded.emit(isExpanded) } } + + private fun CoroutineScope.addRemoteViewModelIfNeeded( + list: MutableList<SliderViewModel>, + remoteMediaDeviceSession: MediaDeviceSession? + ) { + if (remoteMediaDeviceSession?.canAdjustVolume == true) { + val viewModel = + castVolumeSliderViewModelFactory.create( + remoteMediaDeviceSession, + this, + ) + list.add(viewModel) + } + } + + private fun CoroutineScope.addStreamViewModel( + list: MutableList<SliderViewModel>, + stream: Int, + ) { + val viewModel = + streamSliderViewModelFactory.create( + AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)), + this, + ) + list.add(viewModel) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt index 6d52e525d238..97c2db5f9348 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.asStateFlow class FakeMediaControllerRepository : MediaControllerRepository { private val mutableActiveLocalMediaController = MutableStateFlow<MediaController?>(null) - override val activeLocalMediaController: StateFlow<MediaController?> = + override val activeMediaController: StateFlow<MediaController?> = mutableActiveLocalMediaController.asStateFlow() fun setActiveLocalMediaController(controller: MediaController?) { |