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