1 /*
<lambda>null2  * Copyright (C) 2022 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.logging
18 
19 import android.app.Notification
20 import android.app.StatsManager
21 import android.graphics.Bitmap
22 import android.graphics.drawable.Icon
23 import android.stats.sysui.NotificationEnums
24 import android.util.StatsEvent
25 import androidx.test.ext.junit.runners.AndroidJUnit4
26 import androidx.test.filters.SmallTest
27 import com.android.systemui.SysuiTestCase
28 import com.android.systemui.log.assertLogsWtf
29 import com.android.systemui.shared.system.SysUiStatsLog
30 import com.android.systemui.statusbar.notification.collection.NotifPipeline
31 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
32 import com.android.systemui.util.concurrency.FakeExecutor
33 import com.android.systemui.util.mockito.mock
34 import com.android.systemui.util.mockito.whenever
35 import com.android.systemui.util.time.FakeSystemClock
36 import com.google.common.truth.Expect
37 import com.google.common.truth.Truth.assertThat
38 import java.lang.RuntimeException
39 import kotlinx.coroutines.Dispatchers
40 import org.junit.Before
41 import org.junit.Rule
42 import org.junit.Test
43 import org.junit.runner.RunWith
44 import org.mockito.Mock
45 import org.mockito.Mockito.verify
46 import org.mockito.MockitoAnnotations
47 
48 @SmallTest
49 @RunWith(AndroidJUnit4::class)
50 class NotificationMemoryLoggerTest : SysuiTestCase() {
51 
52     @Rule @JvmField val expect = Expect.create()
53 
54     private val bgExecutor = FakeExecutor(FakeSystemClock())
55     private val immediate = Dispatchers.Main.immediate
56 
57     @Mock private lateinit var statsManager: StatsManager
58 
59     @Before
60     fun setUp() {
61         MockitoAnnotations.initMocks(this)
62     }
63 
64     @Test
65     fun onInit_registersCallback() {
66         val logger = createLoggerWithNotifications(listOf())
67         logger.init()
68         verify(statsManager)
69             .setPullAtomCallback(SysUiStatsLog.NOTIFICATION_MEMORY_USE, null, bgExecutor, logger)
70     }
71 
72     @Test
73     fun onPullAtom_wrongAtomId_returnsSkip() {
74         val logger = createLoggerWithNotifications(listOf())
75         val data: MutableList<StatsEvent> = mutableListOf()
76         assertThat(logger.onPullAtom(111, data)).isEqualTo(StatsManager.PULL_SKIP)
77         assertThat(data).isEmpty()
78     }
79 
80     @Test
81     fun onPullAtom_emptyNotifications_returnsZeros() {
82         val logger = createLoggerWithNotifications(listOf())
83         val data: MutableList<StatsEvent> = mutableListOf()
84         assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data))
85             .isEqualTo(StatsManager.PULL_SUCCESS)
86         assertThat(data).isEmpty()
87     }
88 
89     @Test
90     fun onPullAtom_notificationPassed_populatesData() {
91         val icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888))
92         val notification =
93             Notification.Builder(context).setSmallIcon(icon).setContentTitle("title").build()
94         val logger = createLoggerWithNotifications(listOf(notification))
95         val data: MutableList<StatsEvent> = mutableListOf()
96 
97         assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data))
98             .isEqualTo(StatsManager.PULL_SUCCESS)
99         assertThat(data).hasSize(1)
100     }
101 
102     @Test
103     fun onPullAtom_multipleNotificationsPassed_populatesData() {
104         val icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888))
105         val notification =
106             Notification.Builder(context).setSmallIcon(icon).setContentTitle("title").build()
107         val iconTwo = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888))
108 
109         val notificationTwo =
110             Notification.Builder(context)
111                 .setStyle(Notification.BigTextStyle().bigText("text"))
112                 .setSmallIcon(iconTwo)
113                 .setContentTitle("titleTwo")
114                 .build()
115         val logger = createLoggerWithNotifications(listOf(notification, notificationTwo))
116         val data: MutableList<StatsEvent> = mutableListOf()
117 
118         assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data))
119             .isEqualTo(StatsManager.PULL_SUCCESS)
120         assertThat(data).hasSize(2)
121     }
122 
123     @Test
124     fun onPullAtom_throwsInterruptedException_failsGracefully() {
125         val pipeline: NotifPipeline = mock()
126         whenever(pipeline.allNotifs).thenAnswer { throw InterruptedException("Timeout") }
127         val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
128         assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
129             .isEqualTo(StatsManager.PULL_SKIP)
130     }
131 
132     @Test
133     fun onPullAtom_throwsRuntimeException_failsGracefully() {
134         val pipeline: NotifPipeline = mock()
135         whenever(pipeline.allNotifs).thenThrow(RuntimeException("Something broke!"))
136         val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
137         assertLogsWtf {
138             assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
139                 .isEqualTo(StatsManager.PULL_SKIP)
140         }
141     }
142 
143     @Test
144     fun aggregateMemoryUsageData_returnsCorrectlyAggregatedSamePackageData() {
145         val usage = getPresetMemoryUsages()
146         val aggregateUsage = aggregateMemoryUsageData(usage)
147 
148         assertThat(aggregateUsage).hasSize(3)
149         assertThat(aggregateUsage)
150             .containsKey(Pair("package 1", NotificationEnums.STYLE_BIG_PICTURE))
151 
152         // Aggregated fields
153         val aggregatedData =
154             aggregateUsage[Pair("package 1", NotificationEnums.STYLE_BIG_PICTURE)]!!
155         val presetUsage1 = usage[0]
156         val presetUsage2 = usage[1]
157         assertAggregatedData(
158             aggregatedData,
159             2,
160             2,
161             smallIconObject =
162                 presetUsage1.objectUsage.smallIcon + presetUsage2.objectUsage.smallIcon,
163             smallIconBitmapCount = 2,
164             largeIconObject =
165                 presetUsage1.objectUsage.largeIcon + presetUsage2.objectUsage.largeIcon,
166             largeIconBitmapCount = 2,
167             bigPictureObject =
168                 presetUsage1.objectUsage.bigPicture + presetUsage2.objectUsage.bigPicture,
169             bigPictureBitmapCount = 2,
170             extras = presetUsage1.objectUsage.extras + presetUsage2.objectUsage.extras,
171             extenders = presetUsage1.objectUsage.extender + presetUsage2.objectUsage.extender,
172             // Only totals need to be summarized.
173             smallIconViews =
174                 presetUsage1.viewUsage[0].smallIcon + presetUsage2.viewUsage[0].smallIcon,
175             largeIconViews =
176                 presetUsage1.viewUsage[0].largeIcon + presetUsage2.viewUsage[0].largeIcon,
177             systemIconViews =
178                 presetUsage1.viewUsage[0].systemIcons + presetUsage2.viewUsage[0].systemIcons,
179             styleViews = presetUsage1.viewUsage[0].style + presetUsage2.viewUsage[0].style,
180             customViews =
181                 presetUsage1.viewUsage[0].customViews + presetUsage2.viewUsage[0].customViews,
182             softwareBitmaps =
183                 presetUsage1.viewUsage[0].softwareBitmapsPenalty +
184                     presetUsage2.viewUsage[0].softwareBitmapsPenalty,
185             seenCount = 0
186         )
187     }
188 
189     @Test
190     fun aggregateMemoryUsageData_correctlySeparatesDifferentStyles() {
191         val usage = getPresetMemoryUsages()
192         val aggregateUsage = aggregateMemoryUsageData(usage)
193 
194         assertThat(aggregateUsage).hasSize(3)
195         assertThat(aggregateUsage)
196             .containsKey(Pair("package 1", NotificationEnums.STYLE_BIG_PICTURE))
197         assertThat(aggregateUsage).containsKey(Pair("package 1", NotificationEnums.STYLE_BIG_TEXT))
198 
199         // Different style should be separate
200         val separateStyleData =
201             aggregateUsage[Pair("package 1", NotificationEnums.STYLE_BIG_TEXT)]!!
202         val presetUsage = usage[2]
203         assertAggregatedData(
204             separateStyleData,
205             1,
206             1,
207             presetUsage.objectUsage.smallIcon,
208             1,
209             presetUsage.objectUsage.largeIcon,
210             1,
211             presetUsage.objectUsage.bigPicture,
212             1,
213             presetUsage.objectUsage.extras,
214             presetUsage.objectUsage.extender,
215             presetUsage.viewUsage[0].smallIcon,
216             presetUsage.viewUsage[0].largeIcon,
217             presetUsage.viewUsage[0].systemIcons,
218             presetUsage.viewUsage[0].style,
219             presetUsage.viewUsage[0].customViews,
220             presetUsage.viewUsage[0].softwareBitmapsPenalty,
221             0
222         )
223     }
224 
225     @Test
226     fun aggregateMemoryUsageData_correctlySeparatesDifferentProcess() {
227         val usage = getPresetMemoryUsages()
228         val aggregateUsage = aggregateMemoryUsageData(usage)
229 
230         assertThat(aggregateUsage).hasSize(3)
231         assertThat(aggregateUsage)
232             .containsKey(Pair("package 2", NotificationEnums.STYLE_BIG_PICTURE))
233 
234         // Different UID/package should also be separate
235         val separatePackageData =
236             aggregateUsage[Pair("package 2", NotificationEnums.STYLE_BIG_PICTURE)]!!
237         val presetUsage = usage[3]
238         assertAggregatedData(
239             separatePackageData,
240             1,
241             1,
242             presetUsage.objectUsage.smallIcon,
243             1,
244             presetUsage.objectUsage.largeIcon,
245             1,
246             presetUsage.objectUsage.bigPicture,
247             1,
248             presetUsage.objectUsage.extras,
249             presetUsage.objectUsage.extender,
250             presetUsage.viewUsage[0].smallIcon,
251             presetUsage.viewUsage[0].largeIcon,
252             presetUsage.viewUsage[0].systemIcons,
253             presetUsage.viewUsage[0].style,
254             presetUsage.viewUsage[0].customViews,
255             presetUsage.viewUsage[0].softwareBitmapsPenalty,
256             0
257         )
258     }
259 
260     private fun createLoggerWithNotifications(
261         notifications: List<Notification>
262     ): NotificationMemoryLogger {
263         val pipeline: NotifPipeline = mock()
264         val notifications =
265             notifications.map { notification ->
266                 NotificationEntryBuilder().setTag("test").setNotification(notification).build()
267             }
268         whenever(pipeline.allNotifs).thenReturn(notifications)
269         return NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
270     }
271 
272     /**
273      * Short hand for making sure the passed NotificationMemoryUseAtomBuilder object contains
274      * expected values.
275      */
276     private fun assertAggregatedData(
277         value: NotificationMemoryLogger.NotificationMemoryUseAtomBuilder,
278         count: Int,
279         countWithInflatedViews: Int,
280         smallIconObject: Int,
281         smallIconBitmapCount: Int,
282         largeIconObject: Int,
283         largeIconBitmapCount: Int,
284         bigPictureObject: Int,
285         bigPictureBitmapCount: Int,
286         extras: Int,
287         extenders: Int,
288         smallIconViews: Int,
289         largeIconViews: Int,
290         systemIconViews: Int,
291         styleViews: Int,
292         customViews: Int,
293         softwareBitmaps: Int,
294         seenCount: Int
295     ) {
296         expect.withMessage("count").that(value.count).isEqualTo(count)
297         expect
298             .withMessage("countWithInflatedViews")
299             .that(value.countWithInflatedViews)
300             .isEqualTo(countWithInflatedViews)
301         expect.withMessage("smallIconObject").that(value.smallIconObject).isEqualTo(smallIconObject)
302         expect
303             .withMessage("smallIconBitmapCount")
304             .that(value.smallIconBitmapCount)
305             .isEqualTo(smallIconBitmapCount)
306         expect.withMessage("largeIconObject").that(value.largeIconObject).isEqualTo(largeIconObject)
307         expect
308             .withMessage("largeIconBitmapCount")
309             .that(value.largeIconBitmapCount)
310             .isEqualTo(largeIconBitmapCount)
311         expect
312             .withMessage("bigPictureObject")
313             .that(value.bigPictureObject)
314             .isEqualTo(bigPictureObject)
315         expect
316             .withMessage("bigPictureBitmapCount")
317             .that(value.bigPictureBitmapCount)
318             .isEqualTo(bigPictureBitmapCount)
319         expect.withMessage("extras").that(value.extras).isEqualTo(extras)
320         expect.withMessage("extenders").that(value.extenders).isEqualTo(extenders)
321         expect.withMessage("smallIconViews").that(value.smallIconViews).isEqualTo(smallIconViews)
322         expect.withMessage("largeIconViews").that(value.largeIconViews).isEqualTo(largeIconViews)
323         expect.withMessage("systemIconViews").that(value.systemIconViews).isEqualTo(systemIconViews)
324         expect.withMessage("styleViews").that(value.styleViews).isEqualTo(styleViews)
325         expect.withMessage("customViews").that(value.customViews).isEqualTo(customViews)
326         expect.withMessage("softwareBitmaps").that(value.softwareBitmaps).isEqualTo(softwareBitmaps)
327         expect.withMessage("seenCount").that(value.seenCount).isEqualTo(seenCount)
328     }
329 
330     /** Generates a static set of [NotificationMemoryUsage] objects. */
331     private fun getPresetMemoryUsages() =
332         listOf(
333             // A pair of notifications that have to be aggregated, same UID and style
334             NotificationMemoryUsage(
335                 "package 1",
336                 384,
337                 "key1",
338                 Notification.Builder(context).setStyle(Notification.BigPictureStyle()).build(),
339                 NotificationObjectUsage(
340                     23,
341                     45,
342                     67,
343                     NotificationEnums.STYLE_BIG_PICTURE,
344                     12,
345                     483,
346                     4382,
347                     true
348                 ),
349                 listOf(
350                     NotificationViewUsage(ViewType.TOTAL, 493, 584, 4833, 584, 4888, 5843),
351                     NotificationViewUsage(
352                         ViewType.PRIVATE_CONTRACTED_VIEW,
353                         100,
354                         250,
355                         300,
356                         594,
357                         6000,
358                         5843
359                     )
360                 )
361             ),
362             NotificationMemoryUsage(
363                 "package 1",
364                 384,
365                 "key2",
366                 Notification.Builder(context).setStyle(Notification.BigPictureStyle()).build(),
367                 NotificationObjectUsage(
368                     77,
369                     54,
370                     34,
371                     NotificationEnums.STYLE_BIG_PICTURE,
372                     77,
373                     432,
374                     2342,
375                     true
376                 ),
377                 listOf(
378                     NotificationViewUsage(ViewType.TOTAL, 3245, 1234, 7653, 543, 765, 7655),
379                     NotificationViewUsage(
380                         ViewType.PRIVATE_CONTRACTED_VIEW,
381                         160,
382                         350,
383                         300,
384                         5544,
385                         66500,
386                         5433
387                     )
388                 )
389             ),
390             // Different style is different aggregation
391             NotificationMemoryUsage(
392                 "package 1",
393                 384,
394                 "key2",
395                 Notification.Builder(context).setStyle(Notification.BigTextStyle()).build(),
396                 NotificationObjectUsage(
397                     77,
398                     54,
399                     34,
400                     NotificationEnums.STYLE_BIG_TEXT,
401                     77,
402                     432,
403                     2342,
404                     true
405                 ),
406                 listOf(
407                     NotificationViewUsage(ViewType.TOTAL, 3245, 1234, 7653, 543, 765, 7655),
408                     NotificationViewUsage(
409                         ViewType.PRIVATE_CONTRACTED_VIEW,
410                         160,
411                         350,
412                         300,
413                         5544,
414                         66500,
415                         5433
416                     )
417                 )
418             ),
419             // Different package is also different aggregation
420             NotificationMemoryUsage(
421                 "package 2",
422                 684,
423                 "key2",
424                 Notification.Builder(context).setStyle(Notification.BigPictureStyle()).build(),
425                 NotificationObjectUsage(
426                     32,
427                     654,
428                     234,
429                     NotificationEnums.STYLE_BIG_PICTURE,
430                     211,
431                     776,
432                     435,
433                     true
434                 ),
435                 listOf(
436                     NotificationViewUsage(ViewType.TOTAL, 4355, 6543, 4322, 5435, 6546, 65485),
437                     NotificationViewUsage(
438                         ViewType.PRIVATE_CONTRACTED_VIEW,
439                         6546,
440                         7657,
441                         4353,
442                         6546,
443                         76575,
444                         54654
445                     )
446                 )
447             )
448         )
449 }
450