• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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.content.ComponentName
21 import android.os.UserHandle
22 import androidx.test.ext.junit.runners.AndroidJUnit4
23 import androidx.test.filters.SmallTest
24 import com.android.systemui.SysuiTestCase
25 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
26 import com.android.systemui.plugins.statusbar.StatusBarStateController
27 import com.android.systemui.statusbar.StatusBarState
28 import com.android.systemui.statusbar.SysuiStatusBarStateController
29 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
30 import com.android.systemui.statusbar.notification.collection.NotifPipeline
31 import com.android.systemui.statusbar.notification.collection.NotificationEntry
32 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
33 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
34 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
35 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
36 import com.android.systemui.util.concurrency.FakeExecutor
37 import com.android.systemui.util.mockito.any
38 import com.android.systemui.util.mockito.eq
39 import com.android.systemui.util.mockito.withArgCaptor
40 import com.android.systemui.util.time.FakeSystemClock
41 import java.util.concurrent.TimeUnit
42 import org.junit.Assert.assertEquals
43 import org.junit.Assert.assertFalse
44 import org.junit.Assert.assertTrue
45 import org.junit.Before
46 import org.junit.Test
47 import org.junit.runner.RunWith
48 import org.mockito.Mock
49 import org.mockito.Mockito.clearInvocations
50 import org.mockito.Mockito.never
51 import org.mockito.Mockito.verify
52 import org.mockito.Mockito.`when`
53 import org.mockito.MockitoAnnotations
54 
55 @SmallTest
56 @RunWith(AndroidJUnit4::class)
57 class SmartspaceDedupingCoordinatorTest : SysuiTestCase() {
58 
59     @Mock
60     private lateinit var statusBarStateController: SysuiStatusBarStateController
61     @Mock
62     private lateinit var smartspaceController: LockscreenSmartspaceController
63     @Mock
64     private lateinit var notifPipeline: NotifPipeline
65     @Mock
66     private lateinit var pluggableListener: Pluggable.PluggableListener<NotifFilter>
67 
68     private lateinit var filter: NotifFilter
69     private lateinit var collectionListener: NotifCollectionListener
70     private lateinit var statusBarListener: StatusBarStateController.StateListener
71     private lateinit var newTargetListener: SmartspaceTargetListener
72 
73     private lateinit var entry1HasRecentlyAlerted: NotificationEntry
74     private lateinit var entry2HasNotRecentlyAlerted: NotificationEntry
75     private lateinit var entry3NotAssociatedWithTarget: NotificationEntry
76     private lateinit var entry4HasNotRecentlyAlerted: NotificationEntry
77     private lateinit var target1: SmartspaceTarget
78     private lateinit var target2: SmartspaceTarget
79     private lateinit var target4: SmartspaceTarget
80 
81     private val clock = FakeSystemClock()
82     private val executor = FakeExecutor(clock)
83     private val now = clock.currentTimeMillis()
84 
85     private lateinit var deduper: SmartspaceDedupingCoordinator
86 
87     @Before
setUpnull88     fun setUp() {
89         MockitoAnnotations.initMocks(this)
90 
91         // Mock out some behavior
92         `when`(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
93 
94         // Build the deduper
95         deduper = SmartspaceDedupingCoordinator(
96                 statusBarStateController,
97                 smartspaceController,
98                 notifPipeline,
99                 executor,
100                 clock
101         )
102 
103         // Attach the deduper and capture the listeners/filters that it registers
104         deduper.attach(notifPipeline)
105 
106         filter = withArgCaptor {
107             verify(notifPipeline).addPreGroupFilter(capture())
108         }
109         filter.setInvalidationListener(pluggableListener)
110 
111         collectionListener = withArgCaptor {
112             verify(notifPipeline).addCollectionListener(capture())
113         }
114 
115         statusBarListener = withArgCaptor {
116             verify(statusBarStateController).addCallback(capture())
117         }
118 
119         newTargetListener = withArgCaptor {
120             verify(smartspaceController).addListener(capture())
121         }
122 
123         // Initialize some test data
124         entry1HasRecentlyAlerted = NotificationEntryBuilder()
125                 .setPkg(PACKAGE_1)
126                 .setId(11)
127                 .setLastAudiblyAlertedMs(now - 10000)
128                 .build()
129         entry2HasNotRecentlyAlerted = NotificationEntryBuilder()
130                 .setPkg(PACKAGE_2)
131                 .setId(22)
132                 .build()
133         entry3NotAssociatedWithTarget = NotificationEntryBuilder()
134                 .setPkg("com.test.package.3")
135                 .setId(33)
136                 .setLastAudiblyAlertedMs(now - 10000)
137                 .build()
138         entry4HasNotRecentlyAlerted = NotificationEntryBuilder()
139                 .setPkg(PACKAGE_2)
140                 .setId(44)
141                 .build()
142 
143         target1 = buildTargetFor(entry1HasRecentlyAlerted)
144         target2 = buildTargetFor(entry2HasNotRecentlyAlerted)
145         target4 = buildTargetFor(entry4HasNotRecentlyAlerted)
146     }
147 
148     @Test
testBasicFilteringnull149     fun testBasicFiltering() {
150         // GIVEN a few notifications
151         addEntries(
152                 entry2HasNotRecentlyAlerted,
153                 entry3NotAssociatedWithTarget,
154                 entry4HasNotRecentlyAlerted)
155 
156         // WHEN we receive smartspace targets associated with entry 2 and 3
157         sendTargets(target2, target4)
158 
159         // THEN both pipelines are rerun
160         verifyPipelinesInvalidated()
161 
162         // THEN the first target is filtered out, but the other ones aren't
163         assertTrue(filter.shouldFilterOut(entry2HasNotRecentlyAlerted, now))
164         assertFalse(filter.shouldFilterOut(entry3NotAssociatedWithTarget, now))
165         assertFalse(filter.shouldFilterOut(entry4HasNotRecentlyAlerted, now))
166     }
167 
168     @Test
testDoNotFilterRecentlyAlertedNotifsnull169     fun testDoNotFilterRecentlyAlertedNotifs() {
170         // GIVEN one notif that recently alerted and a second that hasn't
171         addEntries(entry1HasRecentlyAlerted, entry2HasNotRecentlyAlerted)
172 
173         // WHEN they become associated with smartspace targets
174         sendTargets(target1, target2)
175 
176         // THEN neither is filtered (the first because it's recently alerted and the second
177         // because it's not in the first position
178         verifyPipelinesNotInvalidated()
179         assertFalse(filter.shouldFilterOut(entry1HasRecentlyAlerted, now))
180         assertFalse(filter.shouldFilterOut(entry2HasNotRecentlyAlerted, now))
181     }
182 
183     @Test
testFilterAlertedButNotRecentNotifsnull184     fun testFilterAlertedButNotRecentNotifs() {
185         // GIVEN a notification that alerted, but a very long time ago
186         val entryOldAlert = NotificationEntryBuilder(entry1HasRecentlyAlerted)
187                 .setLastAudiblyAlertedMs(now - 40000)
188                 .build()
189         addEntries(entryOldAlert)
190 
191         // WHEN it becomes part of smartspace
192         val target = buildTargetFor(entryOldAlert)
193         sendTargets(target)
194 
195         // THEN it's still filtered out (because it's not in the alert window)
196         verifyPipelinesInvalidated()
197         assertTrue(filter.shouldFilterOut(entryOldAlert, now))
198     }
199 
200     @Test
testExceptionExpiresnull201     fun testExceptionExpires() {
202         // GIVEN a recently-alerted notif that is the primary smartspace target
203         addEntries(entry1HasRecentlyAlerted)
204         sendTargets(target1)
205         clearPipelineInvocations()
206 
207         // WHEN we go beyond the target's exception window
208         clock.advanceTime(20000)
209 
210         // THEN the pipeline is invalidated
211         verifyPipelinesInvalidated()
212         assertExecutorIsClear()
213     }
214 
215     @Test
testExceptionIsEventuallyFilterednull216     fun testExceptionIsEventuallyFiltered() {
217         // GIVEN a notif that has recently alerted
218         addEntries(entry1HasRecentlyAlerted)
219 
220         // WHEN it becomes the primary smartspace target
221         sendTargets(target1)
222 
223         // THEN it isn't filtered out (because it recently alerted)
224         assertFalse(filter.shouldFilterOut(entry1HasRecentlyAlerted, now))
225 
226         // WHEN we pass the alert window
227         clock.advanceTime(20000)
228 
229         // THEN the notif is once again filtered
230         assertTrue(filter.shouldFilterOut(entry1HasRecentlyAlerted, clock.uptimeMillis()))
231     }
232 
233     @Test
testExceptionIsUpdatednull234     fun testExceptionIsUpdated() {
235         // GIVEN a notif that has recently alerted and is the primary smartspace target
236         addEntries(entry1HasRecentlyAlerted)
237         sendTargets(target1)
238         clearPipelineInvocations()
239         assertFalse(filter.shouldFilterOut(entry1HasRecentlyAlerted, clock.uptimeMillis()))
240 
241         // GIVEN the notif is updated with a much more recent alert time
242         NotificationEntryBuilder(entry1HasRecentlyAlerted)
243                 .setLastAudiblyAlertedMs(clock.currentTimeMillis() - 500)
244                 .apply(entry1HasRecentlyAlerted)
245         updateEntries(entry1HasRecentlyAlerted)
246         assertFalse(filter.shouldFilterOut(entry1HasRecentlyAlerted, clock.uptimeMillis()))
247 
248         // WHEN we advance beyond the original exception window
249         clock.advanceTime(25000)
250 
251         // THEN the original exception window doesn't fire
252         verifyPipelinesNotInvalidated()
253         assertFalse(filter.shouldFilterOut(entry1HasRecentlyAlerted, clock.uptimeMillis()))
254 
255         // WHEN we advance beyond the new exception window
256         clock.advanceTime(4500)
257 
258         // THEN the pipelines are invalidated and no more timeouts are scheduled
259         verifyPipelinesInvalidated()
260         assertExecutorIsClear()
261         assertTrue(filter.shouldFilterOut(entry1HasRecentlyAlerted, clock.uptimeMillis()))
262     }
263 
264     @Test
testReplacementIsCancelednull265     fun testReplacementIsCanceled() {
266         // GIVEN a single notif and smartspace target
267         addEntries(entry1HasRecentlyAlerted)
268         sendTargets(target1)
269         clearPipelineInvocations()
270 
271         // WHEN a higher-ranked target arrives
272         val newerEntry = NotificationEntryBuilder()
273                 .setPkg(PACKAGE_2)
274                 .setId(55)
275                 .setLastAudiblyAlertedMs(now - 1000)
276                 .build()
277         val newerTarget = buildTargetFor(newerEntry)
278         sendTargets(newerTarget, target1)
279 
280         // THEN the timeout of the other target is canceled and it is no longer filtered
281         assertExecutorIsClear()
282         assertFalse(filter.shouldFilterOut(entry1HasRecentlyAlerted, clock.uptimeMillis()))
283         verifyPipelinesInvalidated()
284         clearPipelineInvocations()
285 
286         // WHEN the entry associated with the newer target later arrives
287         addEntries(newerEntry)
288 
289         // THEN the entry is not filtered out (because it recently alerted)
290         assertFalse(filter.shouldFilterOut(newerEntry, clock.uptimeMillis()))
291 
292         // WHEN its exception window passes
293         clock.advanceTime(ALERT_WINDOW)
294 
295         // THEN we go back to filtering it
296         verifyPipelinesInvalidated()
297         assertExecutorIsClear()
298         assertTrue(filter.shouldFilterOut(newerEntry, clock.uptimeMillis()))
299     }
300 
301     @Test
testRetractedIsCancelednull302     fun testRetractedIsCanceled() {
303         // GIVEN A recently alerted target
304         addEntries(entry1HasRecentlyAlerted)
305         sendTargets(target1)
306 
307         // WHEN the entry is removed
308         removeEntries(entry1HasRecentlyAlerted)
309 
310         // THEN its pending timeout is canceled
311         assertExecutorIsClear()
312         clock.advanceTime(ALERT_WINDOW)
313         verifyPipelinesNotInvalidated()
314     }
315 
316     @Test
testTargetBeforeEntryFunctionsProperlynull317     fun testTargetBeforeEntryFunctionsProperly() {
318         // WHEN targets are added before their entries exist
319         sendTargets(target2, target1)
320 
321         // THEN neither is filtered out
322         assertFalse(filter.shouldFilterOut(entry2HasNotRecentlyAlerted, now))
323         assertFalse(filter.shouldFilterOut(entry1HasRecentlyAlerted, now))
324 
325         // WHEN the entries are later added
326         addEntries(entry2HasNotRecentlyAlerted, entry1HasRecentlyAlerted)
327 
328         // THEN the pipelines are not invalidated (because they're already going to be rerun)
329         // but the first entry is still filtered out properly.
330         verifyPipelinesNotInvalidated()
331         assertTrue(filter.shouldFilterOut(entry2HasNotRecentlyAlerted, now))
332     }
333 
334     @Test
testLockscreenTrackingnull335     fun testLockscreenTracking() {
336         // GIVEN a couple of smartspace targets that haven't alerted recently
337         addEntries(entry2HasNotRecentlyAlerted, entry4HasNotRecentlyAlerted)
338         sendTargets(target2, target4)
339         clearPipelineInvocations()
340 
341         assertTrue(filter.shouldFilterOut(entry2HasNotRecentlyAlerted, now))
342 
343         // WHEN we are no longer on the keyguard
344         statusBarListener.onStateChanged(StatusBarState.SHADE)
345 
346         // THEN the new pipeline is invalidated (but the old one isn't because it's not
347         // necessary) because the notif should no longer be filtered out
348         verify(pluggableListener).onPluggableInvalidated(eq(filter), any())
349         assertFalse(filter.shouldFilterOut(entry2HasNotRecentlyAlerted, now))
350     }
351 
buildTargetFornull352     private fun buildTargetFor(entry: NotificationEntry): SmartspaceTarget {
353         return SmartspaceTarget
354                 .Builder("test", ComponentName("test", "class"), UserHandle.CURRENT)
355                 .setSourceNotificationKey(entry.key)
356                 .build()
357     }
358 
addEntriesnull359     private fun addEntries(vararg entries: NotificationEntry) {
360         for (entry in entries) {
361             `when`(notifPipeline.getEntry(entry.key)).thenReturn(entry)
362             collectionListener.onEntryAdded(entry)
363         }
364     }
365 
updateEntriesnull366     private fun updateEntries(vararg entries: NotificationEntry) {
367         for (entry in entries) {
368             `when`(notifPipeline.getEntry(entry.key)).thenReturn(entry)
369             collectionListener.onEntryUpdated(entry)
370         }
371     }
372 
removeEntriesnull373     private fun removeEntries(vararg entries: NotificationEntry) {
374         for (entry in entries) {
375             `when`(notifPipeline.getEntry(entry.key)).thenReturn(null)
376             collectionListener.onEntryRemoved(entry, 0)
377         }
378     }
379 
sendTargetsnull380     private fun sendTargets(vararg targets: SmartspaceTarget) {
381         newTargetListener.onSmartspaceTargetsUpdated(targets.toMutableList())
382     }
383 
verifyPipelinesInvalidatednull384     private fun verifyPipelinesInvalidated() {
385         verify(pluggableListener).onPluggableInvalidated(eq(filter), any())
386     }
387 
assertExecutorIsClearnull388     private fun assertExecutorIsClear() {
389         assertEquals(0, executor.numPending())
390     }
391 
verifyPipelinesNotInvalidatednull392     private fun verifyPipelinesNotInvalidated() {
393         verify(pluggableListener, never()).onPluggableInvalidated(eq(filter), any())
394     }
395 
clearPipelineInvocationsnull396     private fun clearPipelineInvocations() {
397         clearInvocations(pluggableListener)
398     }
399 }
400 
401 private val ALERT_WINDOW = TimeUnit.SECONDS.toMillis(30)
402 private const val PACKAGE_1 = "com.test.package.1"
403 private const val PACKAGE_2 = "com.test.package.2"
404