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