• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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