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.media.controls.domain.pipeline.interactor 18 19 import android.app.PendingIntent 20 import android.media.MediaDescription 21 import android.media.session.MediaSession 22 import android.media.session.PlaybackState 23 import android.service.notification.StatusBarNotification 24 import com.android.internal.logging.InstanceId 25 import com.android.systemui.CoreStartable 26 import com.android.systemui.dagger.SysUISingleton 27 import com.android.systemui.dagger.qualifiers.Application 28 import com.android.systemui.media.controls.data.repository.MediaFilterRepository 29 import com.android.systemui.media.controls.domain.pipeline.MediaDataCombineLatest 30 import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl 31 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager 32 import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor 33 import com.android.systemui.media.controls.domain.pipeline.MediaDeviceManager 34 import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilter 35 import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener 36 import com.android.systemui.media.controls.domain.resume.MediaResumeListener 37 import com.android.systemui.media.controls.shared.model.MediaCommonModel 38 import com.android.systemui.media.controls.util.MediaFlags 39 import com.android.systemui.scene.shared.flag.SceneContainerFlag 40 import java.io.PrintWriter 41 import javax.inject.Inject 42 import kotlinx.coroutines.CoroutineScope 43 import kotlinx.coroutines.ExperimentalCoroutinesApi 44 import kotlinx.coroutines.flow.SharingStarted 45 import kotlinx.coroutines.flow.StateFlow 46 import kotlinx.coroutines.flow.combine 47 import kotlinx.coroutines.flow.stateIn 48 49 /** Encapsulates business logic for media pipeline. */ 50 @OptIn(ExperimentalCoroutinesApi::class) 51 @SysUISingleton 52 class MediaCarouselInteractor 53 @Inject 54 constructor( 55 @Application applicationScope: CoroutineScope, 56 private val mediaDataProcessor: MediaDataProcessor, 57 private val mediaTimeoutListener: MediaTimeoutListener, 58 private val mediaResumeListener: MediaResumeListener, 59 private val mediaSessionBasedFilter: MediaSessionBasedFilter, 60 private val mediaDeviceManager: MediaDeviceManager, 61 private val mediaDataCombineLatest: MediaDataCombineLatest, 62 private val mediaDataFilter: MediaDataFilterImpl, 63 private val mediaFilterRepository: MediaFilterRepository, 64 private val mediaFlags: MediaFlags, 65 ) : MediaDataManager, CoreStartable { 66 67 /** Are there any media notifications active, including the recommendations? */ 68 val hasActiveMediaOrRecommendation: StateFlow<Boolean> = 69 combine( 70 mediaFilterRepository.selectedUserEntries, 71 mediaFilterRepository.smartspaceMediaData, 72 mediaFilterRepository.reactivatedId 73 ) { entries, smartspaceMediaData, reactivatedKey -> 74 entries.any { it.value.active } || 75 (smartspaceMediaData.isActive && 76 (smartspaceMediaData.isValid() || reactivatedKey != null)) 77 } 78 .stateIn( 79 scope = applicationScope, 80 started = SharingStarted.WhileSubscribed(), 81 initialValue = false, 82 ) 83 84 /** Are there any media entries we should display, including the recommendations? */ 85 val hasAnyMediaOrRecommendation: StateFlow<Boolean> = 86 combine( 87 mediaFilterRepository.selectedUserEntries, 88 mediaFilterRepository.smartspaceMediaData 89 ) { entries, smartspaceMediaData -> 90 entries.isNotEmpty() || 91 (if (mediaFlags.isPersistentSsCardEnabled()) { 92 smartspaceMediaData.isValid() 93 } else { 94 smartspaceMediaData.isActive && smartspaceMediaData.isValid() 95 }) 96 } 97 .stateIn( 98 scope = applicationScope, 99 started = SharingStarted.WhileSubscribed(), 100 initialValue = false, 101 ) 102 103 /** The current list for user media instances */ 104 val currentMedia: StateFlow<List<MediaCommonModel>> = mediaFilterRepository.currentMedia 105 106 override fun start() { 107 if (!mediaFlags.isSceneContainerEnabled()) { 108 return 109 } 110 111 // Initialize the internal processing pipeline. The listeners at the front of the pipeline 112 // are set as internal listeners so that they receive events. From there, events are 113 // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, 114 // so it is responsible for dispatching events to external listeners. To achieve this, 115 // external listeners that are registered with [MediaDataManager.addListener] are actually 116 // registered as listeners to mediaDataFilter. 117 addInternalListener(mediaTimeoutListener) 118 addInternalListener(mediaResumeListener) 119 addInternalListener(mediaSessionBasedFilter) 120 mediaSessionBasedFilter.addListener(mediaDeviceManager) 121 mediaSessionBasedFilter.addListener(mediaDataCombineLatest) 122 mediaDeviceManager.addListener(mediaDataCombineLatest) 123 mediaDataCombineLatest.addListener(mediaDataFilter) 124 125 // Set up links back into the pipeline for listeners that need to send events upstream. 126 mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> 127 mediaDataProcessor.setInactive(key, timedOut) 128 } 129 mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> 130 mediaDataProcessor.updateState(key, state) 131 } 132 mediaTimeoutListener.sessionCallback = { key: String -> 133 mediaDataProcessor.onSessionDestroyed(key) 134 } 135 mediaResumeListener.setManager(this) 136 mediaDataFilter.mediaDataProcessor = mediaDataProcessor 137 } 138 139 override fun addListener(listener: MediaDataManager.Listener) { 140 mediaDataFilter.addListener(listener) 141 } 142 143 override fun removeListener(listener: MediaDataManager.Listener) { 144 mediaDataFilter.removeListener(listener) 145 } 146 147 override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) = unsupported 148 149 override fun onNotificationAdded(key: String, sbn: StatusBarNotification) { 150 mediaDataProcessor.onNotificationAdded(key, sbn) 151 } 152 153 override fun destroy() { 154 mediaSessionBasedFilter.removeListener(mediaDeviceManager) 155 mediaSessionBasedFilter.removeListener(mediaDataCombineLatest) 156 mediaDeviceManager.removeListener(mediaDataCombineLatest) 157 mediaDataCombineLatest.removeListener(mediaDataFilter) 158 mediaDataProcessor.destroy() 159 } 160 161 override fun setResumeAction(key: String, action: Runnable?) { 162 mediaDataProcessor.setResumeAction(key, action) 163 } 164 165 override fun addResumptionControls( 166 userId: Int, 167 desc: MediaDescription, 168 action: Runnable, 169 token: MediaSession.Token, 170 appName: String, 171 appIntent: PendingIntent, 172 packageName: String 173 ) { 174 mediaDataProcessor.addResumptionControls( 175 userId, 176 desc, 177 action, 178 token, 179 appName, 180 appIntent, 181 packageName 182 ) 183 } 184 185 override fun dismissMediaData(key: String, delay: Long, userInitiated: Boolean): Boolean { 186 return mediaDataProcessor.dismissMediaData(key, delay, userInitiated) 187 } 188 189 fun removeMediaControl(instanceId: InstanceId, delay: Long) { 190 mediaDataProcessor.dismissMediaData(instanceId, delay, userInitiated = false) 191 } 192 193 override fun dismissSmartspaceRecommendation(key: String, delay: Long) { 194 return mediaDataProcessor.dismissSmartspaceRecommendation(key, delay) 195 } 196 197 override fun setRecommendationInactive(key: String) = unsupported 198 199 override fun onNotificationRemoved(key: String) { 200 mediaDataProcessor.onNotificationRemoved(key) 201 } 202 203 override fun setMediaResumptionEnabled(isEnabled: Boolean) { 204 mediaDataProcessor.setMediaResumptionEnabled(isEnabled) 205 } 206 207 override fun onSwipeToDismiss() { 208 mediaDataFilter.onSwipeToDismiss() 209 } 210 211 override fun hasActiveMediaOrRecommendation() = hasActiveMediaOrRecommendation.value 212 213 override fun hasAnyMediaOrRecommendation() = hasAnyMediaOrRecommendation.value 214 215 override fun hasActiveMedia() = mediaFilterRepository.hasActiveMedia() 216 217 override fun hasAnyMedia() = mediaFilterRepository.hasAnyMedia() 218 219 override fun isRecommendationActive() = mediaFilterRepository.isRecommendationActive() 220 221 fun reorderMedia() { 222 mediaFilterRepository.setOrderedMedia() 223 } 224 225 /** Add a listener for internal events. */ 226 private fun addInternalListener(listener: MediaDataManager.Listener) = 227 mediaDataProcessor.addInternalListener(listener) 228 229 override fun dump(pw: PrintWriter, args: Array<out String>) { 230 mediaDeviceManager.dump(pw) 231 } 232 233 companion object { 234 val unsupported: Nothing 235 get() = 236 error("Code path not supported when ${SceneContainerFlag.DESCRIPTION} is enabled") 237 } 238 } 239