1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
18 
19 import android.content.pm.PackageManager
20 import android.media.VolumeProvider
21 import android.media.session.MediaController
22 import android.util.Log
23 import com.android.settingslib.media.MediaDevice
24 import com.android.settingslib.volume.data.repository.LocalMediaRepository
25 import com.android.settingslib.volume.data.repository.MediaControllerRepository
26 import com.android.systemui.dagger.qualifiers.Background
27 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
28 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions
29 import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
30 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
31 import com.android.systemui.volume.panel.shared.model.Result
32 import com.android.systemui.volume.panel.shared.model.filterData
33 import com.android.systemui.volume.panel.shared.model.wrapInResult
34 import javax.inject.Inject
35 import kotlin.coroutines.CoroutineContext
36 import kotlinx.coroutines.CoroutineScope
37 import kotlinx.coroutines.ExperimentalCoroutinesApi
38 import kotlinx.coroutines.coroutineScope
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.SharingStarted
41 import kotlinx.coroutines.flow.StateFlow
42 import kotlinx.coroutines.flow.distinctUntilChanged
43 import kotlinx.coroutines.flow.flatMapLatest
44 import kotlinx.coroutines.flow.flowOf
45 import kotlinx.coroutines.flow.flowOn
46 import kotlinx.coroutines.flow.map
47 import kotlinx.coroutines.flow.merge
48 import kotlinx.coroutines.flow.onStart
49 import kotlinx.coroutines.flow.stateIn
50 import kotlinx.coroutines.flow.transformLatest
51 import kotlinx.coroutines.withContext
52 
53 /** Provides observable models about the current media session state. */
54 @OptIn(ExperimentalCoroutinesApi::class)
55 @VolumePanelScope
56 class MediaOutputInteractor
57 @Inject
58 constructor(
59     private val localMediaRepositoryFactory: LocalMediaRepositoryFactory,
60     private val packageManager: PackageManager,
61     @VolumePanelScope private val coroutineScope: CoroutineScope,
62     @Background private val backgroundCoroutineContext: CoroutineContext,
63     mediaControllerRepository: MediaControllerRepository,
64     private val mediaControllerInteractor: MediaControllerInteractor,
65 ) {
66 
67     private val activeMediaControllers: Flow<MediaControllers> =
68         mediaControllerRepository.activeSessions
69             .flatMapLatest { activeSessions ->
70                 activeSessions
71                     .map { activeSession -> activeSession.stateChanges() }
72                     .merge()
73                     .map { activeSessions }
74                     .onStart { emit(activeSessions) }
75             }
76             .map { getMediaControllers(it) }
77             .stateIn(coroutineScope, SharingStarted.Eagerly, MediaControllers(null, null))
78 
79     /** [MediaDeviceSessions] that contains currently active sessions. */
80     val activeMediaDeviceSessions: Flow<MediaDeviceSessions> =
81         activeMediaControllers
82             .map {
83                 MediaDeviceSessions(
84                     local = it.local?.mediaDeviceSession(),
85                     remote = it.remote?.mediaDeviceSession()
86                 )
87             }
88             .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSessions(null, null))
89 
90     /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */
91     val defaultActiveMediaSession: StateFlow<Result<MediaDeviceSession?>> =
92         activeMediaControllers
93             .map {
94                 when {
95                     it.local?.playbackState?.isActive == true -> it.local.mediaDeviceSession()
96                     it.remote?.playbackState?.isActive == true -> it.remote.mediaDeviceSession()
97                     it.local != null -> it.local.mediaDeviceSession()
98                     else -> null
99                 }
100             }
101             .wrapInResult()
102             .flowOn(backgroundCoroutineContext)
103             .stateIn(coroutineScope, SharingStarted.Eagerly, Result.Loading())
104 
105     private val localMediaRepository: Flow<LocalMediaRepository> =
106         defaultActiveMediaSession
107             .filterData()
108             .map { it?.packageName }
109             .distinctUntilChanged()
110             .transformLatest {
111                 coroutineScope { emit(localMediaRepositoryFactory.create(it, this)) }
112             }
113 
114     /** Currently connected [MediaDevice]. */
115     val currentConnectedDevice: Flow<MediaDevice?> =
116         localMediaRepository.flatMapLatest { it.currentConnectedDevice }.distinctUntilChanged()
117 
118     private suspend fun getApplicationLabel(packageName: String): CharSequence? {
119         return try {
120             withContext(backgroundCoroutineContext) {
121                 val appInfo =
122                     packageManager.getApplicationInfo(
123                         packageName,
124                         PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER
125                     )
126                 appInfo.loadLabel(packageManager)
127             }
128         } catch (e: PackageManager.NameNotFoundException) {
129             Log.e(TAG, "Unable to find info for package: $packageName")
130             null
131         }
132     }
133 
134     /** Finds local and remote media controllers. */
135     private fun getMediaControllers(
136         controllers: Collection<MediaController>,
137     ): MediaControllers {
138         var localController: MediaController? = null
139         var remoteController: MediaController? = null
140         val remoteMediaSessions: MutableSet<String> = mutableSetOf()
141         for (controller in controllers) {
142             val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
143             when (playbackInfo.playbackType) {
144                 MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
145                     // MediaController can't be local if there is a remote one for the same package
146                     if (localController?.packageName.equals(controller.packageName)) {
147                         localController = null
148                     }
149                     if (!remoteMediaSessions.contains(controller.packageName)) {
150                         remoteMediaSessions.add(controller.packageName)
151                         remoteController = chooseController(remoteController, controller)
152                     }
153                 }
154                 MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
155                     if (controller.packageName in remoteMediaSessions) continue
156                     localController = chooseController(localController, controller)
157                 }
158             }
159         }
160         return MediaControllers(local = localController, remote = remoteController)
161     }
162 
163     private fun chooseController(
164         currentController: MediaController?,
165         newController: MediaController,
166     ): MediaController {
167         if (currentController == null) {
168             return newController
169         }
170         val isNewControllerActive = newController.playbackState?.isActive == true
171         val isCurrentControllerActive = currentController.playbackState?.isActive == true
172         if (isNewControllerActive && !isCurrentControllerActive) {
173             return newController
174         }
175         return currentController
176     }
177 
178     private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? {
179         return MediaDeviceSession(
180             packageName = packageName,
181             sessionToken = sessionToken,
182             canAdjustVolume =
183                 playbackInfo != null &&
184                     playbackInfo?.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED,
185             appLabel = getApplicationLabel(packageName) ?: return null
186         )
187     }
188 
189     private fun MediaController?.stateChanges(): Flow<MediaController?> {
190         if (this == null) {
191             return flowOf(null)
192         }
193 
194         return mediaControllerInteractor
195             .stateChanges(this)
196             .map { this }
197             .onStart { emit(this@stateChanges) }
198     }
199 
200     private data class MediaControllers(
201         val local: MediaController?,
202         val remote: MediaController?,
203     )
204 
205     private companion object {
206         const val TAG = "MediaOutputInteractor"
207     }
208 }
209