• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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