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.domain.pipeline 18 19 import android.content.Context 20 import android.content.pm.UserInfo 21 import android.util.Log 22 import com.android.internal.annotations.KeepForWeakReference 23 import com.android.internal.annotations.VisibleForTesting 24 import com.android.internal.logging.InstanceId 25 import com.android.systemui.dagger.SysUISingleton 26 import com.android.systemui.dagger.qualifiers.Main 27 import com.android.systemui.media.controls.data.repository.MediaFilterRepository 28 import com.android.systemui.media.controls.shared.MediaLogger 29 import com.android.systemui.media.controls.shared.model.MediaData 30 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel 31 import com.android.systemui.settings.UserTracker 32 import com.android.systemui.statusbar.NotificationLockscreenUserManager 33 import com.android.systemui.util.time.SystemClock 34 import java.util.SortedMap 35 import java.util.concurrent.Executor 36 import javax.inject.Inject 37 38 private const val TAG = "MediaDataFilter" 39 private const val DEBUG = true 40 41 /** 42 * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user 43 * switches (removing entries for the previous user, adding back entries for the current user). 44 * 45 * This is added at the end of the pipeline since we may still need to handle callbacks from 46 * background users (e.g. timeouts). 47 */ 48 @SysUISingleton 49 class MediaDataFilterImpl 50 @Inject 51 constructor( 52 userTracker: UserTracker, 53 private val lockscreenUserManager: NotificationLockscreenUserManager, 54 @Main private val executor: Executor, 55 private val systemClock: SystemClock, 56 private val mediaFilterRepository: MediaFilterRepository, 57 private val mediaLogger: MediaLogger, 58 ) : MediaDataManager.Listener { 59 /** Non-UI listeners to media changes. */ 60 private val _listeners: MutableSet<MediaDataProcessor.Listener> = mutableSetOf() 61 val listeners: Set<MediaDataProcessor.Listener> 62 get() = _listeners.toSet() 63 64 lateinit var mediaDataProcessor: MediaDataProcessor 65 66 // Ensure the field (and associated reference) isn't removed during optimization. 67 @KeepForWeakReference 68 private val userTrackerCallback = 69 object : UserTracker.Callback { 70 override fun onUserChanged(newUser: Int, userContext: Context) { 71 handleUserSwitched() 72 } 73 74 override fun onProfilesChanged(profiles: List<UserInfo>) { 75 handleProfileChanged() 76 } 77 } 78 79 init { 80 userTracker.addCallback(userTrackerCallback, executor) 81 } 82 83 override fun onMediaDataLoaded( 84 key: String, 85 oldKey: String?, 86 data: MediaData, 87 immediately: Boolean, 88 receivedSmartspaceCardLatency: Int, 89 isSsReactivated: Boolean, 90 ) { 91 if (oldKey != null && oldKey != key) { 92 mediaFilterRepository.removeMediaEntry(oldKey) 93 } 94 mediaFilterRepository.addMediaEntry(key, data) 95 96 if ( 97 !lockscreenUserManager.isCurrentProfile(data.userId) || 98 !lockscreenUserManager.isProfileAvailable(data.userId) 99 ) { 100 return 101 } 102 103 val isUpdate = mediaFilterRepository.addSelectedUserMediaEntry(data) 104 105 mediaLogger.logMediaLoaded(data.instanceId, data.active, "loading media") 106 mediaFilterRepository.addMediaDataLoadingState( 107 MediaDataLoadingModel.Loaded(data.instanceId), 108 isUpdate, 109 ) 110 111 // Notify listeners 112 listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } 113 } 114 115 override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { 116 mediaFilterRepository.removeMediaEntry(key)?.let { mediaData -> 117 val instanceId = mediaData.instanceId 118 mediaFilterRepository.removeSelectedUserMediaEntry(instanceId)?.let { 119 mediaFilterRepository.addMediaDataLoadingState( 120 MediaDataLoadingModel.Removed(instanceId) 121 ) 122 mediaLogger.logMediaRemoved(instanceId, "removing media card") 123 // Only notify listeners if something actually changed 124 listeners.forEach { it.onMediaDataRemoved(key, userInitiated) } 125 } 126 } 127 } 128 129 @VisibleForTesting 130 internal fun handleProfileChanged() { 131 // TODO(b/317221348) re-add media removed when profile is available. 132 mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> 133 if (!lockscreenUserManager.isProfileAvailable(data.userId)) { 134 // Only remove media when the profile is unavailable. 135 mediaFilterRepository.removeSelectedUserMediaEntry(data.instanceId, data) 136 mediaFilterRepository.addMediaDataLoadingState( 137 MediaDataLoadingModel.Removed(data.instanceId) 138 ) 139 mediaLogger.logMediaRemoved(data.instanceId, "Removing $key after profile change") 140 listeners.forEach { listener -> listener.onMediaDataRemoved(key, false) } 141 } 142 } 143 } 144 145 @VisibleForTesting 146 internal fun handleUserSwitched() { 147 // If the user changes, remove all current MediaData objects. 148 val listenersCopy = listeners 149 val keyCopy = mediaFilterRepository.selectedUserEntries.value.keys.toMutableList() 150 // Clear the list first and update loading state to remove media from UI. 151 mediaFilterRepository.clearSelectedUserMedia() 152 keyCopy.forEach { instanceId -> 153 mediaFilterRepository.addMediaDataLoadingState( 154 MediaDataLoadingModel.Removed(instanceId) 155 ) 156 mediaLogger.logMediaRemoved(instanceId, "Removing media after user change") 157 getKey(instanceId)?.let { 158 listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it, false) } 159 } 160 } 161 162 mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> 163 if (lockscreenUserManager.isCurrentProfile(data.userId)) { 164 val isUpdate = mediaFilterRepository.addSelectedUserMediaEntry(data) 165 mediaFilterRepository.addMediaDataLoadingState( 166 MediaDataLoadingModel.Loaded(data.instanceId), 167 isUpdate, 168 ) 169 mediaLogger.logMediaLoaded( 170 data.instanceId, 171 data.active, 172 "Re-adding $key after user change", 173 ) 174 listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } 175 } 176 } 177 } 178 179 /** Invoked when the user has dismissed the media carousel */ 180 fun onSwipeToDismiss() { 181 if (DEBUG) Log.d(TAG, "Media carousel swiped away") 182 val mediaEntries = mediaFilterRepository.allUserEntries.value.entries 183 mediaEntries.forEach { (key, data) -> 184 if (mediaFilterRepository.selectedUserEntries.value.containsKey(data.instanceId)) { 185 // Force updates to listeners, needed for re-activated card 186 mediaDataProcessor.setInactive(key, timedOut = true, forceUpdate = true) 187 } 188 } 189 } 190 191 /** Add a listener for filtered [MediaData] changes */ 192 fun addListener(listener: MediaDataProcessor.Listener) = _listeners.add(listener) 193 194 /** Remove a listener that was registered with addListener */ 195 fun removeListener(listener: MediaDataProcessor.Listener) = _listeners.remove(listener) 196 197 /** 198 * Return the time since last active for the most-recent media. 199 * 200 * @param sortedEntries selectedUserEntries sorted from the earliest to the most-recent. 201 * @return The duration in milliseconds from the most-recent media's last active timestamp to 202 * the present. MAX_VALUE will be returned if there is no media. 203 */ 204 private fun timeSinceActiveForMostRecentMedia( 205 sortedEntries: SortedMap<InstanceId, MediaData> 206 ): Long { 207 if (sortedEntries.isEmpty()) { 208 return Long.MAX_VALUE 209 } 210 211 val now = systemClock.elapsedRealtime() 212 val lastActiveInstanceId = sortedEntries.lastKey() // most recently active 213 return sortedEntries[lastActiveInstanceId]?.let { now - it.lastActive } ?: Long.MAX_VALUE 214 } 215 216 private fun getKey(instanceId: InstanceId): String? { 217 val allEntries = mediaFilterRepository.allUserEntries.value 218 val filteredEntries = allEntries.filter { (_, data) -> data.instanceId == instanceId } 219 return if (filteredEntries.isNotEmpty()) { 220 filteredEntries.keys.first() 221 } else { 222 null 223 } 224 } 225 } 226