Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5d05c006 authored by Jernej Virag's avatar Jernej Virag
Browse files

Make custom view stripping behaviour consistent between NMS and SysUI

Right now SysUI cancels the oversized notification while NMS sends a
standard temoplate anyway. Losing notifications is bad, so this makes
SysUI behaviour consistent with NMS and repost oversized custom view
notifications as standard template ones.

Bug: 270553691
Flag:com.android.server.notification.notification_custom_view_uri_restriction
Test: manual test with test app + newly added unit tests

Change-Id: I0ab7d668c982b34331bf6836592021f1b0be0bef
parent 160e1c04
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
/*
 * 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

/** Thrown when a custom notification view exceeds memory limit. */
class CustomViewMemorySizeExceededException(error: String) : InflationException(error)
+45 −27
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import androidx.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.statusbar.notification.CustomViewMemorySizeExceededException;
import com.android.systemui.statusbar.notification.collection.BundleEntry;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
@@ -256,6 +257,22 @@ public class PreparationCoordinator implements Coordinator {
            new NotifInflationErrorListener() {
                @Override
                public void onNotifInflationError(NotificationEntry entry, Exception e) {
                    // If the notification views exceeded their memory restriction, we strip
                    // it down to the basic template and reinflate it in that basic form.
                    if (e instanceof CustomViewMemorySizeExceededException
                            // Prevent endless loop if no custom views are present.
                            && entry.containsCustomViews()) {
                        // "lighten" strips out all notification custom views, large bitmaps and
                        // other extras.
                        entry.getSbn().getNotification().lightenPayload();
                        // Clear the error state and trigger reinflation of changed notification.
                        mNotifErrorManager.clearInflationError(entry);
                        mNotifCollectionListener.onEntryUpdated(entry);
                        mNotifInflationErrorFilter.invalidateList(
                                "reinflate for MemorySizeExceeded for " + logKey(entry));
                        return;
                    }

                    mViewBarn.removeViewForEntry(entry);
                    mInflationStates.put(entry, STATE_ERROR);
                    try {
@@ -273,7 +290,8 @@ public class PreparationCoordinator implements Coordinator {
                    } catch (RemoteException ex) {
                        // System server is dead, nothing to do about that
                    }
            mNotifInflationErrorFilter.invalidateList("onNotifInflationError for " + logKey(entry));
                    mNotifInflationErrorFilter.invalidateList(
                            "onNotifInflationError for " + logKey(entry));
                }

                @Override
+36 −6
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import android.widget.RemoteViews;
import com.android.app.tracing.TraceUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.ImageMessageConsumer;
import com.android.server.notification.Flags;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.NotifInflation;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
@@ -54,6 +55,7 @@ import com.android.systemui.res.R;
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
import com.android.systemui.statusbar.notification.CustomViewMemorySizeExceededException;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.NmSummarizationUiFlag;
import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -834,6 +836,34 @@ public class NotificationContentInflater implements NotificationRowContentBinder

            @Override
            public void onViewApplied(View v) {
                if (Flags.notificationCustomViewUriRestriction()) {
                    onViewAppliedUsingException(v);
                } else {
                    onViewAppliedLegacy(v);
                }
            }

            private void onViewAppliedUsingException(View v) {
                try {
                    validateView(v, entry, row.getResources());
                    if (isNewView) {
                        applyCallback.setResultView(v);
                    } else if (existingWrapper != null) {
                        existingWrapper.onReinflated();
                    }
                } catch (InflationException e) {
                    runningInflations.remove(inflationId);
                    handleInflationError(runningInflations, e,
                            row, entry, callback, logger, "applied invalid view");
                    return;
                }
                runningInflations.remove(inflationId);
                finishIfDone(result, isMinimized,
                        reInflateFlags, remoteViewCache, runningInflations,
                        callback, entry, row, logger);
            }

            private void onViewAppliedLegacy(View v) {
                String invalidReason = isValidView(v, entry, row.getResources());
                if (invalidReason != null) {
                    handleInflationError(runningInflations, new InflationException(invalidReason),
@@ -912,12 +942,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder
            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;
    }

@@ -966,6 +990,12 @@ public class NotificationContentInflater implements NotificationRowContentBinder
        if (invalidReason != null) {
            throw new InflationException(invalidReason);
        }

        if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
            if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
                throw new CustomViewMemorySizeExceededException("Custom view memory size exceeded");
            }
        }
    }

    private static void handleInflationError(
+59 −11
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import android.widget.RemoteViews.OnViewAppliedListener
import com.android.app.tracing.TraceUtils
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.widget.ImageMessageConsumer
import com.android.server.notification.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.NotifInflation
import com.android.systemui.res.R
@@ -47,6 +48,7 @@ import com.android.systemui.statusbar.InflationTask
import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP
import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.CustomViewMemorySizeExceededException
import com.android.systemui.statusbar.notification.InflationException
import com.android.systemui.statusbar.notification.NmSummarizationUiFlag
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -525,7 +527,7 @@ constructor(
                            entry,
                            row,
                            remoteViewClickHandler,
                            this /* callback */,
                            /* callback= */ this,
                            logger,
                        )
                }
@@ -543,7 +545,10 @@ constructor(
            Log.e(TAG, "couldn't inflate view for notification $ident", e)
            callback?.handleInflationException(
                if (NotificationBundleUi.isEnabled) entry else row.entryLegacy,
                InflationException("Couldn't inflate contentViews$e"),
                when (e) {
                    is InflationException -> e
                    else -> InflationException("Couldn't inflate contentViews: $e")
                },
            )

            // Cancel any image loading tasks, not useful any more
@@ -1318,6 +1323,14 @@ constructor(
                    }

                    override fun onViewApplied(v: View) {
                        if (Flags.notificationCustomViewUriRestriction()) {
                            onViewAppliedUsingException(v)
                        } else {
                            onViewAppliedLegacy(v)
                        }
                    }

                    private fun onViewAppliedLegacy(v: View) {
                        val invalidReason = isValidView(v, entry, row.resources)
                        if (invalidReason != null) {
                            handleInflationError(
@@ -1351,11 +1364,46 @@ constructor(
                        )
                    }

                    private fun onViewAppliedUsingException(v: View) {
                        try {
                            validateView(v, entry, row.resources)
                            if (isNewView) {
                                applyCallback.setResultView(v)
                            } else {
                                existingWrapper?.onReinflated()
                            }
                        } catch (e: InflationException) {
                            runningInflations.remove(inflationId)
                            handleInflationError(
                                runningInflations,
                                e,
                                row,
                                entry,
                                callback,
                                logger,
                                "applied invalid view",
                            )
                            return
                        }

                        runningInflations.remove(inflationId)
                        finishIfDone(
                            result,
                            isMinimized,
                            reInflateFlags,
                            remoteViewCache,
                            runningInflations,
                            callback,
                            entry,
                            row,
                            logger,
                        )
                    }

                    override fun onError(e: Exception) {
                        // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this
                        // could
                        // actually also be a system issue, so let's try on the UI thread again to
                        // be safe.
                        // could actually also be a system issue, so let's try on the UI thread
                        // again to be safe.
                        try {
                            val newView =
                                if (isNewView) {
@@ -1424,12 +1472,6 @@ constructor(
                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
        }

@@ -1482,6 +1524,12 @@ constructor(
            if (invalidReason != null) {
                throw InflationException(invalidReason)
            }

            if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
                if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
                    throw CustomViewMemorySizeExceededException("Custom view memory size exceeded")
                }
            }
        }

        private fun handleInflationError(
+67 −0
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE;
import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -39,10 +41,12 @@ import static java.util.Objects.requireNonNull;

import android.app.Flags;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.RemoteException;
import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper;
import android.widget.RemoteViews;

import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -53,6 +57,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.notification.CustomViewMemorySizeExceededException;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder;
import com.android.systemui.statusbar.notification.collection.ListEntry;
@@ -84,6 +89,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

@@ -226,6 +232,67 @@ public class PreparationCoordinatorTest extends SysuiTestCase {
        assertTrue(mInflationErrorFilter.shouldFilterOut(mEntry, 0));
    }

    @Test
    public void testMemorySizeExceeded_reinflatesStandardTemplate() {
        NotificationEntryBuilder eb = getNotificationEntryBuilder()
                .setParent(ROOT_ENTRY);
        eb.modifyNotification(mContext)
                .setLargeIcon(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888))
                .setCustomContentView(
                        new RemoteViews(mContext.getPackageName(), android.R.layout.list_content))
                .build();
        NotificationEntry entry = eb.build();
        // Preconditions check.
        assertThat(entry.getSbn().getNotification().getLargeIcon()).isNotNull();
        assertThat(entry.getSbn().getNotification().contentView).isNotNull();

        mCollectionListener.onEntryInit(entry);
        mErrorManager.setInflationError(entry,
                new CustomViewMemorySizeExceededException("Exception"));
        Mockito.reset(mNotifInflater);
        // Trigger reinflation.
        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(entry));

        // Verify that the notification was stripped.
        assertThat(entry.getSbn().getNotification().getLargeIcon()).isNull();
        assertThat(entry.getSbn().getNotification().contentView).isNull();
        // We'll reinflate the notification so DO NOT call the NMS with error report.
        verifyNoMoreInteractions(mService);
        // Notification should be reinflated.
        verify(mNotifInflater).inflateViews(eq(entry), any(), any());
        // And NOT skipped by error filter.
        assertThat(mInflationErrorFilter.shouldFilterOut(entry, 0)).isFalse();
    }

    // Prevent endless reinflations if the notification doesn't have custom views.
    @Test
    public void testMemorySizeExceeded_dontReinflateNotificationsWithoutCustomViews()
            throws RemoteException {
        NotificationEntryBuilder eb = getNotificationEntryBuilder()
                .setParent(ROOT_ENTRY);
        eb.modifyNotification(mContext)
                .setLargeIcon(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888))
                .build();
        NotificationEntry entry = eb.build();
        // Preconditions check.
        assertThat(entry.getSbn().getNotification().getLargeIcon()).isNotNull();

        Exception exception = new CustomViewMemorySizeExceededException("Exception");
        mCollectionListener.onEntryInit(entry);
        mErrorManager.setInflationError(entry, exception);
        Mockito.reset(mNotifInflater);
        // Verify that NMS was signaled with error and no reinflation was attempted.
        verify(mService).onNotificationError(
                eq(mEntry.getSbn().getPackageName()),
                eq(mEntry.getSbn().getTag()),
                eq(mEntry.getSbn().getId()),
                eq(mEntry.getSbn().getUid()),
                eq(mEntry.getSbn().getInitialPid()),
                eq(exception.getMessage()),
                eq(mEntry.getSbn().getUser().getIdentifier()));
        verifyNoMoreInteractions(mNotifInflater);
    }

    @Test
    @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_APP_ICONS)
    public void testPurgesAppIconProviderCache() {
Loading