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