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