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