1 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