diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index ef4ad9b088d52..978aaa081d45e 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -68,6 +68,9 @@ public class Flags { public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true); + public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112, + false); + // next id: 112 /***************************************/ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 1aa02951f3f7c..8e646a37a4b3e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification import com.android.systemui.ForegroundServiceNotificationListener import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.people.widget.PeopleSpaceWidgetManager import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationListener @@ -36,6 +38,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Co import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder +import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.phone.CentralSurfaces @@ -68,6 +71,8 @@ class NotificationsControllerImpl @Inject constructor( private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager, private val bubblesOptional: Optional, private val fgsNotifListener: ForegroundServiceNotificationListener, + private val memoryMonitor: Lazy, + private val featureFlags: FeatureFlags ) : NotificationsController { override fun initialize( @@ -107,6 +112,9 @@ class NotificationsControllerImpl @Inject constructor( peopleSpaceWidgetManager.attach(notificationListener) fgsNotifListener.init() + if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) { + memoryMonitor.get().init() + } } // TODO: Convert all functions below this line into listeners instead of public methods diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt new file mode 100644 index 0000000000000..832a739a90804 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt @@ -0,0 +1,41 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +/** Describes usage of a notification. */ +data class NotificationMemoryUsage( + val packageName: String, + val notificationId: String, + val objectUsage: NotificationObjectUsage, +) + +/** + * Describes current memory usage of a [android.app.Notification] object. + * + * The values are in bytes. + */ +data class NotificationObjectUsage( + val smallIcon: Int, + val largeIcon: Int, + val extras: Int, + val style: String?, + val styleIcon: Int, + val bigPicture: Int, + val extender: Int, + val hasCustomView: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt new file mode 100644 index 0000000000000..958978ecd8580 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt @@ -0,0 +1,243 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.app.Person +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import android.util.Log +import androidx.annotation.WorkerThread +import androidx.core.util.contains +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import java.io.PrintWriter +import javax.inject.Inject + +/** This class monitors and logs current Notification memory use. */ +@SysUISingleton +class NotificationMemoryMonitor +@Inject +constructor( + val notificationPipeline: NotifPipeline, + val dumpManager: DumpManager, +) : Dumpable { + + companion object { + private const val TAG = "NotificationMemMonitor" + private const val CAR_EXTENSIONS = "android.car.EXTENSIONS" + private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon" + private const val TV_EXTENSIONS = "android.tv.EXTENSIONS" + private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" + private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" + } + + fun init() { + Log.d(TAG, "NotificationMemoryMonitor initialized.") + dumpManager.registerDumpable(javaClass.simpleName, this) + } + + override fun dump(pw: PrintWriter, args: Array) { + currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) } + } + + @WorkerThread + fun currentNotificationMemoryUse(): List { + return notificationMemoryUse(notificationPipeline.allNotifs) + } + + /** Returns a list of memory use entries for currently shown notifications. */ + @WorkerThread + fun notificationMemoryUse( + notifications: Collection + ): List { + return notifications + .asSequence() + .map { entry -> + val packageName = entry.sbn.packageName + val notificationObjectUsage = + computeNotificationObjectUse(entry.sbn.notification, hashSetOf()) + NotificationMemoryUsage( + packageName, + NotificationUtils.logKey(entry.sbn.key), + notificationObjectUsage + ) + } + .toList() + } + + /** + * Computes the estimated memory usage of a given [Notification] object. It'll attempt to + * inspect Bitmaps in the object and provide summary of memory usage. + */ + private fun computeNotificationObjectUse( + notification: Notification, + seenBitmaps: HashSet + ): NotificationObjectUsage { + val extras = notification.extras + val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps) + val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps) + + // Collect memory usage of extra styles + + // Big Picture + val bigPictureIconUse = + computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) + + computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps) + val bigPictureUse = + computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) + + computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) + + // People + val peopleList = extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST) + val peopleUse = + peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0 + + // Calling + val callingPersonUse = + computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps) + val verificationIconUse = + computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps) + + // Messages + val messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_MESSAGES) + ) + val messagesUse = + messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + val historicMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES) + ) + val historyicMessagesUse = + historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + + // Extenders + val carExtender = extras.getBundle(CAR_EXTENSIONS) + val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0 + val carExtenderIcon = + computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps) + + val tvExtender = extras.getBundle(TV_EXTENSIONS) + val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0 + + val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS) + val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0 + val wearExtenderBackground = + computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) + + val style = notification.notificationStyle + val hasCustomView = notification.contentView != null || notification.bigContentView != null + val extrasSize = computeBundleSize(extras) + + return NotificationObjectUsage( + smallIconUse, + largeIconUse, + extrasSize, + style?.simpleName, + bigPictureIconUse + + peopleUse + + callingPersonUse + + verificationIconUse + + messagesUse + + historyicMessagesUse, + bigPictureUse, + carExtenderSize + + carExtenderIcon + + tvExtenderSize + + wearExtenderSize + + wearExtenderBackground, + hasCustomView + ) + } + + /** + * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem + * bitmaps). Can be slow. + */ + private fun computeBundleSize(extras: Bundle): Int { + val parcel = Parcel.obtain() + try { + extras.writeToParcel(parcel, 0) + return parcel.dataSize() + } finally { + parcel.recycle() + } + } + + /** + * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0 + * if the key does not exist in extras. + */ + private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet): Int { + return when (val parcelable = extras?.getParcelable(key)) { + is Bitmap -> computeBitmapUse(parcelable, seenBitmaps) + is Icon -> computeIconUse(parcelable, seenBitmaps) + is Person -> computeIconUse(parcelable.icon, seenBitmaps) + else -> 0 + } + } + + /** + * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is + * defined via Uri or a resource. + * + * @return memory usage in bytes or 0 if the icon is Uri/Resource based + */ + private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet) = + when (icon?.type) { + Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps) + else -> 0 + } + + /** + * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of + * seenBitmaps set, this method returns 0 to avoid double counting. + * + * @return memory usage of the bitmap in bytes + */ + private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet? = null): Int { + val refId = System.identityHashCode(bitmap) + if (seenBitmaps?.contains(refId) == true) { + return 0 + } + + seenBitmaps?.add(refId) + return bitmap.allocationByteCount + } + + private fun computeDataUse(icon: Icon, seenBitmaps: HashSet): Int { + val refId = System.identityHashCode(icon.dataBytes) + if (seenBitmaps.contains(refId)) { + return 0 + } + + seenBitmaps.add(refId) + return icon.dataLength + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt new file mode 100644 index 0000000000000..16e2441c556bc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt @@ -0,0 +1,321 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.app.PendingIntent +import android.app.Person +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.testing.AndroidTestingRunner +import android.widget.RemoteViews +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationMemoryMonitorTest : SysuiTestCase() { + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun currentNotificationMemoryUse_plainNotification() { + val notification = createBasicNotification().build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3316, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)) + val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = 0, + extras = 3316, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_customViewNotification_marksTrue() { + val notification = + createBasicNotification() + .setCustomContentView( + RemoteViews(context.packageName, android.R.layout.list_content) + ) + .build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3384, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = true, + ) + } + + @Test + fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() { + val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444) + val notification = + createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = 444444, + largeIcon = 0, + extras = 3212, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_bigPictureStyle() { + val bigPicture = + Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888)) + val bigPictureIcon = + Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888)) + val notification = + createBasicNotification() + .setStyle( + Notification.BigPictureStyle() + .bigPicture(bigPicture) + .bigLargeIcon(bigPictureIcon) + ) + .build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 4092, + bigPicture = bigPicture.bitmap.allocationByteCount, + extender = 0, + style = "BigPictureStyle", + styleIcon = bigPictureIcon.bitmap.allocationByteCount, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_callingStyle() { + val personIcon = + Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888)) + val person = Person.Builder().setIcon(personIcon).setName("Person").build() + val fakeIntent = + PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) + val notification = + createBasicNotification() + .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent)) + .build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 4084, + bigPicture = 0, + extender = 0, + style = "CallStyle", + styleIcon = personIcon.bitmap.allocationByteCount, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_messagingStyle() { + val personIcon = + Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888)) + val person = Person.Builder().setIcon(personIcon).setName("Person").build() + val message = Notification.MessagingStyle.Message("Message!", 4323, person) + val historicPersonIcon = + Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888)) + val historicPerson = + Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build() + val historicMessage = + Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson) + + val notification = + createBasicNotification() + .setStyle( + Notification.MessagingStyle(person) + .addMessage(message) + .addHistoricMessage(historicMessage) + ) + .build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 5024, + bigPicture = 0, + extender = 0, + style = "MessagingStyle", + styleIcon = + personIcon.bitmap.allocationByteCount + + historicPersonIcon.bitmap.allocationByteCount, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_carExtender() { + val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888) + val extender = Notification.CarExtender().setLargeIcon(carIcon) + val notification = createBasicNotification().extend(extender).build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3612, + bigPicture = 0, + extender = 556656, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_tvWearExtender() { + val tvExtender = Notification.TvExtender().setChannel("channel2") + val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888) + val wearExtender = Notification.WearableExtender().setBackground(wearBackground) + val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3820, + bigPicture = 0, + extender = 388 + wearBackground.allocationByteCount, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + private fun createBasicNotification(): Notification.Builder { + val smallIcon = + Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888)) + val largeIcon = + Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888)) + return Notification.Builder(context) + .setSmallIcon(smallIcon) + .setLargeIcon(largeIcon) + .setContentTitle("This is a title") + .setContentText("This is content text.") + } + + /** This will generate a nicer error message than comparing objects */ + private fun assertNotificationObjectSizes( + memoryUse: NotificationMemoryUsage, + smallIcon: Int, + largeIcon: Int, + extras: Int, + bigPicture: Int, + extender: Int, + style: String?, + styleIcon: Int, + hasCustomView: Boolean + ) { + assertThat(memoryUse.packageName).isEqualTo("test_pkg") + assertThat(memoryUse.notificationId) + .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0")) + assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon) + assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon) + assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture) + if (style == null) { + assertThat(memoryUse.objectUsage.style).isNull() + } else { + assertThat(memoryUse.objectUsage.style).isEqualTo(style) + } + assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon) + assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView) + } + + private fun getUseObject( + singleItemUseList: List + ): NotificationMemoryUsage { + assertThat(singleItemUseList).hasSize(1) + return singleItemUseList[0] + } + + private fun createNMMWithNotifications( + notifications: List + ): NotificationMemoryMonitor { + val notifPipeline: NotifPipeline = mock() + val notificationEntries = + notifications.map { n -> + NotificationEntryBuilder().setTag("test").setNotification(n).build() + } + whenever(notifPipeline.allNotifs).thenReturn(notificationEntries) + return NotificationMemoryMonitor(notifPipeline, mock()) + } +}