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