1 /*
<lambda>null2  * Copyright (C) 2021 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.statusbar.notification.collection.coordinator
18 
19 import android.app.smartspace.SmartspaceTarget
20 import android.os.Parcelable
21 import com.android.systemui.dagger.qualifiers.Main
22 import com.android.systemui.plugins.statusbar.StatusBarStateController
23 import com.android.systemui.statusbar.StatusBarState
24 import com.android.systemui.statusbar.SysuiStatusBarStateController
25 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
26 import com.android.systemui.statusbar.notification.collection.NotifPipeline
27 import com.android.systemui.statusbar.notification.collection.NotificationEntry
28 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
29 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
30 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
31 import com.android.systemui.statusbar.notification.logKey
32 import com.android.systemui.util.concurrency.DelayableExecutor
33 import com.android.systemui.util.time.SystemClock
34 import java.util.concurrent.TimeUnit.SECONDS
35 import javax.inject.Inject
36 
37 /**
38  * Hides notifications on the lockscreen if the content of those notifications is also visible
39  * in smartspace. This ONLY hides the notifications on the lockscreen: if the user pulls the shade
40  * down or unlocks the device, then the notifications are unhidden.
41  *
42  * In addition, notifications that have recently alerted aren't filtered. Tracking this in a way
43  * that involves the fewest pipeline invalidations requires some unfortunately complex logic.
44  */
45 @CoordinatorScope
46 class SmartspaceDedupingCoordinator @Inject constructor(
47     private val statusBarStateController: SysuiStatusBarStateController,
48     private val smartspaceController: LockscreenSmartspaceController,
49     private val notifPipeline: NotifPipeline,
50     @Main private val executor: DelayableExecutor,
51     private val clock: SystemClock
52 ) : Coordinator {
53     private var isOnLockscreen = false
54 
55     private var trackedSmartspaceTargets = mutableMapOf<String, TrackedSmartspaceTarget>()
56 
57     override fun attach(pipeline: NotifPipeline) {
58         pipeline.addPreGroupFilter(filter)
59         pipeline.addCollectionListener(collectionListener)
60         statusBarStateController.addCallback(statusBarStateListener)
61         smartspaceController.addListener(this::onNewSmartspaceTargets)
62 
63         recordStatusBarState(statusBarStateController.state)
64     }
65 
66     private fun isDupedWithSmartspaceContent(entry: NotificationEntry): Boolean {
67         return trackedSmartspaceTargets[entry.key]?.shouldFilter ?: false
68     }
69 
70     private val filter = object : NotifFilter("SmartspaceDedupingFilter") {
71         override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
72             return isOnLockscreen && isDupedWithSmartspaceContent(entry)
73         }
74     }
75 
76     private val collectionListener = object : NotifCollectionListener {
77         override fun onEntryAdded(entry: NotificationEntry) {
78             trackedSmartspaceTargets[entry.key]?.let {
79                 updateFilterStatus(it)
80             }
81         }
82 
83         override fun onEntryUpdated(entry: NotificationEntry) {
84             trackedSmartspaceTargets[entry.key]?.let {
85                 updateFilterStatus(it)
86             }
87         }
88 
89         override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
90             trackedSmartspaceTargets[entry.key]?.let { trackedTarget ->
91                 cancelExceptionTimeout(trackedTarget)
92             }
93         }
94     }
95 
96     private val statusBarStateListener = object : StatusBarStateController.StateListener {
97         override fun onStateChanged(newState: Int) {
98             recordStatusBarState(newState)
99         }
100     }
101 
102     private fun onNewSmartspaceTargets(targets: List<Parcelable>) {
103         var changed = false
104         val newMap = mutableMapOf<String, TrackedSmartspaceTarget>()
105         val oldMap = trackedSmartspaceTargets
106 
107         for (target in targets) {
108             // For all targets that are SmartspaceTargets and have non-null sourceNotificationKeys
109             (target as? SmartspaceTarget)?.sourceNotificationKey?.let { key ->
110                 val trackedTarget = oldMap.getOrElse(key) {
111                     TrackedSmartspaceTarget(key)
112                 }
113                 newMap[key] = trackedTarget
114                 changed = changed || updateFilterStatus(trackedTarget)
115             }
116             // Currently, only filter out the first target
117             break
118         }
119 
120         for (prevKey in oldMap.keys) {
121             if (!newMap.containsKey(prevKey)) {
122                 oldMap[prevKey]?.cancelTimeoutRunnable?.run()
123                 changed = true
124             }
125         }
126 
127         if (changed) {
128             filter.invalidateList("onNewSmartspaceTargets")
129         }
130 
131         trackedSmartspaceTargets = newMap
132     }
133 
134     /**
135      * Returns true if the target's alert exception status has changed
136      */
137     private fun updateFilterStatus(target: TrackedSmartspaceTarget): Boolean {
138         val prevShouldFilter = target.shouldFilter
139 
140         val entry = notifPipeline.getEntry(target.key)
141         if (entry != null) {
142             updateAlertException(target, entry)
143 
144             target.shouldFilter = !hasRecentlyAlerted(entry)
145         }
146 
147         return target.shouldFilter != prevShouldFilter && isOnLockscreen
148     }
149 
150     private fun updateAlertException(target: TrackedSmartspaceTarget, entry: NotificationEntry) {
151         val now = clock.currentTimeMillis()
152         val alertExceptionExpires = entry.ranking.lastAudiblyAlertedMillis + ALERT_WINDOW
153 
154         if (alertExceptionExpires != target.alertExceptionExpires &&
155                 alertExceptionExpires > now) {
156             // If we got here, the target is subject to a new alert exception window, so we
157             // should update our timeout to fire at the end of the new window
158 
159             target.cancelTimeoutRunnable?.run()
160             target.alertExceptionExpires = alertExceptionExpires
161             target.cancelTimeoutRunnable = executor.executeDelayed({
162                 target.cancelTimeoutRunnable = null
163                 target.shouldFilter = true
164                 filter.invalidateList("updateAlertException: ${entry.logKey}")
165             }, alertExceptionExpires - now)
166         }
167     }
168 
169     private fun cancelExceptionTimeout(target: TrackedSmartspaceTarget) {
170         target.cancelTimeoutRunnable?.run()
171         target.cancelTimeoutRunnable = null
172         target.alertExceptionExpires = 0
173     }
174 
175     private fun recordStatusBarState(newState: Int) {
176         val wasOnLockscreen = isOnLockscreen
177         isOnLockscreen = newState == StatusBarState.KEYGUARD
178 
179         if (isOnLockscreen != wasOnLockscreen) {
180             filter.invalidateList("recordStatusBarState: " + StatusBarState.toString(newState))
181             // No need to call notificationEntryManager.updateNotifications; something else already
182             // does it for us when the keyguard state changes
183         }
184     }
185 
186     private fun hasRecentlyAlerted(entry: NotificationEntry): Boolean {
187         return clock.currentTimeMillis() - entry.ranking.lastAudiblyAlertedMillis <= ALERT_WINDOW
188     }
189 }
190 
191 private class TrackedSmartspaceTarget(
192     val key: String
193 ) {
194     var cancelTimeoutRunnable: Runnable? = null
195     var alertExceptionExpires: Long = 0
196     var shouldFilter = false
197 }
198 
199 private val ALERT_WINDOW = SECONDS.toMillis(30)
200