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