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