• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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