<lambda>null1 package com.android.systemui.statusbar.notification.logging
2 
3 import android.graphics.Bitmap
4 import android.graphics.drawable.BitmapDrawable
5 import android.graphics.drawable.Drawable
6 import android.util.Log
7 import android.view.View
8 import android.view.ViewGroup
9 import android.widget.ImageView
10 import com.android.internal.R
11 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
12 import com.android.systemui.util.children
13 
14 /** Walks view hiearchy of a given notification to estimate its memory use. */
15 internal object NotificationMemoryViewWalker {
16 
17     private const val TAG = "NotificationMemory"
18 
19     /** Builder for [NotificationViewUsage] objects. */
20     private class UsageBuilder {
21         private var smallIcon: Int = 0
22         private var largeIcon: Int = 0
23         private var systemIcons: Int = 0
24         private var style: Int = 0
25         private var customViews: Int = 0
26         private var softwareBitmaps = 0
27 
28         fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse }
29         fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse }
30         fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse }
31         fun addStyle(styleUse: Int) = apply { style += styleUse }
32         fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply {
33             softwareBitmaps += softwareBitmapUse
34         }
35 
36         fun addCustomViews(customViewsUse: Int) = apply { customViews += customViewsUse }
37 
38         fun build(viewType: ViewType): NotificationViewUsage {
39             return NotificationViewUsage(
40                 viewType = viewType,
41                 smallIcon = smallIcon,
42                 largeIcon = largeIcon,
43                 systemIcons = systemIcons,
44                 style = style,
45                 customViews = customViews,
46                 softwareBitmapsPenalty = softwareBitmaps,
47             )
48         }
49     }
50 
51     /**
52      * Returns memory usage of public and private views contained in passed
53      * [ExpandableNotificationRow]. Each entry will correspond to one of the [ViewType] values with
54      * [ViewType.TOTAL] totalling all memory use. If a type of view is missing, the corresponding
55      * entry will not appear in resulting list.
56      *
57      * This will return an empty list if the ExpandableNotificationRow has no views inflated.
58      */
59     fun getViewUsage(row: ExpandableNotificationRow?): List<NotificationViewUsage> {
60         if (row == null) {
61             return listOf()
62         }
63 
64         // The ordering here is significant since it determines deduplication of seen drawables.
65         val perViewUsages =
66             listOf(
67                     getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
68                     getViewUsage(
69                         ViewType.PRIVATE_CONTRACTED_VIEW,
70                         row.privateLayout?.contractedChild
71                     ),
72                     getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
73                     getViewUsage(
74                         ViewType.PUBLIC_VIEW,
75                         row.publicLayout?.expandedChild,
76                         row.publicLayout?.contractedChild,
77                         row.publicLayout?.headsUpChild
78                     ),
79                 )
80                 .filterNotNull()
81 
82         return if (perViewUsages.isNotEmpty()) {
83             // Attach summed totals field only if there was any view actually measured.
84             // This reduces bug report noise and makes checks for collapsed views easier.
85             val totals = getTotalUsage(row)
86             if (totals == null) {
87                 perViewUsages
88             } else {
89                 perViewUsages + totals
90             }
91         } else {
92             listOf()
93         }
94     }
95 
96     /**
97      * Calculate total usage of all views - we need to do a separate traversal to make sure we don't
98      * double count fields.
99      */
100     private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage? {
101         val seenObjects = hashSetOf<Int>()
102         return getViewUsage(
103             ViewType.TOTAL,
104             row.privateLayout?.expandedChild,
105             row.privateLayout?.contractedChild,
106             row.privateLayout?.headsUpChild,
107             row.publicLayout?.expandedChild,
108             row.publicLayout?.contractedChild,
109             row.publicLayout?.headsUpChild,
110             seenObjects = seenObjects
111         )
112     }
113 
114     private fun getViewUsage(
115         type: ViewType,
116         vararg rootViews: View?,
117         seenObjects: HashSet<Int> = hashSetOf()
118     ): NotificationViewUsage? {
119         val usageBuilder = lazy { UsageBuilder() }
120         rootViews.forEach { rootView ->
121             (rootView as? ViewGroup)?.let { rootViewGroup ->
122                 computeViewHierarchyUse(rootViewGroup, usageBuilder.value, seenObjects)
123             }
124         }
125 
126         return if (usageBuilder.isInitialized()) {
127             usageBuilder.value.build(type)
128         } else {
129             null
130         }
131     }
132 
133     private fun computeViewHierarchyUse(
134         rootView: ViewGroup,
135         builder: UsageBuilder,
136         seenObjects: HashSet<Int> = hashSetOf(),
137     ) {
138         for (child in rootView.children) {
139             if (child is ViewGroup) {
140                 computeViewHierarchyUse(child, builder, seenObjects)
141             } else {
142                 computeViewUse(child, builder, seenObjects)
143             }
144         }
145     }
146 
147     private fun computeViewUse(view: View, builder: UsageBuilder, seenObjects: HashSet<Int>) {
148         if (view !is ImageView) return
149         val drawable = view.drawable ?: return
150         val drawableRef = System.identityHashCode(drawable)
151         if (seenObjects.contains(drawableRef)) return
152         val drawableUse = computeDrawableUse(drawable, seenObjects)
153         // TODO(b/235451049): We need to make sure we traverse large icon before small icon -
154         // sometimes the large icons are assigned to small icon views and we want to
155         // attribute them to large view in those cases.
156         when (view.id) {
157             R.id.left_icon,
158             R.id.icon,
159             R.id.conversation_icon -> builder.addSmallIcon(drawableUse)
160             R.id.right_icon -> builder.addLargeIcon(drawableUse)
161             R.id.big_picture -> builder.addStyle(drawableUse)
162             // Elements that are part of platform with resources
163             R.id.phishing_alert,
164             R.id.feedback,
165             R.id.alerted_icon,
166             R.id.expand_button_icon,
167             R.id.remote_input_send -> builder.addSystem(drawableUse)
168             // Custom view ImageViews
169             else -> {
170                 if (Log.isLoggable(TAG, Log.DEBUG)) {
171                     Log.d(TAG, "Custom view: ${identifierForView(view)}")
172                 }
173                 builder.addCustomViews(drawableUse)
174             }
175         }
176 
177         if (isDrawableSoftwareBitmap(drawable)) {
178             builder.addSoftwareBitmapPenalty(drawableUse)
179         }
180 
181         seenObjects.add(drawableRef)
182     }
183 
184     private fun computeDrawableUse(drawable: Drawable, seenObjects: HashSet<Int>): Int =
185         when (drawable) {
186             is BitmapDrawable -> {
187                 drawable.bitmap?.let {
188                     val ref = System.identityHashCode(it)
189                     if (seenObjects.contains(ref)) {
190                         0
191                     } else {
192                         seenObjects.add(ref)
193                         it.allocationByteCount
194                     }
195                 } ?: 0
196             }
197             else -> 0
198         }
199 
200     private fun isDrawableSoftwareBitmap(drawable: Drawable) =
201         drawable is BitmapDrawable && drawable.bitmap?.config != Bitmap.Config.HARDWARE
202 
203     private fun identifierForView(view: View) =
204         if (view.id == View.NO_ID) {
205             "no-id"
206         } else {
207             view.resources.getResourceName(view.id)
208         }
209 }
210