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