• 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.data.repository
18 
19 import com.android.internal.logging.InstanceId
20 import com.android.systemui.dagger.SysUISingleton
21 import com.android.systemui.media.controls.data.model.MediaSortKeyModel
22 import com.android.systemui.media.controls.shared.model.MediaCommonModel
23 import com.android.systemui.media.controls.shared.model.MediaData
24 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
25 import com.android.systemui.util.time.SystemClock
26 import java.util.TreeMap
27 import javax.inject.Inject
28 import kotlinx.coroutines.flow.MutableStateFlow
29 import kotlinx.coroutines.flow.StateFlow
30 import kotlinx.coroutines.flow.asStateFlow
31 
32 /** A repository that holds the state of filtered media data on the device. */
33 @SysUISingleton
34 class MediaFilterRepository @Inject constructor(private val systemClock: SystemClock) {
35 
36     private val _selectedUserEntries: MutableStateFlow<Map<InstanceId, MediaData>> =
37         MutableStateFlow(LinkedHashMap())
38     val selectedUserEntries: StateFlow<Map<InstanceId, MediaData>> =
39         _selectedUserEntries.asStateFlow()
40 
41     private val _allUserEntries: MutableStateFlow<Map<String, MediaData>> =
42         MutableStateFlow(LinkedHashMap())
43     val allUserEntries: StateFlow<Map<String, MediaData>> = _allUserEntries.asStateFlow()
44 
45     private val comparator =
46         compareByDescending<MediaSortKeyModel> {
47                 it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_LOCAL
48             }
49             .thenByDescending {
50                 it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL
51             }
52             .thenByDescending { it.active }
53             .thenByDescending { !it.isResume }
54             .thenByDescending { it.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
55             .thenByDescending { it.lastActive }
56             .thenByDescending { it.updateTime }
57             .thenByDescending { it.notificationKey }
58 
59     private val _currentMedia: MutableStateFlow<List<MediaCommonModel>> =
60         MutableStateFlow(mutableListOf())
61     val currentMedia = _currentMedia.asStateFlow()
62 
63     private var sortedMedia = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
64 
65     fun addMediaEntry(key: String, data: MediaData) {
66         val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
67         entries[key] = data
68         _allUserEntries.value = entries
69     }
70 
71     /**
72      * Removes the media entry corresponding to the given [key].
73      *
74      * @return media data if an entry is actually removed, `null` otherwise.
75      */
76     fun removeMediaEntry(key: String): MediaData? {
77         val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
78         val mediaData = entries.remove(key)
79         _allUserEntries.value = entries
80         return mediaData
81     }
82 
83     /** @return whether the added media data already exists. */
84     fun addSelectedUserMediaEntry(data: MediaData): Boolean {
85         val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value)
86         val update = _selectedUserEntries.value.containsKey(data.instanceId)
87         entries[data.instanceId] = data
88         _selectedUserEntries.value = entries
89         return update
90     }
91 
92     /**
93      * Removes selected user media entry given the corresponding key.
94      *
95      * @return media data if an entry is actually removed, `null` otherwise.
96      */
97     fun removeSelectedUserMediaEntry(key: InstanceId): MediaData? {
98         val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value)
99         val mediaData = entries.remove(key)
100         _selectedUserEntries.value = entries
101         return mediaData
102     }
103 
104     /**
105      * Removes selected user media entry given a key and media data.
106      *
107      * @return true if media data is removed, false otherwise.
108      */
109     fun removeSelectedUserMediaEntry(key: InstanceId, data: MediaData): Boolean {
110         val entries = LinkedHashMap<InstanceId, MediaData>(_selectedUserEntries.value)
111         val succeed = entries.remove(key, data)
112         if (!succeed) {
113             return false
114         }
115         _selectedUserEntries.value = entries
116         return true
117     }
118 
119     fun clearSelectedUserMedia() {
120         _selectedUserEntries.value = LinkedHashMap()
121     }
122 
123     fun addMediaDataLoadingState(
124         mediaDataLoadingModel: MediaDataLoadingModel,
125         isUpdate: Boolean = true,
126     ) {
127         val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
128         sortedMap.putAll(
129             sortedMedia.filter { (_, commonModel) ->
130                 commonModel.mediaLoadedModel.instanceId != mediaDataLoadingModel.instanceId
131             }
132         )
133 
134         _selectedUserEntries.value[mediaDataLoadingModel.instanceId]?.let {
135             val sortKey =
136                 MediaSortKeyModel(
137                     it.isPlaying,
138                     it.playbackLocation,
139                     it.active,
140                     it.resumption,
141                     it.lastActive,
142                     it.notificationKey,
143                     systemClock.currentTimeMillis(),
144                     it.instanceId,
145                 )
146 
147             if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) {
148                 val newCommonModel =
149                     MediaCommonModel(
150                         mediaDataLoadingModel,
151                         canBeRemoved(it),
152                         if (isUpdate) systemClock.currentTimeMillis() else 0,
153                     )
154                 sortedMap[sortKey] = newCommonModel
155 
156                 var isNewToCurrentMedia = true
157                 val currentList =
158                     mutableListOf<MediaCommonModel>().apply { addAll(_currentMedia.value) }
159                 currentList.forEachIndexed { index, mediaCommonModel ->
160                     if (
161                         mediaCommonModel.mediaLoadedModel.instanceId ==
162                             mediaDataLoadingModel.instanceId
163                     ) {
164                         // When loading an update for an existing media control.
165                         isNewToCurrentMedia = false
166                         if (mediaCommonModel != newCommonModel) {
167                             // Update media model if changed.
168                             currentList[index] = newCommonModel
169                         }
170                     }
171                 }
172                 if (isNewToCurrentMedia && it.active) {
173                     _currentMedia.value = sortedMap.values.toList()
174                 } else {
175                     _currentMedia.value = currentList
176                 }
177 
178                 sortedMedia = sortedMap
179             }
180         }
181 
182         // On removal we want to keep the order being shown to user.
183         if (mediaDataLoadingModel is MediaDataLoadingModel.Removed) {
184             _currentMedia.value =
185                 _currentMedia.value.filter { commonModel ->
186                     mediaDataLoadingModel.instanceId != commonModel.mediaLoadedModel.instanceId
187                 }
188             sortedMedia = sortedMap
189         }
190     }
191 
192     fun setOrderedMedia() {
193         _currentMedia.value = sortedMedia.values.toList()
194     }
195 
196     fun hasActiveMedia(): Boolean {
197         return _selectedUserEntries.value.any { it.value.active }
198     }
199 
200     fun hasAnyMedia(): Boolean {
201         return _selectedUserEntries.value.entries.isNotEmpty()
202     }
203 
204     private fun canBeRemoved(data: MediaData): Boolean {
205         return data.isPlaying?.let { !it } ?: data.isClearable && !data.active
206     }
207 }
208