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.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.systemui.dagger.qualifiers.Main 25 import com.android.systemui.media.controls.shared.model.MediaData 26 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData 27 import com.android.systemui.settings.UserTracker 28 import com.android.systemui.statusbar.NotificationLockscreenUserManager 29 import com.android.systemui.util.time.SystemClock 30 import java.util.SortedMap 31 import java.util.concurrent.Executor 32 import javax.inject.Inject 33 import kotlin.collections.LinkedHashMap 34 35 private const val TAG = "MediaDataFilter" 36 private const val DEBUG = true 37 38 /** 39 * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user 40 * switches (removing entries for the previous user, adding back entries for the current user). Also 41 * filters out smartspace updates in favor of local recent media, when avaialble. 42 * 43 * This is added at the end of the pipeline since we may still need to handle callbacks from 44 * background users (e.g. timeouts). 45 */ 46 class LegacyMediaDataFilterImpl 47 @Inject 48 constructor( 49 private val userTracker: UserTracker, 50 private val lockscreenUserManager: NotificationLockscreenUserManager, 51 @Main private val executor: Executor, 52 private val systemClock: SystemClock, 53 ) : MediaDataManager.Listener { 54 private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() 55 val listeners: Set<MediaDataManager.Listener> 56 get() = _listeners.toSet() 57 58 lateinit var mediaDataManager: MediaDataManager 59 60 private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() 61 // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager 62 private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() 63 64 // Ensure the field (and associated reference) isn't removed during optimization. 65 @KeepForWeakReference 66 private val userTrackerCallback = 67 object : UserTracker.Callback { 68 override fun onUserChanged(newUser: Int, userContext: Context) { 69 handleUserSwitched() 70 } 71 72 override fun onProfilesChanged(profiles: List<UserInfo>) { 73 handleProfileChanged() 74 } 75 } 76 77 init { 78 userTracker.addCallback(userTrackerCallback, executor) 79 } 80 81 override fun onMediaDataLoaded( 82 key: String, 83 oldKey: String?, 84 data: MediaData, 85 immediately: Boolean, 86 receivedSmartspaceCardLatency: Int, 87 isSsReactivated: Boolean, 88 ) { 89 if (oldKey != null && oldKey != key) { 90 allEntries.remove(oldKey) 91 } 92 allEntries.put(key, data) 93 94 if ( 95 !lockscreenUserManager.isCurrentProfile(data.userId) || 96 !lockscreenUserManager.isProfileAvailable(data.userId) 97 ) { 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 { it.onMediaDataLoaded(key, oldKey, data) } 108 } 109 110 override fun onSmartspaceMediaDataLoaded( 111 key: String, 112 data: SmartspaceMediaData, 113 shouldPrioritize: Boolean, 114 ) { 115 // TODO(b/382680767): remove 116 } 117 118 override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { 119 allEntries.remove(key) 120 userEntries.remove(key)?.let { 121 // Only notify listeners if something actually changed 122 listeners.forEach { it.onMediaDataRemoved(key, userInitiated) } 123 } 124 } 125 126 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { 127 // TODO(b/382680767): remove 128 } 129 130 @VisibleForTesting 131 internal fun handleProfileChanged() { 132 // TODO(b/317221348) re-add media removed when profile is available. 133 allEntries.forEach { (key, data) -> 134 if (!lockscreenUserManager.isProfileAvailable(data.userId)) { 135 // Only remove media when the profile is unavailable. 136 if (DEBUG) Log.d(TAG, "Removing $key after profile change") 137 userEntries.remove(key, data) 138 listeners.forEach { listener -> listener.onMediaDataRemoved(key, false) } 139 } 140 } 141 } 142 143 @VisibleForTesting 144 internal fun handleUserSwitched() { 145 // If the user changes, remove all current MediaData objects and inform listeners 146 val listenersCopy = listeners 147 val keyCopy = userEntries.keys.toMutableList() 148 // Clear the list first, to make sure callbacks from listeners if we have any entries 149 // are up to date 150 userEntries.clear() 151 keyCopy.forEach { 152 if (DEBUG) Log.d(TAG, "Removing $it after user change") 153 listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it, false) } 154 } 155 156 allEntries.forEach { (key, data) -> 157 if (lockscreenUserManager.isCurrentProfile(data.userId)) { 158 if (DEBUG) Log.d(TAG, "Re-adding $key after user change") 159 userEntries.put(key, data) 160 listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } 161 } 162 } 163 } 164 165 /** Invoked when the user has dismissed the media carousel */ 166 fun onSwipeToDismiss() { 167 if (DEBUG) Log.d(TAG, "Media carousel swiped away") 168 val mediaKeys = userEntries.keys.toSet() 169 mediaKeys.forEach { 170 // Force updates to listeners, needed for re-activated card 171 mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true) 172 } 173 } 174 175 /** Are there any media notifications active? */ 176 fun hasActiveMedia() = userEntries.any { it.value.active } 177 178 /** Are there any media entries we should display? */ 179 fun hasAnyMedia() = userEntries.isNotEmpty() 180 181 /** Add a listener for filtered [MediaData] changes */ 182 fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) 183 184 /** Remove a listener that was registered with addListener */ 185 fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) 186 187 /** 188 * Return the time since last active for the most-recent media. 189 * 190 * @param sortedEntries userEntries sorted from the earliest to the most-recent. 191 * @return The duration in milliseconds from the most-recent media's last active timestamp to 192 * the present. MAX_VALUE will be returned if there is no media. 193 */ 194 private fun timeSinceActiveForMostRecentMedia( 195 sortedEntries: SortedMap<String, MediaData> 196 ): Long { 197 if (sortedEntries.isEmpty()) { 198 return Long.MAX_VALUE 199 } 200 201 val now = systemClock.elapsedRealtime() 202 val lastActiveKey = sortedEntries.lastKey() // most recently active 203 return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE 204 } 205 } 206