1 /* <lambda>null2 * Copyright (C) 2020 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 18 19 import android.os.SystemProperties 20 import android.util.Log 21 import com.android.internal.annotations.VisibleForTesting 22 import com.android.systemui.broadcast.BroadcastDispatcher 23 import com.android.systemui.dagger.qualifiers.Main 24 import com.android.systemui.settings.CurrentUserTracker 25 import com.android.systemui.statusbar.NotificationLockscreenUserManager 26 import com.android.systemui.util.time.SystemClock 27 import java.util.SortedMap 28 import java.util.concurrent.Executor 29 import java.util.concurrent.TimeUnit 30 import javax.inject.Inject 31 import kotlin.collections.LinkedHashMap 32 33 private const val TAG = "MediaDataFilter" 34 private const val DEBUG = true 35 private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds" 36 37 /** 38 * Maximum age of a media control to re-activate on smartspace signal. If there is no media control 39 * available within this time window, smartspace recommendations will be shown instead. 40 */ 41 @VisibleForTesting 42 internal val SMARTSPACE_MAX_AGE = SystemProperties 43 .getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30)) 44 45 /** 46 * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user 47 * switches (removing entries for the previous user, adding back entries for the current user). Also 48 * filters out smartspace updates in favor of local recent media, when avaialble. 49 * 50 * This is added at the end of the pipeline since we may still need to handle callbacks from 51 * background users (e.g. timeouts). 52 */ 53 class MediaDataFilter @Inject constructor( 54 private val broadcastDispatcher: BroadcastDispatcher, 55 private val mediaResumeListener: MediaResumeListener, 56 private val lockscreenUserManager: NotificationLockscreenUserManager, 57 @Main private val executor: Executor, 58 private val systemClock: SystemClock 59 ) : MediaDataManager.Listener { 60 private val userTracker: CurrentUserTracker 61 private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() 62 internal val listeners: Set<MediaDataManager.Listener> 63 get() = _listeners.toSet() 64 internal lateinit var mediaDataManager: MediaDataManager 65 66 private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() 67 // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager 68 private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() 69 private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA 70 private var reactivatedKey: String? = null 71 72 init { 73 userTracker = object : CurrentUserTracker(broadcastDispatcher) { 74 override fun onUserSwitched(newUserId: Int) { 75 // Post this so we can be sure lockscreenUserManager already got the broadcast 76 executor.execute { handleUserSwitched(newUserId) } 77 } 78 } 79 userTracker.startTracking() 80 } 81 82 override fun onMediaDataLoaded( 83 key: String, 84 oldKey: String?, 85 data: MediaData, 86 immediately: Boolean, 87 isSsReactivated: Boolean 88 ) { 89 if (oldKey != null && oldKey != key) { 90 allEntries.remove(oldKey) 91 } 92 allEntries.put(key, data) 93 94 if (!lockscreenUserManager.isCurrentProfile(data.userId)) { 95 return 96 } 97 98 if (oldKey != null && oldKey != key) { 99 userEntries.remove(oldKey) 100 } 101 userEntries.put(key, data) 102 103 // Notify listeners 104 listeners.forEach { 105 it.onMediaDataLoaded(key, oldKey, data, isSsReactivated = isSsReactivated) 106 } 107 } 108 109 override fun onSmartspaceMediaDataLoaded( 110 key: String, 111 data: SmartspaceMediaData, 112 shouldPrioritize: Boolean 113 ) { 114 if (!data.isActive) { 115 Log.d(TAG, "Inactive recommendation data. Skip triggering.") 116 return 117 } 118 119 // Override the pass-in value here, as the order of Smartspace card is only determined here. 120 var shouldPrioritizeMutable = false 121 smartspaceMediaData = data 122 // Override the pass-in value here, as the Smartspace reactivation could only happen here. 123 var isSsReactivated = false 124 125 // Before forwarding the smartspace target, first check if we have recently inactive media 126 val sorted = userEntries.toSortedMap(compareBy { 127 userEntries.get(it)?.lastActive ?: -1 128 }) 129 val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted) 130 var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE 131 data.cardAction?.let { 132 val smartspaceMaxAgeSeconds = 133 it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0) 134 if (smartspaceMaxAgeSeconds > 0) { 135 smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds) 136 } 137 } 138 if (timeSinceActive < smartspaceMaxAgeMillis) { 139 val lastActiveKey = sorted.lastKey() // most recently active 140 // Notify listeners to consider this media active 141 Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") 142 reactivatedKey = lastActiveKey 143 if (MediaPlayerData.firstActiveMediaIndex() == -1) { 144 isSsReactivated = true 145 } 146 val mediaData = sorted.get(lastActiveKey)!!.copy(active = true) 147 listeners.forEach { 148 it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, 149 isSsReactivated = isSsReactivated) 150 } 151 } else { 152 // Mark to prioritize Smartspace card if no recent media. 153 shouldPrioritizeMutable = true 154 } 155 156 if (!data.isValid) { 157 Log.d(TAG, "Invalid recommendation data. Skip showing the rec card") 158 return 159 } 160 listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } 161 } 162 163 override fun onMediaDataRemoved(key: String) { 164 allEntries.remove(key) 165 userEntries.remove(key)?.let { 166 // Only notify listeners if something actually changed 167 listeners.forEach { 168 it.onMediaDataRemoved(key) 169 } 170 } 171 } 172 173 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { 174 // First check if we had reactivated media instead of forwarding smartspace 175 reactivatedKey?.let { 176 val lastActiveKey = it 177 reactivatedKey = null 178 Log.d(TAG, "expiring reactivated key $lastActiveKey") 179 // Notify listeners to update with actual active value 180 userEntries.get(lastActiveKey)?.let { mediaData -> 181 listeners.forEach { 182 it.onMediaDataLoaded( 183 lastActiveKey, lastActiveKey, mediaData, immediately) 184 } 185 } 186 } 187 188 if (smartspaceMediaData.isActive) { 189 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( 190 targetId = smartspaceMediaData.targetId, isValid = smartspaceMediaData.isValid) 191 } 192 listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } 193 } 194 195 @VisibleForTesting 196 internal fun handleUserSwitched(id: Int) { 197 // If the user changes, remove all current MediaData objects and inform listeners 198 val listenersCopy = listeners 199 val keyCopy = userEntries.keys.toMutableList() 200 // Clear the list first, to make sure callbacks from listeners if we have any entries 201 // are up to date 202 userEntries.clear() 203 keyCopy.forEach { 204 if (DEBUG) Log.d(TAG, "Removing $it after user change") 205 listenersCopy.forEach { listener -> 206 listener.onMediaDataRemoved(it) 207 } 208 } 209 210 allEntries.forEach { (key, data) -> 211 if (lockscreenUserManager.isCurrentProfile(data.userId)) { 212 if (DEBUG) Log.d(TAG, "Re-adding $key after user change") 213 userEntries.put(key, data) 214 listenersCopy.forEach { listener -> 215 listener.onMediaDataLoaded(key, null, data) 216 } 217 } 218 } 219 } 220 221 /** 222 * Invoked when the user has dismissed the media carousel 223 */ 224 fun onSwipeToDismiss() { 225 if (DEBUG) Log.d(TAG, "Media carousel swiped away") 226 val mediaKeys = userEntries.keys.toSet() 227 mediaKeys.forEach { 228 // Force updates to listeners, needed for re-activated card 229 mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true) 230 } 231 if (smartspaceMediaData.isActive) { 232 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( 233 targetId = smartspaceMediaData.targetId, isValid = smartspaceMediaData.isValid) 234 } 235 mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId, delay = 0L) 236 } 237 238 /** 239 * Are there any media notifications active? 240 */ 241 fun hasActiveMedia() = userEntries.any { it.value.active } || smartspaceMediaData.isActive 242 243 /** 244 * Are there any media entries we should display? 245 */ 246 fun hasAnyMedia() = userEntries.isNotEmpty() || smartspaceMediaData.isActive 247 248 /** 249 * Add a listener for filtered [MediaData] changes 250 */ 251 fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) 252 253 /** 254 * Remove a listener that was registered with addListener 255 */ 256 fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) 257 258 /** 259 * Return the time since last active for the most-recent media. 260 * 261 * @param sortedEntries userEntries sorted from the earliest to the most-recent. 262 * 263 * @return The duration in milliseconds from the most-recent media's last active timestamp to 264 * the present. MAX_VALUE will be returned if there is no media. 265 */ 266 private fun timeSinceActiveForMostRecentMedia( 267 sortedEntries: SortedMap<String, MediaData> 268 ): Long { 269 if (sortedEntries.isEmpty()) { 270 return Long.MAX_VALUE 271 } 272 273 val now = systemClock.elapsedRealtime() 274 val lastActiveKey = sortedEntries.lastKey() // most recently active 275 return sortedEntries.get(lastActiveKey)?.let { 276 now - it.lastActive 277 } ?: Long.MAX_VALUE 278 } 279 } 280