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.controls.pipeline 18 19 import android.content.Context 20 import android.os.SystemProperties 21 import android.util.Log 22 import com.android.internal.annotations.VisibleForTesting 23 import com.android.systemui.broadcast.BroadcastSender 24 import com.android.systemui.dagger.qualifiers.Main 25 import com.android.systemui.media.controls.models.player.MediaData 26 import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_RESUME 27 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData 28 import com.android.systemui.media.controls.util.MediaFlags 29 import com.android.systemui.media.controls.util.MediaUiEventLogger 30 import com.android.systemui.settings.UserTracker 31 import com.android.systemui.statusbar.NotificationLockscreenUserManager 32 import com.android.systemui.util.time.SystemClock 33 import java.util.SortedMap 34 import java.util.concurrent.Executor 35 import java.util.concurrent.TimeUnit 36 import javax.inject.Inject 37 import kotlin.collections.LinkedHashMap 38 39 private const val TAG = "MediaDataFilter" 40 private const val DEBUG = true 41 private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = 42 ("com.google" + 43 ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity") 44 private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds" 45 46 /** 47 * Maximum age of a media control to re-activate on smartspace signal. If there is no media control 48 * available within this time window, smartspace recommendations will be shown instead. 49 */ 50 @VisibleForTesting 51 internal val SMARTSPACE_MAX_AGE = 52 SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30)) 53 54 /** 55 * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user 56 * switches (removing entries for the previous user, adding back entries for the current user). Also 57 * filters out smartspace updates in favor of local recent media, when avaialble. 58 * 59 * This is added at the end of the pipeline since we may still need to handle callbacks from 60 * background users (e.g. timeouts). 61 */ 62 class MediaDataFilter 63 @Inject 64 constructor( 65 private val context: Context, 66 private val userTracker: UserTracker, 67 private val broadcastSender: BroadcastSender, 68 private val lockscreenUserManager: NotificationLockscreenUserManager, 69 @Main private val executor: Executor, 70 private val systemClock: SystemClock, 71 private val logger: MediaUiEventLogger, 72 private val mediaFlags: MediaFlags, 73 ) : MediaDataManager.Listener { 74 private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() 75 internal val listeners: Set<MediaDataManager.Listener> 76 get() = _listeners.toSet() 77 internal lateinit var mediaDataManager: MediaDataManager 78 79 private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() 80 // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager 81 private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() 82 private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA 83 private var reactivatedKey: String? = null 84 85 private val userTrackerCallback = 86 object : UserTracker.Callback { 87 override fun onUserChanged(newUser: Int, userContext: Context) { 88 handleUserSwitched(newUser) 89 } 90 } 91 92 init { 93 userTracker.addCallback(userTrackerCallback, executor) 94 } 95 96 override fun onMediaDataLoaded( 97 key: String, 98 oldKey: String?, 99 data: MediaData, 100 immediately: Boolean, 101 receivedSmartspaceCardLatency: Int, 102 isSsReactivated: Boolean 103 ) { 104 if (oldKey != null && oldKey != key) { 105 allEntries.remove(oldKey) 106 } 107 allEntries.put(key, data) 108 109 if (!lockscreenUserManager.isCurrentProfile(data.userId)) { 110 return 111 } 112 113 if (oldKey != null && oldKey != key) { 114 userEntries.remove(oldKey) 115 } 116 userEntries.put(key, data) 117 118 // Notify listeners 119 listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } 120 } 121 122 override fun onSmartspaceMediaDataLoaded( 123 key: String, 124 data: SmartspaceMediaData, 125 shouldPrioritize: Boolean 126 ) { 127 // With persistent recommendation card, we could get a background update while inactive 128 // Otherwise, consider it an invalid update 129 if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) { 130 Log.d(TAG, "Inactive recommendation data. Skip triggering.") 131 return 132 } 133 134 // Override the pass-in value here, as the order of Smartspace card is only determined here. 135 var shouldPrioritizeMutable = false 136 smartspaceMediaData = data 137 138 // Before forwarding the smartspace target, first check if we have recently inactive media 139 val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 }) 140 val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted) 141 var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE 142 data.cardAction?.extras?.let { 143 val smartspaceMaxAgeSeconds = it.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0) 144 if (smartspaceMaxAgeSeconds > 0) { 145 smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds) 146 } 147 } 148 149 // Check if smartspace has explicitly specified whether to re-activate resumable media. 150 // The default behavior is to trigger if the smartspace data is active. 151 val shouldTriggerResume = 152 if (data.cardAction?.extras?.containsKey(EXTRA_KEY_TRIGGER_RESUME) == true) { 153 data.cardAction.extras.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) 154 } else { 155 true 156 } 157 val shouldReactivate = 158 shouldTriggerResume && !hasActiveMedia() && hasAnyMedia() && data.isActive 159 160 if (timeSinceActive < smartspaceMaxAgeMillis) { 161 // It could happen there are existing active media resume cards, then we don't need to 162 // reactivate. 163 if (shouldReactivate) { 164 val lastActiveKey = sorted.lastKey() // most recently active 165 // Notify listeners to consider this media active 166 Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") 167 reactivatedKey = lastActiveKey 168 val mediaData = sorted.get(lastActiveKey)!!.copy(active = true) 169 logger.logRecommendationActivated( 170 mediaData.appUid, 171 mediaData.packageName, 172 mediaData.instanceId 173 ) 174 listeners.forEach { 175 it.onMediaDataLoaded( 176 lastActiveKey, 177 lastActiveKey, 178 mediaData, 179 receivedSmartspaceCardLatency = 180 (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) 181 .toInt(), 182 isSsReactivated = true 183 ) 184 } 185 } 186 } else if (data.isActive) { 187 // Mark to prioritize Smartspace card if no recent media. 188 shouldPrioritizeMutable = true 189 } 190 191 if (!data.isValid()) { 192 Log.d(TAG, "Invalid recommendation data. Skip showing the rec card") 193 return 194 } 195 logger.logRecommendationAdded( 196 smartspaceMediaData.packageName, 197 smartspaceMediaData.instanceId 198 ) 199 listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } 200 } 201 202 override fun onMediaDataRemoved(key: String) { 203 allEntries.remove(key) 204 userEntries.remove(key)?.let { 205 // Only notify listeners if something actually changed 206 listeners.forEach { it.onMediaDataRemoved(key) } 207 } 208 } 209 210 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { 211 // First check if we had reactivated media instead of forwarding smartspace 212 reactivatedKey?.let { 213 val lastActiveKey = it 214 reactivatedKey = null 215 Log.d(TAG, "expiring reactivated key $lastActiveKey") 216 // Notify listeners to update with actual active value 217 userEntries.get(lastActiveKey)?.let { mediaData -> 218 listeners.forEach { 219 it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately) 220 } 221 } 222 } 223 224 if (smartspaceMediaData.isActive) { 225 smartspaceMediaData = 226 EMPTY_SMARTSPACE_MEDIA_DATA.copy( 227 targetId = smartspaceMediaData.targetId, 228 instanceId = smartspaceMediaData.instanceId 229 ) 230 } 231 listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } 232 } 233 234 @VisibleForTesting 235 internal fun handleUserSwitched(id: Int) { 236 // If the user changes, remove all current MediaData objects and inform listeners 237 val listenersCopy = listeners 238 val keyCopy = userEntries.keys.toMutableList() 239 // Clear the list first, to make sure callbacks from listeners if we have any entries 240 // are up to date 241 userEntries.clear() 242 keyCopy.forEach { 243 if (DEBUG) Log.d(TAG, "Removing $it after user change") 244 listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) } 245 } 246 247 allEntries.forEach { (key, data) -> 248 if (lockscreenUserManager.isCurrentProfile(data.userId)) { 249 if (DEBUG) Log.d(TAG, "Re-adding $key after user change") 250 userEntries.put(key, data) 251 listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } 252 } 253 } 254 } 255 256 /** Invoked when the user has dismissed the media carousel */ 257 fun onSwipeToDismiss() { 258 if (DEBUG) Log.d(TAG, "Media carousel swiped away") 259 val mediaKeys = userEntries.keys.toSet() 260 mediaKeys.forEach { 261 // Force updates to listeners, needed for re-activated card 262 mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true) 263 } 264 if (smartspaceMediaData.isActive) { 265 val dismissIntent = smartspaceMediaData.dismissIntent 266 if (dismissIntent == null) { 267 Log.w( 268 TAG, 269 "Cannot create dismiss action click action: extras missing dismiss_intent." 270 ) 271 } else if ( 272 dismissIntent.getComponent() != null && 273 dismissIntent.getComponent().getClassName() == 274 EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME 275 ) { 276 // Dismiss the card Smartspace data through Smartspace trampoline activity. 277 context.startActivity(dismissIntent) 278 } else { 279 broadcastSender.sendBroadcast(dismissIntent) 280 } 281 282 if (mediaFlags.isPersistentSsCardEnabled()) { 283 smartspaceMediaData = smartspaceMediaData.copy(isActive = false) 284 mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId) 285 } else { 286 smartspaceMediaData = 287 EMPTY_SMARTSPACE_MEDIA_DATA.copy( 288 targetId = smartspaceMediaData.targetId, 289 instanceId = smartspaceMediaData.instanceId, 290 ) 291 mediaDataManager.dismissSmartspaceRecommendation( 292 smartspaceMediaData.targetId, 293 delay = 0L, 294 ) 295 } 296 } 297 } 298 299 /** Are there any active media entries, including the recommendation? */ 300 fun hasActiveMediaOrRecommendation() = 301 userEntries.any { it.value.active } || 302 (smartspaceMediaData.isActive && 303 (smartspaceMediaData.isValid() || reactivatedKey != null)) 304 305 /** Are there any media entries we should display? */ 306 fun hasAnyMediaOrRecommendation(): Boolean { 307 val hasSmartspace = 308 if (mediaFlags.isPersistentSsCardEnabled()) { 309 smartspaceMediaData.isValid() 310 } else { 311 smartspaceMediaData.isActive && smartspaceMediaData.isValid() 312 } 313 return userEntries.isNotEmpty() || hasSmartspace 314 } 315 316 /** Are there any media notifications active (excluding the recommendation)? */ 317 fun hasActiveMedia() = userEntries.any { it.value.active } 318 319 /** Are there any media entries we should display (excluding the recommendation)? */ 320 fun hasAnyMedia() = userEntries.isNotEmpty() 321 322 /** Add a listener for filtered [MediaData] changes */ 323 fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) 324 325 /** Remove a listener that was registered with addListener */ 326 fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) 327 328 /** 329 * Return the time since last active for the most-recent media. 330 * 331 * @param sortedEntries userEntries sorted from the earliest to the most-recent. 332 * @return The duration in milliseconds from the most-recent media's last active timestamp to 333 * the present. MAX_VALUE will be returned if there is no media. 334 */ 335 private fun timeSinceActiveForMostRecentMedia( 336 sortedEntries: SortedMap<String, MediaData> 337 ): Long { 338 if (sortedEntries.isEmpty()) { 339 return Long.MAX_VALUE 340 } 341 342 val now = systemClock.elapsedRealtime() 343 val lastActiveKey = sortedEntries.lastKey() // most recently active 344 return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE 345 } 346 } 347