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

Commit 66132631 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Make custom view stripping behaviour consistent between NMS and SysUI" into main

parents 9e1224b9 5d05c006
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