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.os.SystemProperties
22 import android.util.Log
23 import com.android.internal.annotations.KeepForWeakReference
24 import com.android.internal.annotations.VisibleForTesting
25 import com.android.systemui.broadcast.BroadcastSender
26 import com.android.systemui.dagger.qualifiers.Main
27 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
28 import com.android.systemui.media.controls.shared.model.MediaData
29 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
30 import com.android.systemui.media.controls.util.MediaFlags
31 import com.android.systemui.media.controls.util.MediaUiEventLogger
32 import com.android.systemui.settings.UserTracker
33 import com.android.systemui.statusbar.NotificationLockscreenUserManager
34 import com.android.systemui.util.time.SystemClock
35 import java.util.SortedMap
36 import java.util.concurrent.Executor
37 import java.util.concurrent.TimeUnit
38 import javax.inject.Inject
39 import kotlin.collections.LinkedHashMap
40 
41 private const val TAG = "MediaDataFilter"
42 private const val DEBUG = true
43 private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME =
44     ("com.google" +
45         ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity")
46 private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"
47 
48 /**
49  * Maximum age of a media control to re-activate on smartspace signal. If there is no media control
50  * available within this time window, smartspace recommendations will be shown instead.
51  */
52 @VisibleForTesting
53 internal val SMARTSPACE_MAX_AGE =
54     SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
55 
56 /**
57  * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
58  * switches (removing entries for the previous user, adding back entries for the current user). Also
59  * filters out smartspace updates in favor of local recent media, when avaialble.
60  *
61  * This is added at the end of the pipeline since we may still need to handle callbacks from
62  * background users (e.g. timeouts).
63  */
64 class LegacyMediaDataFilterImpl
65 @Inject
66 constructor(
67     private val context: Context,
68     private val userTracker: UserTracker,
69     private val broadcastSender: BroadcastSender,
70     private val lockscreenUserManager: NotificationLockscreenUserManager,
71     @Main private val executor: Executor,
72     private val systemClock: SystemClock,
73     private val logger: MediaUiEventLogger,
74     private val mediaFlags: MediaFlags,
75 ) : MediaDataManager.Listener {
76     private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
77     val listeners: Set<MediaDataManager.Listener>
78         get() = _listeners.toSet()
79     lateinit var mediaDataManager: MediaDataManager
80 
81     private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
82     // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
83     private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
84     private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
85     private var reactivatedKey: String? = null
86 
87     // Ensure the field (and associated reference) isn't removed during optimization.
88     @KeepForWeakReference
89     private val userTrackerCallback =
90         object : UserTracker.Callback {
91             override fun onUserChanged(newUser: Int, userContext: Context) {
92                 handleUserSwitched()
93             }
94 
95             override fun onProfilesChanged(profiles: List<UserInfo>) {
96                 handleProfileChanged()
97             }
98         }
99 
100     init {
101         userTracker.addCallback(userTrackerCallback, executor)
102     }
103 
104     override fun onMediaDataLoaded(
105         key: String,
106         oldKey: String?,
107         data: MediaData,
108         immediately: Boolean,
109         receivedSmartspaceCardLatency: Int,
110         isSsReactivated: Boolean
111     ) {
112         if (oldKey != null && oldKey != key) {
113             allEntries.remove(oldKey)
114         }
115         allEntries.put(key, data)
116 
117         if (
118             !lockscreenUserManager.isCurrentProfile(data.userId) ||
119                 !lockscreenUserManager.isProfileAvailable(data.userId)
120         ) {
121             return
122         }
123 
124         if (oldKey != null && oldKey != key) {
125             userEntries.remove(oldKey)
126         }
127         userEntries.put(key, data)
128 
129         // Notify listeners
130         listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) }
131     }
132 
133     override fun onSmartspaceMediaDataLoaded(
134         key: String,
135         data: SmartspaceMediaData,
136         shouldPrioritize: Boolean
137     ) {
138         // With persistent recommendation card, we could get a background update while inactive
139         // Otherwise, consider it an invalid update
140         if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) {
141             Log.d(TAG, "Inactive recommendation data. Skip triggering.")
142             return
143         }
144 
145         // Override the pass-in value here, as the order of Smartspace card is only determined here.
146         var shouldPrioritizeMutable = false
147         smartspaceMediaData = data
148 
149         // Before forwarding the smartspace target, first check if we have recently inactive media
150         val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 })
151         val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
152         var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
153         data.cardAction?.extras?.let {
154             val smartspaceMaxAgeSeconds = it.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
155             if (smartspaceMaxAgeSeconds > 0) {
156                 smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
157             }
158         }
159 
160         // Check if smartspace has explicitly specified whether to re-activate resumable media.
161         // The default behavior is to trigger if the smartspace data is active.
162         val shouldTriggerResume =
163             data.cardAction?.extras?.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) ?: true
164         val shouldReactivate =
165             shouldTriggerResume && !hasActiveMedia() && hasAnyMedia() && data.isActive
166 
167         if (timeSinceActive < smartspaceMaxAgeMillis) {
168             // It could happen there are existing active media resume cards, then we don't need to
169             // reactivate.
170             if (shouldReactivate) {
171                 val lastActiveKey = sorted.lastKey() // most recently active
172                 // Notify listeners to consider this media active
173                 Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
174                 reactivatedKey = lastActiveKey
175                 val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
176                 logger.logRecommendationActivated(
177                     mediaData.appUid,
178                     mediaData.packageName,
179                     mediaData.instanceId
180                 )
181                 listeners.forEach {
182                     it.onMediaDataLoaded(
183                         lastActiveKey,
184                         lastActiveKey,
185                         mediaData,
186                         receivedSmartspaceCardLatency =
187                             (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
188                                 .toInt(),
189                         isSsReactivated = true
190                     )
191                 }
192             }
193         } else if (data.isActive) {
194             // Mark to prioritize Smartspace card if no recent media.
195             shouldPrioritizeMutable = true
196         }
197 
198         if (!data.isValid()) {
199             Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
200             return
201         }
202         logger.logRecommendationAdded(
203             smartspaceMediaData.packageName,
204             smartspaceMediaData.instanceId
205         )
206         listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
207     }
208 
209     override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
210         allEntries.remove(key)
211         userEntries.remove(key)?.let {
212             // Only notify listeners if something actually changed
213             listeners.forEach { it.onMediaDataRemoved(key, userInitiated) }
214         }
215     }
216 
217     override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
218         // First check if we had reactivated media instead of forwarding smartspace
219         reactivatedKey?.let {
220             val lastActiveKey = it
221             reactivatedKey = null
222             Log.d(TAG, "expiring reactivated key $lastActiveKey")
223             // Notify listeners to update with actual active value
224             userEntries.get(lastActiveKey)?.let { mediaData ->
225                 listeners.forEach {
226                     it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
227                 }
228             }
229         }
230 
231         if (smartspaceMediaData.isActive) {
232             smartspaceMediaData =
233                 EMPTY_SMARTSPACE_MEDIA_DATA.copy(
234                     targetId = smartspaceMediaData.targetId,
235                     instanceId = smartspaceMediaData.instanceId
236                 )
237         }
238         listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
239     }
240 
241     @VisibleForTesting
242     internal fun handleProfileChanged() {
243         // TODO(b/317221348) re-add media removed when profile is available.
244         allEntries.forEach { (key, data) ->
245             if (!lockscreenUserManager.isProfileAvailable(data.userId)) {
246                 // Only remove media when the profile is unavailable.
247                 if (DEBUG) Log.d(TAG, "Removing $key after profile change")
248                 userEntries.remove(key, data)
249                 listeners.forEach { listener -> listener.onMediaDataRemoved(key, false) }
250             }
251         }
252     }
253 
254     @VisibleForTesting
255     internal fun handleUserSwitched() {
256         // If the user changes, remove all current MediaData objects and inform listeners
257         val listenersCopy = listeners
258         val keyCopy = userEntries.keys.toMutableList()
259         // Clear the list first, to make sure callbacks from listeners if we have any entries
260         // are up to date
261         userEntries.clear()
262         keyCopy.forEach {
263             if (DEBUG) Log.d(TAG, "Removing $it after user change")
264             listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it, false) }
265         }
266 
267         allEntries.forEach { (key, data) ->
268             if (lockscreenUserManager.isCurrentProfile(data.userId)) {
269                 if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
270                 userEntries.put(key, data)
271                 listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) }
272             }
273         }
274     }
275 
276     /** Invoked when the user has dismissed the media carousel */
277     fun onSwipeToDismiss() {
278         if (DEBUG) Log.d(TAG, "Media carousel swiped away")
279         val mediaKeys = userEntries.keys.toSet()
280         mediaKeys.forEach {
281             // Force updates to listeners, needed for re-activated card
282             mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true)
283         }
284         if (smartspaceMediaData.isActive) {
285             val dismissIntent = smartspaceMediaData.dismissIntent
286             if (dismissIntent == null) {
287                 Log.w(
288                     TAG,
289                     "Cannot create dismiss action click action: extras missing dismiss_intent."
290                 )
291             } else if (
292                 dismissIntent.component?.className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
293             ) {
294                 // Dismiss the card Smartspace data through Smartspace trampoline activity.
295                 context.startActivity(dismissIntent)
296             } else {
297                 broadcastSender.sendBroadcast(dismissIntent)
298             }
299 
300             if (mediaFlags.isPersistentSsCardEnabled()) {
301                 smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
302                 mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId)
303             } else {
304                 smartspaceMediaData =
305                     EMPTY_SMARTSPACE_MEDIA_DATA.copy(
306                         targetId = smartspaceMediaData.targetId,
307                         instanceId = smartspaceMediaData.instanceId,
308                     )
309                 mediaDataManager.dismissSmartspaceRecommendation(
310                     smartspaceMediaData.targetId,
311                     delay = 0L,
312                 )
313             }
314         }
315     }
316 
317     /** Are there any active media entries, including the recommendation? */
318     fun hasActiveMediaOrRecommendation() =
319         userEntries.any { it.value.active } ||
320             (smartspaceMediaData.isActive &&
321                 (smartspaceMediaData.isValid() || reactivatedKey != null))
322 
323     /** Are there any media entries we should display? */
324     fun hasAnyMediaOrRecommendation(): Boolean {
325         val hasSmartspace =
326             if (mediaFlags.isPersistentSsCardEnabled()) {
327                 smartspaceMediaData.isValid()
328             } else {
329                 smartspaceMediaData.isActive && smartspaceMediaData.isValid()
330             }
331         return userEntries.isNotEmpty() || hasSmartspace
332     }
333 
334     /** Are there any media notifications active (excluding the recommendation)? */
335     fun hasActiveMedia() = userEntries.any { it.value.active }
336 
337     /** Are there any media entries we should display (excluding the recommendation)? */
338     fun hasAnyMedia() = userEntries.isNotEmpty()
339 
340     /** Add a listener for filtered [MediaData] changes */
341     fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener)
342 
343     /** Remove a listener that was registered with addListener */
344     fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener)
345 
346     /**
347      * Return the time since last active for the most-recent media.
348      *
349      * @param sortedEntries userEntries sorted from the earliest to the most-recent.
350      * @return The duration in milliseconds from the most-recent media's last active timestamp to
351      *   the present. MAX_VALUE will be returned if there is no media.
352      */
353     private fun timeSinceActiveForMostRecentMedia(
354         sortedEntries: SortedMap<String, MediaData>
355     ): Long {
356         if (sortedEntries.isEmpty()) {
357             return Long.MAX_VALUE
358         }
359 
360         val now = systemClock.elapsedRealtime()
361         val lastActiveKey = sortedEntries.lastKey() // most recently active
362         return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE
363     }
364 }
365