1 /* 2 * Copyright (C) 2024 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.row 18 19 import android.app.compat.CompatChanges 20 import android.content.Context 21 import android.graphics.drawable.AdaptiveIconDrawable 22 import android.graphics.drawable.BitmapDrawable 23 import android.graphics.drawable.Drawable 24 import android.os.Build 25 import android.util.Log 26 import android.view.View 27 import android.view.ViewGroup 28 import android.widget.ImageView 29 import androidx.annotation.VisibleForTesting 30 import com.android.app.tracing.traceSection 31 import com.android.systemui.statusbar.notification.collection.NotificationEntry 32 33 /** Checks whether Notifications with Custom content views conform to configured memory limits. */ 34 object NotificationCustomContentMemoryVerifier { 35 36 private const val NOTIFICATION_SERVICE_TAG = "NotificationService" 37 38 /** Notifications with custom views need to conform to maximum memory consumption. */ 39 @JvmStatic requiresImageViewMemorySizeChecknull40 fun requiresImageViewMemorySizeCheck(entry: NotificationEntry): Boolean { 41 if (!com.android.server.notification.Flags.notificationCustomViewUriRestriction()) { 42 return false 43 } 44 45 return entry.containsCustomViews() 46 } 47 48 /** 49 * This walks the custom view hierarchy contained in the passed Notification view and determines 50 * if the total memory consumption of all image views satisfies the limit set by 51 * [getStripViewSizeLimit]. It will also log to logcat if the limit exceeds 52 * [getWarnViewSizeLimit]. 53 * 54 * @return true if the Notification conforms to the view size limits. 55 */ 56 @JvmStatic satisfiesMemoryLimitsnull57 fun satisfiesMemoryLimits(view: View, entry: NotificationEntry): Boolean { 58 val mainColumnView = 59 view.findViewById<View>(com.android.internal.R.id.notification_main_column) 60 if (mainColumnView == null) { 61 Log.wtf( 62 NOTIFICATION_SERVICE_TAG, 63 "R.id.notification_main_column view should not be null!", 64 ) 65 return true 66 } 67 68 val memorySize = 69 traceSection("computeViewHiearchyImageViewSize") { 70 computeViewHierarchyImageViewSize(view) 71 } 72 73 if (memorySize > getStripViewSizeLimit(view.context)) { 74 val stripOversizedView = isCompatChangeEnabledForUid(entry.sbn.uid) 75 if (stripOversizedView) { 76 Log.w( 77 NOTIFICATION_SERVICE_TAG, 78 "Dropped notification due to too large RemoteViews ($memorySize bytes) on " + 79 "pkg: ${entry.sbn.packageName} tag: ${entry.sbn.tag} id: ${entry.sbn.id}", 80 ) 81 } else { 82 Log.w( 83 NOTIFICATION_SERVICE_TAG, 84 "RemoteViews too large on pkg: ${entry.sbn.packageName} " + 85 "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " + 86 "this WILL notification WILL be dropped when targetSdk " + 87 "is set to ${Build.VERSION_CODES.BAKLAVA}!", 88 ) 89 } 90 91 // We still warn for size, but return "satisfies = ok" if the target SDK 92 // is too low. 93 return !stripOversizedView 94 } 95 96 if (memorySize > getWarnViewSizeLimit(view.context)) { 97 // We emit the same warning as NotificationManagerService does to keep some consistency 98 // for developers. 99 Log.w( 100 NOTIFICATION_SERVICE_TAG, 101 "RemoteViews too large on pkg: ${entry.sbn.packageName} " + 102 "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " + 103 "this notifications might be dropped in a future release", 104 ) 105 } 106 return true 107 } 108 isCompatChangeEnabledForUidnull109 private fun isCompatChangeEnabledForUid(uid: Int): Boolean = 110 try { 111 CompatChanges.isChangeEnabled( 112 NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS, 113 uid, 114 ) 115 } catch (e: RuntimeException) { 116 Log.wtf(NOTIFICATION_SERVICE_TAG, "Failed to contact system_server for compat change.") 117 false 118 } 119 120 @VisibleForTesting 121 @JvmStatic computeViewHierarchyImageViewSizenull122 fun computeViewHierarchyImageViewSize(view: View): Int = 123 when (view) { 124 is ViewGroup -> { 125 var use = 0 126 for (i in 0 until view.childCount) { 127 use += computeViewHierarchyImageViewSize(view.getChildAt(i)) 128 } 129 use 130 } 131 is ImageView -> computeImageViewSize(view) 132 else -> 0 133 } 134 135 /** 136 * Returns the memory size of a Bitmap contained in a passed [ImageView] in bytes. If the view 137 * contains any other kind of drawable, the memory size is estimated from its intrinsic 138 * dimensions. 139 * 140 * @return Bitmap size in bytes or 0 if no drawable is set. 141 */ computeImageViewSizenull142 private fun computeImageViewSize(view: ImageView): Int { 143 val drawable = view.drawable 144 return computeDrawableSize(drawable) 145 } 146 computeDrawableSizenull147 private fun computeDrawableSize(drawable: Drawable?): Int { 148 return when (drawable) { 149 null -> 0 150 is AdaptiveIconDrawable -> 151 computeDrawableSize(drawable.foreground) + 152 computeDrawableSize(drawable.background) + 153 computeDrawableSize(drawable.monochrome) 154 is BitmapDrawable -> drawable.bitmap.allocationByteCount 155 // People can sneak large drawables into those custom memory views via resources - 156 // we use the intrisic size as a proxy for how much memory rendering those will 157 // take. 158 else -> drawable.intrinsicWidth * drawable.intrinsicHeight * 4 159 } 160 } 161 162 /** @return Size of remote views after which a size warning is logged. */ 163 @VisibleForTesting getWarnViewSizeLimitnull164 fun getWarnViewSizeLimit(context: Context): Int = 165 context.resources.getInteger( 166 com.android.internal.R.integer.config_notificationWarnRemoteViewSizeBytes 167 ) 168 169 /** @return Size of remote views after which the notification is dropped. */ 170 @VisibleForTesting 171 fun getStripViewSizeLimit(context: Context): Int = 172 context.resources.getInteger( 173 com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes 174 ) 175 } 176