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