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