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.data.repository
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.media.controls.data.model.MediaSortKeyModel
24 import com.android.systemui.media.controls.shared.model.MediaCommonModel
25 import com.android.systemui.media.controls.shared.model.MediaData
26 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
27 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
28 import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
29 import com.android.systemui.statusbar.policy.ConfigurationController
30 import com.android.systemui.util.time.SystemClock
31 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
32 import java.util.Locale
33 import java.util.TreeMap
34 import javax.inject.Inject
35 import kotlinx.coroutines.channels.awaitClose
36 import kotlinx.coroutines.flow.Flow
37 import kotlinx.coroutines.flow.MutableStateFlow
38 import kotlinx.coroutines.flow.StateFlow
39 import kotlinx.coroutines.flow.asStateFlow
40 
41 /** A repository that holds the state of filtered media data on the device. */
42 @SysUISingleton
43 class MediaFilterRepository
44 @Inject
45 constructor(
46     @Application applicationContext: Context,
47     private val systemClock: SystemClock,
48     private val configurationController: ConfigurationController,
49 ) {
50 
51     val onAnyMediaConfigurationChange: Flow<Unit> = conflatedCallbackFlow {
52         val callback =
53             object : ConfigurationController.ConfigurationListener {
54                 override fun onDensityOrFontScaleChanged() {
55                     trySend(Unit)
56                 }
57 
58                 override fun onThemeChanged() {
59                     trySend(Unit)
60                 }
61 
62                 override fun onUiModeChanged() {
63                     trySend(Unit)
64                 }
65 
66                 override fun onLocaleListChanged() {
67                     if (locale != applicationContext.resources.configuration.locales.get(0)) {
68                         locale = applicationContext.resources.configuration.locales.get(0)
69                         trySend(Unit)
70                     }
71                 }
72             }
73         configurationController.addCallback(callback)
74         trySend(Unit)
75         awaitClose { configurationController.removeCallback(callback) }
76     }
77 
78     /** Instance id of media control that recommendations card reactivated. */
79     private val _reactivatedId: MutableStateFlow<InstanceId?> = MutableStateFlow(null)
80     val reactivatedId: StateFlow<InstanceId?> = _reactivatedId.asStateFlow()
81 
82     private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> =
83         MutableStateFlow(SmartspaceMediaData())
84     val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow()
85 
86     private val _selectedUserEntries: MutableStateFlow<Map<InstanceId, MediaData>> =
87         MutableStateFlow(LinkedHashMap())
88     val selectedUserEntries: StateFlow<Map<InstanceId, MediaData>> =
89         _selectedUserEntries.asStateFlow()
90 
91     private val _allUserEntries: MutableStateFlow<Map<String, MediaData>> =
92         MutableStateFlow(LinkedHashMap())
93     val allUserEntries: StateFlow<Map<String, MediaData>> = _allUserEntries.asStateFlow()
94 
95     private val comparator =
96         compareByDescending<MediaSortKeyModel> {
97                 it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_LOCAL
98             }
99             .thenByDescending {
100                 it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL
101             }
102             .thenByDescending { it.active }
103             .thenByDescending { it.isPrioritizedRec }
104             .thenByDescending { !it.isResume }
105             .thenByDescending { it.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
106             .thenByDescending { it.lastActive }
107             .thenByDescending { it.updateTime }
108             .thenByDescending { it.notificationKey }
109 
110     private val _currentMedia: MutableStateFlow<List<MediaCommonModel>> =
111         MutableStateFlow(mutableListOf())
112     val currentMedia = _currentMedia.asStateFlow()
113 
114     private var sortedMedia = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
115     private var mediaFromRecPackageName: String? = null
116     private var locale: Locale = applicationContext.resources.configuration.locales.get(0)
117 
118     fun addMediaEntry(key: String, data: MediaData) {
119         val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
120         entries[key] = data
121         _allUserEntries.value = entries
122     }
123 
124     /**
125      * Removes the media entry corresponding to the given [key].
126      *
127      * @return media data if an entry is actually removed, `null` otherwise.
128      */
129     fun removeMediaEntry(key: String): MediaData? {
130         val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
131         val mediaData = entries.remove(key)
132         _allUserEntries.value = entries
133         return mediaData
134     }
135 
136     fun addSelectedUserMediaEntry(data: MediaData) {
137         val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value)
138         entries[data.instanceId] = data
139         _selectedUserEntries.value = entries
140     }
141 
142     /**
143      * Removes selected user media entry given the corresponding key.
144      *
145      * @return media data if an entry is actually removed, `null` otherwise.
146      */
147     fun removeSelectedUserMediaEntry(key: InstanceId): MediaData? {
148         val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value)
149         val mediaData = entries.remove(key)
150         _selectedUserEntries.value = entries
151         return mediaData
152     }
153 
154     /**
155      * Removes selected user media entry given a key and media data.
156      *
157      * @return true if media data is removed, false otherwise.
158      */
159     fun removeSelectedUserMediaEntry(key: InstanceId, data: MediaData): Boolean {
160         val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value)
161         val succeed = entries.remove(key, data)
162         if (!succeed) {
163             return false
164         }
165         _selectedUserEntries.value = entries
166         return true
167     }
168 
169     fun clearSelectedUserMedia() {
170         _selectedUserEntries.value = LinkedHashMap()
171     }
172 
173     /** Updates recommendation data with a new smartspace media data. */
174     fun setRecommendation(smartspaceMediaData: SmartspaceMediaData) {
175         _smartspaceMediaData.value = smartspaceMediaData
176     }
177 
178     /** Updates media control key that recommendations card reactivated. */
179     fun setReactivatedId(instanceId: InstanceId?) {
180         _reactivatedId.value = instanceId
181     }
182 
183     fun addMediaDataLoadingState(mediaDataLoadingModel: MediaDataLoadingModel) {
184         val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
185         sortedMap.putAll(
186             sortedMedia.filter { (_, commonModel) ->
187                 commonModel !is MediaCommonModel.MediaControl ||
188                     commonModel.mediaLoadedModel.instanceId != mediaDataLoadingModel.instanceId
189             }
190         )
191 
192         _selectedUserEntries.value[mediaDataLoadingModel.instanceId]?.let {
193             val sortKey =
194                 MediaSortKeyModel(
195                     isPrioritizedRec = false,
196                     it.isPlaying,
197                     it.playbackLocation,
198                     it.active,
199                     it.resumption,
200                     it.lastActive,
201                     it.notificationKey,
202                     systemClock.currentTimeMillis(),
203                     it.instanceId,
204                 )
205 
206             if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) {
207                 val newCommonModel =
208                     MediaCommonModel.MediaControl(
209                         mediaDataLoadingModel,
210                         canBeRemoved(it),
211                         isMediaFromRec(it)
212                     )
213                 sortedMap[sortKey] = newCommonModel
214 
215                 // On Addition or tapping on recommendations, we should show the new order of media.
216                 if (mediaFromRecPackageName == it.packageName) {
217                     if (it.isPlaying == true) {
218                         mediaFromRecPackageName = null
219                         _currentMedia.value = sortedMap.values.toList()
220                     }
221                 } else if (sortedMap.size > _currentMedia.value.size && it.active) {
222                     _currentMedia.value = sortedMap.values.toList()
223                 } else {
224                     // When loading an update for an existing media control.
225                     val currentList =
226                         mutableListOf<MediaCommonModel>().apply { addAll(_currentMedia.value) }
227                     currentList.forEachIndexed { index, mediaCommonModel ->
228                         if (
229                             mediaCommonModel is MediaCommonModel.MediaControl &&
230                                 mediaCommonModel.mediaLoadedModel.instanceId ==
231                                     mediaDataLoadingModel.instanceId &&
232                                 mediaCommonModel != newCommonModel
233                         ) {
234                             // Update media model if changed.
235                             currentList[index] = newCommonModel
236                         }
237                     }
238                     _currentMedia.value = currentList
239                 }
240             }
241         }
242 
243         sortedMedia = sortedMap
244 
245         // On removal we want to keep the order being shown to user.
246         if (mediaDataLoadingModel is MediaDataLoadingModel.Removed) {
247             _currentMedia.value =
248                 _currentMedia.value.filter { commonModel ->
249                     commonModel !is MediaCommonModel.MediaControl ||
250                         mediaDataLoadingModel.instanceId != commonModel.mediaLoadedModel.instanceId
251                 }
252         }
253     }
254 
255     fun setRecommendationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) {
256         val isPrioritized =
257             when (smartspaceMediaLoadingModel) {
258                 is SmartspaceMediaLoadingModel.Loaded -> smartspaceMediaLoadingModel.isPrioritized
259                 else -> false
260             }
261         val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
262         sortedMap.putAll(
263             sortedMedia.filter { (_, commonModel) ->
264                 commonModel !is MediaCommonModel.MediaRecommendations
265             }
266         )
267 
268         val sortKey =
269             MediaSortKeyModel(
270                 isPrioritizedRec = isPrioritized,
271                 isPlaying = false,
272                 active = _smartspaceMediaData.value.isActive,
273             )
274         when (smartspaceMediaLoadingModel) {
275             is SmartspaceMediaLoadingModel.Loaded ->
276                 sortedMap[sortKey] =
277                     MediaCommonModel.MediaRecommendations(smartspaceMediaLoadingModel)
278             is SmartspaceMediaLoadingModel.Removed ->
279                 _currentMedia.value =
280                     _currentMedia.value.filter { commonModel ->
281                         commonModel !is MediaCommonModel.MediaRecommendations
282                     }
283         }
284 
285         if (sortedMap.size > sortedMedia.size) {
286             _currentMedia.value = sortedMap.values.toList()
287         }
288         sortedMedia = sortedMap
289     }
290 
291     fun setOrderedMedia() {
292         _currentMedia.value = sortedMedia.values.toList()
293     }
294 
295     fun setMediaFromRecPackageName(packageName: String) {
296         mediaFromRecPackageName = packageName
297     }
298 
299     fun hasActiveMedia(): Boolean {
300         return _selectedUserEntries.value.any { it.value.active }
301     }
302 
303     fun hasAnyMedia(): Boolean {
304         return _selectedUserEntries.value.entries.isNotEmpty()
305     }
306 
307     fun isRecommendationActive(): Boolean {
308         return _smartspaceMediaData.value.isActive
309     }
310 
311     private fun canBeRemoved(data: MediaData): Boolean {
312         return data.isPlaying?.let { !it } ?: data.isClearable && !data.active
313     }
314 
315     private fun isMediaFromRec(data: MediaData): Boolean {
316         return data.isPlaying == true && mediaFromRecPackageName == data.packageName
317     }
318 }
319