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