1 /* <lambda>null2 * Copyright (C) 2023 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.stack.ui.view 18 19 import android.service.notification.notificationListenerService 20 import androidx.test.ext.junit.runners.AndroidJUnit4 21 import androidx.test.filters.SmallTest 22 import com.android.internal.statusbar.NotificationVisibility 23 import com.android.internal.statusbar.statusBarService 24 import com.android.systemui.SysuiTestCase 25 import com.android.systemui.kosmos.testScope 26 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel 27 import com.android.systemui.statusbar.notification.logging.nano.Notifications 28 import com.android.systemui.statusbar.notification.logging.notificationPanelLogger 29 import com.android.systemui.statusbar.notification.stack.ExpandableViewState 30 import com.android.systemui.testKosmos 31 import com.android.systemui.util.mockito.argumentCaptor 32 import com.android.systemui.util.mockito.eq 33 import com.google.common.truth.Truth.assertThat 34 import java.util.concurrent.Callable 35 import kotlinx.coroutines.test.runCurrent 36 import kotlinx.coroutines.test.runTest 37 import org.junit.Test 38 import org.junit.runner.RunWith 39 import org.mockito.Mockito.clearInvocations 40 import org.mockito.Mockito.spy 41 import org.mockito.Mockito.verify 42 import org.mockito.Mockito.verifyNoMoreInteractions 43 44 @SmallTest 45 @RunWith(AndroidJUnit4::class) 46 class NotificationStatsLoggerTest : SysuiTestCase() { 47 48 private val kosmos = testKosmos() 49 50 private val testScope = kosmos.testScope 51 private val mockNotificationListenerService = kosmos.notificationListenerService 52 private val mockPanelLogger = kosmos.notificationPanelLogger 53 private val mockStatusBarService = kosmos.statusBarService 54 55 private val underTest = kosmos.notificationStatsLogger 56 57 private val visibilityArrayCaptor = argumentCaptor<Array<NotificationVisibility>>() 58 private val stringArrayCaptor = argumentCaptor<Array<String>>() 59 private val notificationListProtoCaptor = argumentCaptor<Notifications.NotificationList>() 60 61 @Test 62 fun onNotificationListUpdated_itemsAdded_logsNewlyVisibleItems() = 63 testScope.runTest { 64 // WHEN new Notifications are added 65 // AND they're visible 66 val (ranks, locations) = fakeNotificationMaps("key0", "key1") 67 val callable = Callable { locations } 68 underTest.onNotificationLocationsChanged(callable, ranks) 69 runCurrent() 70 71 // THEN visibility changes are reported 72 verify(mockStatusBarService) 73 .onNotificationVisibilityChanged(visibilityArrayCaptor.capture(), eq(emptyArray())) 74 verify(mockNotificationListenerService) 75 .setNotificationsShown(stringArrayCaptor.capture()) 76 val loggedVisibilities = visibilityArrayCaptor.value 77 val loggedKeys = stringArrayCaptor.value 78 assertThat(loggedVisibilities).hasLength(2) 79 assertThat(loggedKeys).hasLength(2) 80 assertThat(loggedVisibilities[0]).apply { 81 isKeyEqualTo("key0") 82 isRankEqualTo(0) 83 isVisible() 84 isInMainArea() 85 isCountEqualTo(2) 86 } 87 assertThat(loggedVisibilities[1]).apply { 88 isKeyEqualTo("key1") 89 isRankEqualTo(1) 90 isVisible() 91 isInMainArea() 92 isCountEqualTo(2) 93 } 94 assertThat(loggedKeys[0]).isEqualTo("key0") 95 assertThat(loggedKeys[1]).isEqualTo("key1") 96 } 97 98 @Test 99 fun onNotificationListUpdated_itemsRemoved_logsNoLongerVisibleItems() = 100 testScope.runTest { 101 // GIVEN some visible Notifications are reported 102 val (ranks, locations) = fakeNotificationMaps("key0", "key1") 103 val callable = Callable { locations } 104 underTest.onNotificationLocationsChanged(callable, ranks) 105 runCurrent() 106 clearInvocations(mockStatusBarService, mockNotificationListenerService) 107 108 // WHEN the same Notifications are removed 109 val emptyCallable = Callable { emptyMap<String, Int>() } 110 underTest.onNotificationLocationsChanged(emptyCallable, emptyMap()) 111 runCurrent() 112 113 // THEN visibility changes are reported 114 verify(mockStatusBarService) 115 .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture()) 116 verifyNoMoreInteractions(mockNotificationListenerService) 117 val noLongerVisible = visibilityArrayCaptor.value 118 assertThat(noLongerVisible).hasLength(2) 119 assertThat(noLongerVisible[0]).apply { 120 isKeyEqualTo("key0") 121 isRankEqualTo(0) 122 notVisible() 123 isInMainArea() 124 isCountEqualTo(0) 125 } 126 assertThat(noLongerVisible[1]).apply { 127 isKeyEqualTo("key1") 128 isRankEqualTo(1) 129 notVisible() 130 isInMainArea() 131 isCountEqualTo(0) 132 } 133 } 134 135 @Test 136 fun onNotificationListUpdated_itemsBecomeInvisible_logsNoLongerVisibleItems() = 137 testScope.runTest { 138 // GIVEN some visible Notifications are reported 139 val (ranks, locations) = fakeNotificationMaps("key0", "key1") 140 val callable = Callable { locations } 141 underTest.onNotificationLocationsChanged(callable, ranks) 142 runCurrent() 143 clearInvocations(mockStatusBarService, mockNotificationListenerService) 144 145 // WHEN the same Notifications are becoming invisible 146 val emptyCallable = Callable { emptyMap<String, Int>() } 147 underTest.onNotificationLocationsChanged(emptyCallable, ranks) 148 runCurrent() 149 150 // THEN visibility changes are reported 151 verify(mockStatusBarService) 152 .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture()) 153 verifyNoMoreInteractions(mockNotificationListenerService) 154 val noLongerVisible = visibilityArrayCaptor.value 155 assertThat(noLongerVisible).hasLength(2) 156 assertThat(noLongerVisible[0]).apply { 157 isKeyEqualTo("key0") 158 isRankEqualTo(0) 159 notVisible() 160 isInMainArea() 161 isCountEqualTo(2) 162 } 163 assertThat(noLongerVisible[1]).apply { 164 isKeyEqualTo("key1") 165 isRankEqualTo(1) 166 notVisible() 167 isInMainArea() 168 isCountEqualTo(2) 169 } 170 } 171 172 @Test 173 fun onNotificationVisibilityChanged_thenShadeNotInteractive_noDuplicateLogs() = 174 testScope.runTest { 175 // GIVEN a visible Notifications is reported 176 val (ranks, locations) = fakeNotificationMaps("key0") 177 val callable = Callable { locations } 178 underTest.onNotificationLocationsChanged(callable, ranks) 179 runCurrent() 180 clearInvocations(mockStatusBarService, mockNotificationListenerService) 181 182 // WHEN the same Notification becomins invisible 183 val emptyCallable = Callable { emptyMap<String, Int>() } 184 underTest.onNotificationLocationsChanged(emptyCallable, ranks) 185 // AND notifications become non interactible 186 underTest.onLockscreenOrShadeNotInteractive(emptyList()) 187 runCurrent() 188 189 // THEN visibility changes are reported 190 verify(mockStatusBarService) 191 .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture()) 192 val noLongerVisible = visibilityArrayCaptor.value 193 assertThat(noLongerVisible).hasLength(1) 194 assertThat(noLongerVisible[0]).apply { 195 isKeyEqualTo("key0") 196 isRankEqualTo(0) 197 notVisible() 198 isInMainArea() 199 isCountEqualTo(1) 200 } 201 202 // AND nothing else is logged 203 verifyNoMoreInteractions(mockStatusBarService) 204 verifyNoMoreInteractions(mockNotificationListenerService) 205 } 206 207 @Test 208 fun onNotificationListUpdated_itemsChangedPositions_nothingLogged() = 209 testScope.runTest { 210 // GIVEN some visible Notifications are reported 211 val (ranks, locations) = fakeNotificationMaps("key0", "key1") 212 underTest.onNotificationLocationsChanged({ locations }, ranks) 213 runCurrent() 214 clearInvocations(mockStatusBarService, mockNotificationListenerService) 215 216 // WHEN the reported Notifications are changing positions 217 val (newRanks, newLocations) = fakeNotificationMaps("key1", "key0") 218 underTest.onNotificationLocationsChanged({ newLocations }, newRanks) 219 runCurrent() 220 221 // THEN no visibility changes are reported 222 verifyNoMoreInteractions(mockStatusBarService, mockNotificationListenerService) 223 } 224 225 @Test 226 fun onNotificationListUpdated_calledTwice_usesTheNewCallable() = 227 testScope.runTest { 228 // GIVEN some visible Notifications are reported 229 val (ranks, locations) = fakeNotificationMaps("key0", "key1", "key2") 230 val callable = spy(Callable { locations }) 231 underTest.onNotificationLocationsChanged(callable, ranks) 232 runCurrent() 233 clearInvocations(callable) 234 235 // WHEN a new update comes 236 val otherCallable = spy(Callable { locations }) 237 underTest.onNotificationLocationsChanged(otherCallable, ranks) 238 runCurrent() 239 240 // THEN we call the new Callable 241 verifyNoMoreInteractions(callable) 242 verify(otherCallable).call() 243 } 244 245 @Test 246 fun onLockscreenOrShadeNotInteractive_logsNoLongerVisibleItems() = 247 testScope.runTest { 248 // GIVEN some visible Notifications are reported 249 val (ranks, locations) = fakeNotificationMaps("key0", "key1") 250 val callable = Callable { locations } 251 underTest.onNotificationLocationsChanged(callable, ranks) 252 runCurrent() 253 clearInvocations(mockStatusBarService, mockNotificationListenerService) 254 255 // WHEN the Shade becomes non interactive 256 underTest.onLockscreenOrShadeNotInteractive(emptyList()) 257 runCurrent() 258 259 // THEN visibility changes are reported 260 verify(mockStatusBarService) 261 .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture()) 262 verifyNoMoreInteractions(mockNotificationListenerService) 263 val noLongerVisible = visibilityArrayCaptor.value 264 assertThat(noLongerVisible).hasLength(2) 265 assertThat(noLongerVisible[0]).apply { 266 isKeyEqualTo("key0") 267 isRankEqualTo(0) 268 notVisible() 269 isInMainArea() 270 isCountEqualTo(0) 271 } 272 assertThat(noLongerVisible[1]).apply { 273 isKeyEqualTo("key1") 274 isRankEqualTo(1) 275 notVisible() 276 isInMainArea() 277 isCountEqualTo(0) 278 } 279 } 280 281 @Test 282 fun onLockscreenOrShadeInteractive_logsPanelShown() = 283 testScope.runTest { 284 // WHEN the Shade becomes interactive 285 underTest.onLockscreenOrShadeInteractive( 286 isOnLockScreen = true, 287 listOf( 288 activeNotificationModel( 289 key = "key0", 290 uid = 0, 291 packageName = "com.android.first", 292 ), 293 activeNotificationModel( 294 key = "key1", 295 uid = 1, 296 packageName = "com.android.second", 297 ), 298 ), 299 ) 300 runCurrent() 301 302 // THEN the Panel shown event is reported 303 verify(mockPanelLogger).logPanelShown(eq(true), notificationListProtoCaptor.capture()) 304 val loggedNotifications = notificationListProtoCaptor.value.notifications 305 assertThat(loggedNotifications.size).isEqualTo(2) 306 with(loggedNotifications[0]) { 307 assertThat(uid).isEqualTo(0) 308 assertThat(packageName).isEqualTo("com.android.first") 309 } 310 with(loggedNotifications[1]) { 311 assertThat(uid).isEqualTo(1) 312 assertThat(packageName).isEqualTo("com.android.second") 313 } 314 } 315 316 @Test 317 fun onNotificationExpansionChanged_whenExpandedInVisibleLocation_logsExpansion() = 318 testScope.runTest { 319 // WHEN a Notification is expanded 320 underTest.onNotificationExpansionChanged( 321 key = "key", 322 isExpanded = true, 323 location = ExpandableViewState.LOCATION_MAIN_AREA, 324 isUserAction = true, 325 ) 326 runCurrent() 327 328 // THEN the Expand event is reported 329 verify(mockStatusBarService) 330 .onNotificationExpansionChanged( 331 /* key = */ "key", 332 /* userAction = */ true, 333 /* expanded = */ true, 334 NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, 335 ) 336 } 337 338 @Test 339 fun onNotificationExpansionChanged_whenCalledTwiceWithTheSameUpdate_doesNotDuplicateLogs() = 340 testScope.runTest { 341 // GIVEN a Notification is expanded 342 underTest.onNotificationExpansionChanged( 343 key = "key", 344 isExpanded = true, 345 location = ExpandableViewState.LOCATION_MAIN_AREA, 346 isUserAction = true, 347 ) 348 runCurrent() 349 clearInvocations(mockStatusBarService) 350 351 // WHEN the logger receives the same expansion update 352 underTest.onNotificationExpansionChanged( 353 key = "key", 354 isExpanded = true, 355 location = ExpandableViewState.LOCATION_MAIN_AREA, 356 isUserAction = true, 357 ) 358 runCurrent() 359 360 // THEN the Expand event is not reported again 361 verifyNoMoreInteractions(mockStatusBarService) 362 } 363 364 @Test 365 fun onNotificationExpansionChanged_whenCalledForNotVisibleItem_nothingLogged() = 366 testScope.runTest { 367 // WHEN a NOT visible Notification is expanded 368 underTest.onNotificationExpansionChanged( 369 key = "key", 370 isExpanded = true, 371 location = ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN, 372 isUserAction = true, 373 ) 374 runCurrent() 375 376 // No events are reported 377 verifyNoMoreInteractions(mockStatusBarService) 378 } 379 380 @Test 381 fun onNotificationExpansionChanged_whenNotVisibleItemBecomesVisible_logsChanges() = 382 testScope.runTest { 383 // WHEN a NOT visible Notification is expanded 384 underTest.onNotificationExpansionChanged( 385 key = "key", 386 isExpanded = true, 387 location = ExpandableViewState.LOCATION_GONE, 388 isUserAction = false, 389 ) 390 runCurrent() 391 392 // AND it becomes visible 393 val (ranks, locations) = fakeNotificationMaps("key") 394 val callable = Callable { locations } 395 underTest.onNotificationLocationsChanged(callable, ranks) 396 runCurrent() 397 398 // THEN the Expand event is reported 399 verify(mockStatusBarService) 400 .onNotificationExpansionChanged( 401 /* key = */ "key", 402 /* userAction = */ false, 403 /* expanded = */ true, 404 NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, 405 ) 406 } 407 408 @Test 409 fun onNotificationExpansionChanged_whenUpdatedItemBecomesVisible_logsChanges() = 410 testScope.runTest { 411 // GIVEN a NOT visible Notification is expanded 412 underTest.onNotificationExpansionChanged( 413 key = "key", 414 isExpanded = true, 415 location = ExpandableViewState.LOCATION_GONE, 416 isUserAction = false, 417 ) 418 runCurrent() 419 // AND we open the shade, so we log its events 420 val (ranks, locations) = fakeNotificationMaps("key") 421 val callable = Callable { locations } 422 underTest.onNotificationLocationsChanged(callable, ranks) 423 runCurrent() 424 // AND we close the shade, so it is NOT visible 425 val emptyCallable = Callable { emptyMap<String, Int>() } 426 underTest.onNotificationLocationsChanged(emptyCallable, ranks) 427 runCurrent() 428 clearInvocations(mockStatusBarService) // clear the previous expand log 429 430 // WHEN it receives an update 431 underTest.onNotificationUpdated("key") 432 // AND it becomes visible again 433 underTest.onNotificationLocationsChanged(callable, ranks) 434 runCurrent() 435 436 // THEN we log its expand event again 437 verify(mockStatusBarService) 438 .onNotificationExpansionChanged( 439 /* key = */ "key", 440 /* userAction = */ false, 441 /* expanded = */ true, 442 NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, 443 ) 444 } 445 446 @Test 447 fun onNotificationExpansionChanged_whenCollapsedForTheFirstTime_nothingLogged() = 448 testScope.runTest { 449 // WHEN a Notification is collapsed, and it is the first interaction 450 underTest.onNotificationExpansionChanged( 451 key = "key", 452 isExpanded = false, 453 location = ExpandableViewState.LOCATION_MAIN_AREA, 454 isUserAction = false, 455 ) 456 runCurrent() 457 458 // THEN no events are reported, because we consider the Notification initially 459 // collapsed, so only expanded is logged in the first time. 460 verifyNoMoreInteractions(mockStatusBarService) 461 } 462 463 @Test 464 fun onNotificationExpansionChanged_receivesMultipleUpdates_logsChanges() = 465 testScope.runTest { 466 // GIVEN a Notification is expanded 467 underTest.onNotificationExpansionChanged( 468 key = "key", 469 isExpanded = true, 470 location = ExpandableViewState.LOCATION_MAIN_AREA, 471 isUserAction = true, 472 ) 473 runCurrent() 474 475 // WHEN the Notification is collapsed 476 verify(mockStatusBarService) 477 .onNotificationExpansionChanged( 478 /* key = */ "key", 479 /* userAction = */ true, 480 /* expanded = */ true, 481 NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, 482 ) 483 484 // AND the Notification is expanded again 485 underTest.onNotificationExpansionChanged( 486 key = "key", 487 isExpanded = false, 488 location = ExpandableViewState.LOCATION_MAIN_AREA, 489 isUserAction = true, 490 ) 491 runCurrent() 492 493 // THEN the expansion changes are logged 494 verify(mockStatusBarService) 495 .onNotificationExpansionChanged( 496 /* key = */ "key", 497 /* userAction = */ true, 498 /* expanded = */ false, 499 NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, 500 ) 501 } 502 503 @Test 504 fun onNotificationUpdated_clearsTrackedExpansionChanges() = 505 testScope.runTest { 506 // GIVEN some notification updates are posted 507 underTest.onNotificationExpansionChanged( 508 key = "key1", 509 isExpanded = true, 510 location = ExpandableViewState.LOCATION_MAIN_AREA, 511 isUserAction = true, 512 ) 513 runCurrent() 514 underTest.onNotificationExpansionChanged( 515 key = "key2", 516 isExpanded = true, 517 location = ExpandableViewState.LOCATION_MAIN_AREA, 518 isUserAction = true, 519 ) 520 runCurrent() 521 clearInvocations(mockStatusBarService) 522 523 // WHEN a Notification is updated 524 underTest.onNotificationUpdated("key1") 525 526 // THEN the tracked expansion changes are updated 527 assertThat(underTest.lastReportedExpansionValues.keys).containsExactly("key2") 528 } 529 530 @Test 531 fun onNotificationRemoved_clearsTrackedExpansionChanges() = 532 testScope.runTest { 533 // GIVEN some notification updates are posted 534 underTest.onNotificationExpansionChanged( 535 key = "key1", 536 isExpanded = true, 537 location = ExpandableViewState.LOCATION_MAIN_AREA, 538 isUserAction = true, 539 ) 540 runCurrent() 541 underTest.onNotificationExpansionChanged( 542 key = "key2", 543 isExpanded = true, 544 location = ExpandableViewState.LOCATION_MAIN_AREA, 545 isUserAction = true, 546 ) 547 runCurrent() 548 clearInvocations(mockStatusBarService) 549 550 // WHEN a Notification is removed 551 underTest.onNotificationRemoved("key1") 552 553 // THEN it is removed from the tracked expansion changes 554 assertThat(underTest.lastReportedExpansionValues.keys).doesNotContain("key1") 555 } 556 557 private fun fakeNotificationMaps( 558 vararg keys: String 559 ): Pair<Map<String, Int>, Map<String, Int>> { 560 val ranks: Map<String, Int> = keys.mapIndexed { index, key -> key to index }.toMap() 561 val locations: Map<String, Int> = 562 keys.associateWith { ExpandableViewState.LOCATION_MAIN_AREA } 563 564 return Pair(ranks, locations) 565 } 566 567 private fun assertThat(visibility: NotificationVisibility) = 568 NotificationVisibilitySubject(visibility) 569 } 570 571 private class NotificationVisibilitySubject(private val visibility: NotificationVisibility) { isKeyEqualTonull572 fun isKeyEqualTo(key: String) = assertThat(visibility.key).isEqualTo(key) 573 574 fun isRankEqualTo(rank: Int) = assertThat(visibility.rank).isEqualTo(rank) 575 576 fun isCountEqualTo(count: Int) = assertThat(visibility.count).isEqualTo(count) 577 578 fun isVisible() = assertThat(this.visibility.visible).isTrue() 579 580 fun notVisible() = assertThat(this.visibility.visible).isFalse() 581 582 fun isInMainArea() = 583 assertThat(this.visibility.location) 584 .isEqualTo(NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA) 585 } 586