Restrict size of custom views on SysUI side as well

Bug: 270553691
Flag: com.android.server.notification.notification_custom_view_uri_restriction
Test: newly added unit tests
Change-Id: I8e222355305098f0a466b41ac51a5252e2a4094a
diff --git a/Android.bp b/Android.bp
index 9d3b64d..303fa2c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -583,6 +583,7 @@
         "documents-ui-compat-config",
         "calendar-provider-compat-config",
         "contacts-provider-platform-compat-config",
+        "SystemUI-core-compat-config",
     ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), {
         "true": [],
         default: [
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 744388f..9c487be 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -207,6 +207,8 @@
         "tests/src/**/systemui/statusbar/notification/row/NotificationConversationInfoTest.java",
         "tests/src/**/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt",
         "tests/src/**/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt",
+        "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java",
+        "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierDisabledTest.java",
         "tests/src/**/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java",
         "tests/src/**/systemui/statusbar/phone/CentralSurfacesImplTest.java",
         "tests/src/**/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java",
@@ -552,6 +554,11 @@
     },
 }
 
+platform_compat_config {
+    name: "SystemUI-core-compat-config",
+    src: ":SystemUI-core",
+}
+
 filegroup {
     name: "AAA-src",
     srcs: ["tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java"],
@@ -754,6 +761,7 @@
         "kosmos",
         "testables",
         "androidx.test.rules",
+        "platform-compat-test-rules",
     ],
     libs: [
         "android.test.runner.stubs.system",
@@ -888,6 +896,7 @@
     static_libs: [
         "RoboTestLibraries",
         "androidx.compose.runtime_runtime",
+        "platform-compat-test-rules",
     ],
     libs: [
         "android.test.runner.impl",
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 09cc3f2..9dc651e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -643,6 +643,10 @@
         return row.isMediaRow();
     }
 
+    public boolean containsCustomViews() {
+        return getSbn().getNotification().containsCustomViews();
+    }
+
     public void resetUserExpansion() {
         if (row != null) row.resetUserExpansion();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
index 6491223..f9e9bee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -12,7 +12,7 @@
 import com.android.systemui.util.children
 
 /** Walks view hiearchy of a given notification to estimate its memory use. */
-internal object NotificationMemoryViewWalker {
+object NotificationMemoryViewWalker {
 
     private const val TAG = "NotificationMemory"
 
@@ -26,9 +26,13 @@
         private var softwareBitmaps = 0
 
         fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse }
+
         fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse }
+
         fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse }
+
         fun addStyle(styleUse: Int) = apply { style += styleUse }
+
         fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply {
             softwareBitmaps += softwareBitmapUse
         }
@@ -67,14 +71,14 @@
                     getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
                     getViewUsage(
                         ViewType.PRIVATE_CONTRACTED_VIEW,
-                        row.privateLayout?.contractedChild
+                        row.privateLayout?.contractedChild,
                     ),
                     getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
                     getViewUsage(
                         ViewType.PUBLIC_VIEW,
                         row.publicLayout?.expandedChild,
                         row.publicLayout?.contractedChild,
-                        row.publicLayout?.headsUpChild
+                        row.publicLayout?.headsUpChild,
                     ),
                 )
                 .filterNotNull()
@@ -107,14 +111,14 @@
             row.publicLayout?.expandedChild,
             row.publicLayout?.contractedChild,
             row.publicLayout?.headsUpChild,
-            seenObjects = seenObjects
+            seenObjects = seenObjects,
         )
     }
 
     private fun getViewUsage(
         type: ViewType,
         vararg rootViews: View?,
-        seenObjects: HashSet<Int> = hashSetOf()
+        seenObjects: HashSet<Int> = hashSetOf(),
     ): NotificationViewUsage? {
         val usageBuilder = lazy { UsageBuilder() }
         rootViews.forEach { rootView ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index c7e15fd..73e8246 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -901,6 +901,13 @@
         if (!satisfiesMinHeightRequirement(view, entry, resources)) {
             return "inflated notification does not meet minimum height requirement";
         }
+
+        if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
+            if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
+                return "inflated notification does not meet maximum memory size requirement";
+            }
+        }
+
         return null;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java
new file mode 100644
index 0000000..c55cb67
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 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.row;
+
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
+import android.os.Build;
+
+/**
+ * Holds compat {@link ChangeId} for {@link NotificationCustomContentMemoryVerifier}.
+ */
+final class NotificationCustomContentCompat {
+    /**
+     * Enables memory size checking of custom views included in notifications to ensure that
+     * they conform to the size limit set in `config_notificationStripRemoteViewSizeBytes`
+     * config.xml parameter.
+     * Notifications exceeding the size will be rejected.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
+    public static final long CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS = 270553691L;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt
new file mode 100644
index 0000000..a3e6a5c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2024 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.row
+
+import android.app.compat.CompatChanges
+import android.content.Context
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.traceSection
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+/** Checks whether Notifications with Custom content views conform to configured memory limits. */
+object NotificationCustomContentMemoryVerifier {
+
+    private const val NOTIFICATION_SERVICE_TAG = "NotificationService"
+
+    /** Notifications with custom views need to conform to maximum memory consumption. */
+    @JvmStatic
+    fun requiresImageViewMemorySizeCheck(entry: NotificationEntry): Boolean {
+        if (!com.android.server.notification.Flags.notificationCustomViewUriRestriction()) {
+            return false
+        }
+
+        return entry.containsCustomViews()
+    }
+
+    /**
+     * This walks the custom view hierarchy contained in the passed Notification view and determines
+     * if the total memory consumption of all image views satisfies the limit set by
+     * [getStripViewSizeLimit]. It will also log to logcat if the limit exceeds
+     * [getWarnViewSizeLimit].
+     *
+     * @return true if the Notification conforms to the view size limits.
+     */
+    @JvmStatic
+    fun satisfiesMemoryLimits(view: View, entry: NotificationEntry): Boolean {
+        val mainColumnView =
+            view.findViewById<View>(com.android.internal.R.id.notification_main_column)
+        if (mainColumnView == null) {
+            Log.wtf(
+                NOTIFICATION_SERVICE_TAG,
+                "R.id.notification_main_column view should not be null!",
+            )
+            return true
+        }
+
+        val memorySize =
+            traceSection("computeViewHiearchyImageViewSize") {
+                computeViewHierarchyImageViewSize(view)
+            }
+
+        if (memorySize > getStripViewSizeLimit(view.context)) {
+            val stripOversizedView = isCompatChangeEnabledForUid(entry.sbn.uid)
+            if (stripOversizedView) {
+                Log.w(
+                    NOTIFICATION_SERVICE_TAG,
+                    "Dropped notification due to too large RemoteViews ($memorySize bytes) on " +
+                        "pkg: ${entry.sbn.packageName} tag: ${entry.sbn.tag} id: ${entry.sbn.id}",
+                )
+            } else {
+                Log.w(
+                    NOTIFICATION_SERVICE_TAG,
+                    "RemoteViews too large on pkg: ${entry.sbn.packageName} " +
+                        "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " +
+                        "this WILL notification WILL be dropped when targetSdk " +
+                        "is set to ${Build.VERSION_CODES.BAKLAVA}!",
+                )
+            }
+
+            // We still warn for size, but return "satisfies = ok" if the target SDK
+            // is too low.
+            return !stripOversizedView
+        }
+
+        if (memorySize > getWarnViewSizeLimit(view.context)) {
+            // We emit the same warning as NotificationManagerService does to keep some consistency
+            // for developers.
+            Log.w(
+                NOTIFICATION_SERVICE_TAG,
+                "RemoteViews too large on pkg: ${entry.sbn.packageName} " +
+                    "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " +
+                    "this notifications might be dropped in a future release",
+            )
+        }
+        return true
+    }
+
+    private fun isCompatChangeEnabledForUid(uid: Int): Boolean =
+        try {
+            CompatChanges.isChangeEnabled(
+                NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS,
+                uid,
+            )
+        } catch (e: RuntimeException) {
+            Log.wtf(NOTIFICATION_SERVICE_TAG, "Failed to contact system_server for compat change.")
+            false
+        }
+
+    @VisibleForTesting
+    @JvmStatic
+    fun computeViewHierarchyImageViewSize(view: View): Int =
+        when (view) {
+            is ViewGroup -> {
+                var use = 0
+                for (i in 0 until view.childCount) {
+                    use += computeViewHierarchyImageViewSize(view.getChildAt(i))
+                }
+                use
+            }
+            is ImageView -> computeImageViewSize(view)
+            else -> 0
+        }
+
+    /**
+     * Returns the memory size of a Bitmap contained in a passed [ImageView] in bytes. If the view
+     * contains any other kind of drawable, the memory size is estimated from its intrinsic
+     * dimensions.
+     *
+     * @return Bitmap size in bytes or 0 if no drawable is set.
+     */
+    private fun computeImageViewSize(view: ImageView): Int {
+        val drawable = view.drawable
+        return computeDrawableSize(drawable)
+    }
+
+    private fun computeDrawableSize(drawable: Drawable?): Int {
+        return when (drawable) {
+            null -> 0
+            is AdaptiveIconDrawable ->
+                computeDrawableSize(drawable.foreground) +
+                    computeDrawableSize(drawable.background) +
+                    computeDrawableSize(drawable.monochrome)
+            is BitmapDrawable -> drawable.bitmap.allocationByteCount
+            // People can sneak large drawables into those custom memory views via resources -
+            // we use the intrisic size as a proxy for how much memory rendering those will
+            // take.
+            else -> drawable.intrinsicWidth * drawable.intrinsicHeight * 4
+        }
+    }
+
+    /** @return Size of remote views after which a size warning is logged. */
+    @VisibleForTesting
+    fun getWarnViewSizeLimit(context: Context): Int =
+        context.resources.getInteger(
+            com.android.internal.R.integer.config_notificationWarnRemoteViewSizeBytes
+        )
+
+    /** @return Size of remote views after which the notification is dropped. */
+    @VisibleForTesting
+    fun getStripViewSizeLimit(context: Context): Int =
+        context.resources.getInteger(
+            com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index bc3653a..fda5e74 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -1393,9 +1393,17 @@
          */
         @VisibleForTesting
         fun isValidView(view: View, entry: NotificationEntry, resources: Resources): String? {
-            return if (!satisfiesMinHeightRequirement(view, entry, resources)) {
-                "inflated notification does not meet minimum height requirement"
-            } else null
+            if (!satisfiesMinHeightRequirement(view, entry, resources)) {
+                return "inflated notification does not meet minimum height requirement"
+            }
+
+            if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
+                if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
+                    return "inflated notification does not meet maximum memory size requirement"
+                }
+            }
+
+            return null
         }
 
         private fun satisfiesMinHeightRequirement(
diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml
new file mode 100644
index 0000000..eb3ba82
--- /dev/null
+++ b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <ViewFlipper
+        android:id="@+id/flipper"
+        android:layout_width="match_parent"
+        android:layout_height="400dp"
+        android:flipInterval="1000"
+        />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml
new file mode 100644
index 0000000..e2a00bd
--- /dev/null
+++ b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/imageview"
+    android:layout_width="match_parent"
+    android:layout_height="400dp" />
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java
new file mode 100644
index 0000000..09fa387
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 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.row;
+
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.DisableFlags;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.notification.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NotificationCustomContentMemoryVerifierFlagDisabledTest extends SysuiTestCase {
+
+    @Rule
+    public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+    @Test
+    @DisableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION)
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS
+    })
+    public void requiresImageViewMemorySizeCheck_flagDisabled_returnsFalse() {
+        NotificationEntry entry = buildAcceptableNotificationEntry(mContext);
+        assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+                .isFalse();
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java
new file mode 100644
index 0000000..1cadb3c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2025 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.row;
+
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotification;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildOversizedNotification;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildWarningSizedNotification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.platform.test.annotations.EnableFlags;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.notification.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@EnableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION)
+public class NotificationCustomContentMemoryVerifierTest extends SysuiTestCase {
+
+    private static final String AUTHORITY = "notification.memory.test.authority";
+    private static final Uri TEST_URI = new Uri.Builder()
+            .scheme("content")
+            .authority(AUTHORITY)
+            .path("path")
+            .build();
+
+    @Rule
+    public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+    @Before
+    public void setUp() {
+        TestImageContentProvider provider = new TestImageContentProvider(mContext);
+        mContext.getContentResolver().addProvider(AUTHORITY, provider);
+        provider.onCreate();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void requiresImageViewMemorySizeCheck_customViewNotification_returnsTrue() {
+        NotificationEntry entry =
+                buildAcceptableNotificationEntry(
+                        mContext);
+        assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+                .isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void requiresImageViewMemorySizeCheck_plainNotification_returnsFalse() {
+        Notification notification =
+                new Notification.Builder(mContext, "ChannelId")
+                        .setContentTitle("Just a notification")
+                        .setContentText("Yep")
+                        .build();
+        NotificationEntry entry = new NotificationEntryBuilder().setNotification(
+                notification).build();
+        assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+                .isFalse();
+    }
+
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void satisfiesMemoryLimits_smallNotification_returnsTrue() {
+        Notification.Builder notification =
+                buildAcceptableNotification(mContext,
+                        TEST_URI);
+        NotificationEntry entry = toEntry(notification);
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+        )
+                .isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void satisfiesMemoryLimits_oversizedNotification_returnsFalse() {
+        Notification.Builder notification =
+                buildOversizedNotification(mContext,
+                        TEST_URI);
+        NotificationEntry entry = toEntry(notification);
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+        ).isFalse();
+    }
+
+    @Test
+    @DisableCompatChanges(
+            {NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}
+    )
+    public void satisfiesMemoryLimits_oversizedNotification_compatDisabled_returnsTrue() {
+        Notification.Builder notification =
+                buildOversizedNotification(mContext,
+                        TEST_URI);
+        NotificationEntry entry = toEntry(notification);
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+        ).isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void satisfiesMemoryLimits_warningSizedNotification_returnsTrue() {
+        Notification.Builder notification =
+                buildWarningSizedNotification(mContext,
+                        TEST_URI);
+        NotificationEntry entry = toEntry(notification);
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+        )
+                .isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void satisfiesMemoryLimits_viewWithoutCustomNotificationRoot_returnsTrue() {
+        NotificationEntry entry = new NotificationEntryBuilder().build();
+        View view = new FrameLayout(mContext);
+        assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry))
+                .isTrue();
+    }
+
+    @Test
+    @EnableCompatChanges({
+            NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+    public void computeViewHierarchyImageViewSize_smallNotification_returnsSensibleValue() {
+        Notification.Builder notification =
+                buildAcceptableNotification(mContext,
+                        TEST_URI);
+        // This should have a size of a single image
+        View inflatedView = inflateNotification(notification);
+        assertThat(
+                NotificationCustomContentMemoryVerifier.computeViewHierarchyImageViewSize(
+                        inflatedView))
+                .isGreaterThan(170000);
+    }
+
+    private View inflateNotification(Notification.Builder builder) {
+        RemoteViews remoteViews = builder.createBigContentView();
+        return remoteViews.apply(mContext, new FrameLayout(mContext));
+    }
+
+    private NotificationEntry toEntry(Notification.Builder builder) {
+        return new NotificationEntryBuilder().setNotification(builder.build())
+                .setUid(Process.myUid()).build();
+    }
+
+
+    /** This provider serves the images for inflation. */
+    class TestImageContentProvider extends ContentProvider {
+
+        TestImageContentProvider(Context context) {
+            ProviderInfo info = new ProviderInfo();
+            info.authority = AUTHORITY;
+            info.exported = true;
+            attachInfoForTesting(context, info);
+            setAuthorities(AUTHORITY);
+        }
+
+        @Override
+        public boolean onCreate() {
+            return true;
+        }
+
+        @Override
+        public ParcelFileDescriptor openFile(Uri uri, String mode) {
+            return getContext().getResources().openRawResourceFd(
+                    NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE())
+                        .getParcelFileDescriptor();
+        }
+
+        @Override
+        public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) {
+            return getContext().getResources().openRawResourceFd(
+                    NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE());
+        }
+
+        @Override
+        public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
+                CancellationSignal signal) throws FileNotFoundException {
+            return openTypedAssetFile(uri, mimeTypeFilter, opts);
+        }
+
+        @Override
+        public int delete(Uri uri, Bundle extras) {
+            return 0;
+        }
+
+        @Override
+        public int delete(Uri uri, String selection, String[] selectionArgs) {
+            return 0;
+        }
+
+        @Override
+        public String getType(Uri uri) {
+            return "image/png";
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values) {
+            return null;
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values, Bundle extras) {
+            return super.insert(uri, values, extras);
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, Bundle queryArgs,
+                CancellationSignal cancellationSignal) {
+            return super.query(uri, projection, queryArgs, cancellationSignal);
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            return null;
+        }
+
+        @Override
+        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+            return 0;
+        }
+    }
+
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt
new file mode 100644
index 0000000..ca4f24d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 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
+ */
+
+@file:JvmName("NotificationCustomContentNotificationBuilder")
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Notification.DecoratedCustomViewStyle
+import android.content.Context
+import android.graphics.drawable.BitmapDrawable
+import android.net.Uri
+import android.os.Process
+import android.widget.RemoteViews
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.tests.R
+import org.hamcrest.Matchers.lessThan
+import org.junit.Assume.assumeThat
+
+public val DRAWABLE_IMAGE_RESOURCE = R.drawable.romainguy_rockaway
+
+fun buildAcceptableNotificationEntry(context: Context): NotificationEntry {
+    return NotificationEntryBuilder()
+        .setNotification(buildAcceptableNotification(context, null).build())
+        .setUid(Process.myUid())
+        .build()
+}
+
+fun buildAcceptableNotification(context: Context, uri: Uri?): Notification.Builder =
+    buildNotification(context, uri, 1)
+
+fun buildOversizedNotification(context: Context, uri: Uri): Notification.Builder {
+    val numImagesForOversize =
+        (NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context) /
+            drawableSizeOnDevice(context)) + 2
+    return buildNotification(context, uri, numImagesForOversize)
+}
+
+fun buildWarningSizedNotification(context: Context, uri: Uri): Notification.Builder {
+    val numImagesForOversize =
+        (NotificationCustomContentMemoryVerifier.getWarnViewSizeLimit(context) /
+            drawableSizeOnDevice(context)) + 1
+    // The size needs to be smaller than outright stripping size.
+    assumeThat(
+        numImagesForOversize * drawableSizeOnDevice(context),
+        lessThan(NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context)),
+    )
+    return buildNotification(context, uri, numImagesForOversize)
+}
+
+fun buildNotification(context: Context, uri: Uri?, numImages: Int): Notification.Builder {
+    val remoteViews = RemoteViews(context.packageName, R.layout.custom_view_flipper)
+    repeat(numImages) { i ->
+        val remoteViewFlipperImageView =
+            RemoteViews(context.packageName, R.layout.custom_view_flipper_image)
+
+        if (uri == null) {
+            remoteViewFlipperImageView.setImageViewResource(
+                R.id.imageview,
+                R.drawable.romainguy_rockaway,
+            )
+        } else {
+            val imageUri = uri.buildUpon().appendPath(i.toString()).build()
+            remoteViewFlipperImageView.setImageViewUri(R.id.imageview, imageUri)
+        }
+        remoteViews.addView(R.id.flipper, remoteViewFlipperImageView)
+    }
+
+    return Notification.Builder(context, "ChannelId")
+        .setSmallIcon(android.R.drawable.ic_info)
+        .setStyle(DecoratedCustomViewStyle())
+        .setCustomContentView(remoteViews)
+        .setCustomBigContentView(remoteViews)
+        .setContentTitle("This is a remote view!")
+}
+
+fun drawableSizeOnDevice(context: Context): Int {
+    val drawable = context.resources.getDrawable(DRAWABLE_IMAGE_RESOURCE)
+    return (drawable as BitmapDrawable).bitmap.allocationByteCount
+}