• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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