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.ui.viewmodel
18 
19 import android.content.Context
20 import com.android.internal.logging.InstanceId
21 import com.android.systemui.dagger.SysUISingleton
22 import com.android.systemui.dagger.qualifiers.Application
23 import com.android.systemui.dagger.qualifiers.Background
24 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
25 import com.android.systemui.media.controls.domain.pipeline.interactor.factory.MediaControlInteractorFactory
26 import com.android.systemui.media.controls.shared.model.MediaCommonModel
27 import com.android.systemui.media.controls.util.MediaFlags
28 import com.android.systemui.media.controls.util.MediaUiEventLogger
29 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
30 import com.android.systemui.util.Utils
31 import java.util.concurrent.Executor
32 import javax.inject.Inject
33 import kotlinx.coroutines.CoroutineDispatcher
34 import kotlinx.coroutines.CoroutineScope
35 import kotlinx.coroutines.flow.SharingStarted
36 import kotlinx.coroutines.flow.StateFlow
37 import kotlinx.coroutines.flow.map
38 import kotlinx.coroutines.flow.stateIn
39 
40 /** Models UI state and handles user inputs for media carousel */
41 @SysUISingleton
42 class MediaCarouselViewModel
43 @Inject
44 constructor(
45     @Application private val applicationScope: CoroutineScope,
46     @Application private val applicationContext: Context,
47     @Background private val backgroundDispatcher: CoroutineDispatcher,
48     @Background private val backgroundExecutor: Executor,
49     private val visualStabilityProvider: VisualStabilityProvider,
50     private val interactor: MediaCarouselInteractor,
51     private val controlInteractorFactory: MediaControlInteractorFactory,
52     private val recommendationsViewModel: MediaRecommendationsViewModel,
53     private val logger: MediaUiEventLogger,
54     private val mediaFlags: MediaFlags,
55 ) {
56 
57     val mediaItems: StateFlow<List<MediaCommonViewModel>> =
58         interactor.currentMedia
59             .map { sortedItems ->
60                 val mediaList = buildList {
61                     sortedItems.forEach { commonModel ->
62                         // When view is started we should make sure to clean models that are pending
63                         // removal.
64                         // This action should only be triggered once.
65                         if (!allowReorder || !modelsPendingRemoval.contains(commonModel)) {
66                             when (commonModel) {
67                                 is MediaCommonModel.MediaControl -> add(toViewModel(commonModel))
68                                 is MediaCommonModel.MediaRecommendations ->
69                                     add(toViewModel(commonModel))
70                             }
71                         }
72                     }
73                 }
74                 if (allowReorder) {
75                     if (modelsPendingRemoval.size > 0) {
76                         updateHostVisibility()
77                     }
78                     modelsPendingRemoval.clear()
79                 }
80                 allowReorder = false
81 
82                 mediaList
83             }
84             .stateIn(
85                 scope = applicationScope,
86                 started = SharingStarted.WhileSubscribed(),
87                 initialValue = emptyList(),
88             )
89 
90     var updateHostVisibility: () -> Unit = {}
91 
92     private val mediaControlByInstanceId =
93         mutableMapOf<InstanceId, MediaCommonViewModel.MediaControl>()
94 
95     private var mediaRecs: MediaCommonViewModel.MediaRecommendations? = null
96 
97     private var modelsPendingRemoval: MutableSet<MediaCommonModel> = mutableSetOf()
98 
99     private var allowReorder = false
100 
101     fun onSwipeToDismiss() {
102         logger.logSwipeDismiss()
103         interactor.onSwipeToDismiss()
104     }
105 
106     fun onReorderingAllowed() {
107         allowReorder = true
108         interactor.reorderMedia()
109     }
110 
111     private fun toViewModel(
112         commonModel: MediaCommonModel.MediaControl
113     ): MediaCommonViewModel.MediaControl {
114         val instanceId = commonModel.mediaLoadedModel.instanceId
115         return mediaControlByInstanceId[instanceId]?.copy(
116             immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi
117         )
118             ?: MediaCommonViewModel.MediaControl(
119                     instanceId = instanceId,
120                     immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi,
121                     controlViewModel = createMediaControlViewModel(instanceId),
122                     onAdded = { onMediaControlAddedOrUpdated(it, commonModel) },
123                     onRemoved = {
124                         interactor.removeMediaControl(instanceId, delay = 0L)
125                         mediaControlByInstanceId.remove(instanceId)
126                     },
127                     onUpdated = { onMediaControlAddedOrUpdated(it, commonModel) },
128                     isMediaFromRec = commonModel.isMediaFromRec
129                 )
130                 .also { mediaControlByInstanceId[instanceId] = it }
131     }
132 
133     private fun createMediaControlViewModel(instanceId: InstanceId): MediaControlViewModel {
134         return MediaControlViewModel(
135             applicationContext = applicationContext,
136             backgroundDispatcher = backgroundDispatcher,
137             backgroundExecutor = backgroundExecutor,
138             interactor = controlInteractorFactory.create(instanceId),
139             logger = logger,
140         )
141     }
142 
143     private fun toViewModel(
144         commonModel: MediaCommonModel.MediaRecommendations
145     ): MediaCommonViewModel.MediaRecommendations {
146         return mediaRecs?.copy(
147             key = commonModel.recsLoadingModel.key,
148             loadingEnabled =
149                 interactor.isRecommendationActive() || mediaFlags.isPersistentSsCardEnabled()
150         )
151             ?: MediaCommonViewModel.MediaRecommendations(
152                     key = commonModel.recsLoadingModel.key,
153                     loadingEnabled =
154                         interactor.isRecommendationActive() ||
155                             mediaFlags.isPersistentSsCardEnabled(),
156                     recsViewModel = recommendationsViewModel,
157                     onAdded = { commonViewModel ->
158                         onMediaRecommendationAddedOrUpdated(
159                             commonViewModel as MediaCommonViewModel.MediaRecommendations
160                         )
161                     },
162                     onRemoved = { immediatelyRemove ->
163                         onMediaRecommendationRemoved(commonModel, immediatelyRemove)
164                     },
165                     onUpdated = { commonViewModel ->
166                         onMediaRecommendationAddedOrUpdated(
167                             commonViewModel as MediaCommonViewModel.MediaRecommendations
168                         )
169                     },
170                 )
171                 .also { mediaRecs = it }
172     }
173 
174     private fun onMediaControlAddedOrUpdated(
175         commonViewModel: MediaCommonViewModel,
176         commonModel: MediaCommonModel.MediaControl
177     ) {
178         // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_RECEIVED)
179         if (commonModel.canBeRemoved && !Utils.useMediaResumption(applicationContext)) {
180             // This media control is due for removal as it is now paused + timed out, and resumption
181             // setting is off.
182             if (isReorderingAllowed()) {
183                 commonViewModel.onRemoved(true)
184             } else {
185                 modelsPendingRemoval.add(commonModel)
186             }
187         } else {
188             modelsPendingRemoval.remove(commonModel)
189         }
190     }
191 
192     private fun onMediaRecommendationAddedOrUpdated(
193         commonViewModel: MediaCommonViewModel.MediaRecommendations
194     ) {
195         if (!interactor.isRecommendationActive()) {
196             if (!mediaFlags.isPersistentSsCardEnabled()) {
197                 commonViewModel.onRemoved(true)
198             }
199         } else {
200             // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_RECEIVED)
201         }
202     }
203 
204     private fun onMediaRecommendationRemoved(
205         commonModel: MediaCommonModel.MediaRecommendations,
206         immediatelyRemove: Boolean
207     ) {
208         if (immediatelyRemove || isReorderingAllowed()) {
209             interactor.dismissSmartspaceRecommendation(commonModel.recsLoadingModel.key, 0L)
210             mediaRecs = null
211             if (!immediatelyRemove) {
212                 // Although it wasn't requested, we were able to process the removal
213                 // immediately since reordering is allowed. So, notify hosts to update
214                 updateHostVisibility()
215             }
216         } else {
217             modelsPendingRemoval.add(commonModel)
218         }
219     }
220 
221     private fun isReorderingAllowed(): Boolean {
222         return visualStabilityProvider.isReorderingAllowed
223     }
224 }
225