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

Commit b681ffe9 authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Strip large bitmaps from remoteviews

Test: atest
Bug: 136496704
Change-Id: I2b693117815992130df4c0a969cf6cc5a77fc6a5
parent 3af61ed7
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -4210,6 +4210,14 @@
         one bar higher than they actually are -->
    <bool name="config_inflateSignalStrength">false</bool>

    <!-- Trigger a warning for notifications with RemoteView objects that are larger in bytes than
    this value (default 1MB)-->
    <integer name="config_notificationWarnRemoteViewSizeBytes">1000000</integer>

    <!-- Strip notification RemoteView objects that are larger in bytes than this value (also log)
    (default 2MB) -->
    <integer name="config_notificationStripRemoteViewSizeBytes">2000000</integer>

    <!-- Sharesheet: define a max number of targets per application for new shortcuts-based direct share introduced in Q -->
    <integer name="config_maxShortcutTargetsPerApp">3</integer>

+4 −0
Original line number Diff line number Diff line
@@ -3823,5 +3823,9 @@

  <java-symbol type="string" name="config_defaultSupervisionProfileOwnerComponent" />
  <java-symbol type="bool" name="config_inflateSignalStrength" />

  <java-symbol type="integer" name="config_notificationWarnRemoteViewSizeBytes" />
  <java-symbol type="integer" name="config_notificationStripRemoteViewSizeBytes" />

  <java-symbol type="string" name="config_factoryResetPackage" />
</resources>
+56 −3
Original line number Diff line number Diff line
@@ -207,6 +207,7 @@ import android.util.Xml;
import android.util.proto.ProtoOutputStream;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.RemoteViews;
import android.widget.Toast;

import com.android.internal.R;
@@ -463,6 +464,9 @@ public class NotificationManagerService extends SystemService {
    private boolean mIsAutomotive;
    private boolean mNotificationEffectsEnabledForAutomotive;

    private int mWarnRemoteViewsSizeBytes;
    private int mStripRemoteViewsSizeBytes;

    private MetricsLogger mMetricsLogger;
    private TriPredicate<String, Integer, String> mAllowedManagedServicePackages;

@@ -1723,6 +1727,11 @@ public class NotificationManagerService extends SystemService {

        mZenModeHelper.setPriorityOnlyDndExemptPackages(getContext().getResources().getStringArray(
                com.android.internal.R.array.config_priorityOnlyDndExemptPackages));

        mWarnRemoteViewsSizeBytes = getContext().getResources().getInteger(
                com.android.internal.R.integer.config_notificationWarnRemoteViewSizeBytes);
        mStripRemoteViewsSizeBytes = getContext().getResources().getInteger(
                com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes);
    }

    @Override
@@ -4712,7 +4721,7 @@ public class NotificationManagerService extends SystemService {

        // Fix the notification as best we can.
        try {
            fixNotification(notification, pkg, userId);
            fixNotification(notification, pkg, tag, id, userId);

        } catch (NameNotFoundException e) {
            Slog.e(TAG, "Cannot create a context for sending app", e);
@@ -4817,8 +4826,8 @@ public class NotificationManagerService extends SystemService {
    }

    @VisibleForTesting
    protected void fixNotification(Notification notification, String pkg, int userId)
            throws NameNotFoundException {
    protected void fixNotification(Notification notification, String pkg, String tag, int id,
            int userId) throws NameNotFoundException {
        final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(
                pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
                (userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);
@@ -4841,6 +4850,50 @@ public class NotificationManagerService extends SystemService {
                        ": Use of fullScreenIntent requires the USE_FULL_SCREEN_INTENT permission");
            }
        }

        // Remote views? Are they too big?
        checkRemoteViews(pkg, tag, id, notification);
    }

    private void checkRemoteViews(String pkg, String tag, int id, Notification notification) {
        if (removeRemoteView(pkg, tag, id, notification.contentView)) {
            notification.contentView = null;
        }
        if (removeRemoteView(pkg, tag, id, notification.bigContentView)) {
            notification.bigContentView = null;
        }
        if (removeRemoteView(pkg, tag, id, notification.headsUpContentView)) {
            notification.headsUpContentView = null;
        }
        if (notification.publicVersion != null) {
            if (removeRemoteView(pkg, tag, id, notification.publicVersion.contentView)) {
                notification.publicVersion.contentView = null;
            }
            if (removeRemoteView(pkg, tag, id, notification.publicVersion.bigContentView)) {
                notification.publicVersion.bigContentView = null;
            }
            if (removeRemoteView(pkg, tag, id, notification.publicVersion.headsUpContentView)) {
                notification.publicVersion.headsUpContentView = null;
            }
        }
    }

    private boolean removeRemoteView(String pkg, String tag, int id, RemoteViews contentView) {
        if (contentView == null) {
            return false;
        }
        final int contentViewSize = contentView.estimateMemoryUsage();
        if (contentViewSize > mWarnRemoteViewsSizeBytes
                && contentViewSize < mStripRemoteViewsSizeBytes) {
            Slog.w(TAG, "RemoteViews too large on tag: " + tag + " id: " + id
                    + " this might be stripped in a future release");
        }
        if (contentViewSize >= mStripRemoteViewsSizeBytes) {
            mUsageStats.registerImageRemoved(pkg);
            Slog.w(TAG, "Removed too large RemoteViews on tag: " + tag + " id: " + id);
            return true;
        }
        return false;
    }

    /**
+17 −1
Original line number Diff line number Diff line
@@ -41,7 +41,6 @@ import org.json.JSONException;
import org.json.JSONObject;

import java.io.PrintWriter;
import java.lang.Math;
import java.util.ArrayDeque;
import java.util.Calendar;
import java.util.GregorianCalendar;
@@ -263,6 +262,17 @@ public class NotificationUsageStats {
        }
    }

    /**
     * Call this when RemoteViews object has been removed from a notification because the images
     * it contains are too big (even after rescaling).
     */
    public synchronized void registerImageRemoved(String packageName) {
        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
        for (AggregatedStats stats : aggregatedStatsArray) {
            stats.numImagesRemoved++;
        }
    }

    // Locked by this.
    private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
        return getAggregatedStatsLocked(record.sbn.getPackageName());
@@ -405,6 +415,7 @@ public class NotificationUsageStats {
        public int numAlertViolations;
        public int numQuotaViolations;
        public long mLastAccessTime;
        public int numImagesRemoved;

        public AggregatedStats(Context context, String key) {
            this.key = key;
@@ -529,6 +540,7 @@ public class NotificationUsageStats {
            maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
            maybeCount("note_over_alert_rate", (numAlertViolations - previous.numAlertViolations));
            maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
            maybeCount("note_images_removed", (numImagesRemoved - previous.numImagesRemoved));
            noisyImportance.maybeCount(previous.noisyImportance);
            quietImportance.maybeCount(previous.quietImportance);
            finalImportance.maybeCount(previous.finalImportance);
@@ -562,6 +574,7 @@ public class NotificationUsageStats {
            previous.numRateViolations = numRateViolations;
            previous.numAlertViolations = numAlertViolations;
            previous.numQuotaViolations = numQuotaViolations;
            previous.numImagesRemoved = numImagesRemoved;
            noisyImportance.update(previous.noisyImportance);
            quietImportance.update(previous.quietImportance);
            finalImportance.update(previous.finalImportance);
@@ -667,6 +680,8 @@ public class NotificationUsageStats {
            output.append("numAlertViolations=").append(numAlertViolations).append("\n");
            output.append(indentPlusTwo);
            output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
            output.append(indentPlusTwo);
            output.append("numImagesRemoved=").append(numImagesRemoved).append("\n");
            output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
            output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
            output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
@@ -709,6 +724,7 @@ public class NotificationUsageStats {
            maybePut(dump, "numQuotaLViolations", numQuotaViolations);
            maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
            maybePut(dump, "numAlertViolations", numAlertViolations);
            maybePut(dump, "numImagesRemoved", numImagesRemoved);
            noisyImportance.maybePut(dump, previous.noisyImportance);
            quietImportance.maybePut(dump, previous.quietImportance);
            finalImportance.maybePut(dump, previous.finalImportance);
+94 −6
Original line number Diff line number Diff line
@@ -132,6 +132,7 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Xml;
import android.widget.RemoteViews;

import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
@@ -174,6 +175,7 @@ import java.util.List;
import java.util.Map;
import java.util.function.Consumer;


@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@@ -348,12 +350,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
        mHandler = mService.new WorkerHandler(mTestableLooper.getLooper());
        // MockPackageManager - default returns ApplicationInfo with matching calling UID
        mContext.setMockPackageManager(mPackageManagerClient);
        final ApplicationInfo applicationInfo = new ApplicationInfo();
        applicationInfo.uid = mUid;

        when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt()))
                .thenReturn(applicationInfo);
                .thenAnswer((Answer<ApplicationInfo>) invocation -> {
                    Object[] args = invocation.getArguments();
                    return getApplicationInfo((String) args[0], mUid);
                });
        when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
                .thenReturn(applicationInfo);
                .thenAnswer((Answer<ApplicationInfo>) invocation -> {
                    Object[] args = invocation.getArguments();
                    return getApplicationInfo((String) args[0], mUid);
                });
        when(mPackageManagerClient.getPackageUidAsUser(any(), anyInt())).thenReturn(mUid);
        final LightsManager mockLightsManager = mock(LightsManager.class);
        when(mockLightsManager.getLight(anyInt())).thenReturn(mock(Light.class));
@@ -389,7 +396,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {

        when(mAssistants.isAdjustmentAllowed(anyString())).thenReturn(true);


        mService.init(mTestableLooper.getLooper(),
                mPackageManager, mPackageManagerClient, mockLightsManager,
                mListeners, mAssistants, mConditionProviders,
@@ -413,12 +419,32 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {

    @After
    public void tearDown() throws Exception {
        mFile.delete();
        if (mFile != null) mFile.delete();
        clearDeviceConfig();
        InstrumentationRegistry.getInstrumentation()
                .getUiAutomation().dropShellPermissionIdentity();
    }

    private ApplicationInfo getApplicationInfo(String pkg, int uid) {
        final ApplicationInfo applicationInfo = new ApplicationInfo();
        applicationInfo.uid = uid;
        switch (pkg) {
            case PKG_N_MR1:
                applicationInfo.targetSdkVersion = Build.VERSION_CODES.N_MR1;
                break;
            case PKG_O:
                applicationInfo.targetSdkVersion = Build.VERSION_CODES.O;
                break;
            case PKG_P:
                applicationInfo.targetSdkVersion = Build.VERSION_CODES.P;
                break;
            default:
                applicationInfo.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
                break;
        }
        return applicationInfo;
    }

    public void waitForIdle() {
        mTestableLooper.processAllMessages();
    }
@@ -5122,4 +5148,66 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
        assertEquals(1, notifsAfter.length);
        assertEquals((notifsAfter[0].getNotification().flags & FLAG_BUBBLE), 0);
    }

    @Test
    public void testRemoveLargeRemoteViews() throws Exception {
        int removeSize = mContext.getResources().getInteger(
                com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes);

        RemoteViews rv = mock(RemoteViews.class);
        when(rv.estimateMemoryUsage()).thenReturn(removeSize);
        when(rv.clone()).thenReturn(rv);
        RemoteViews rv1 = mock(RemoteViews.class);
        when(rv1.estimateMemoryUsage()).thenReturn(removeSize);
        when(rv1.clone()).thenReturn(rv1);
        RemoteViews rv2 = mock(RemoteViews.class);
        when(rv2.estimateMemoryUsage()).thenReturn(removeSize);
        when(rv2.clone()).thenReturn(rv2);
        RemoteViews rv3 = mock(RemoteViews.class);
        when(rv3.estimateMemoryUsage()).thenReturn(removeSize);
        when(rv3.clone()).thenReturn(rv3);
        RemoteViews rv4 = mock(RemoteViews.class);
        when(rv4.estimateMemoryUsage()).thenReturn(removeSize);
        when(rv4.clone()).thenReturn(rv4);
        // note: different!
        RemoteViews rv5 = mock(RemoteViews.class);
        when(rv5.estimateMemoryUsage()).thenReturn(removeSize - 1);
        when(rv5.clone()).thenReturn(rv5);

        Notification np = new Notification.Builder(mContext, "test")
                .setSmallIcon(android.R.drawable.sym_def_app_icon)
                .setContentText("test")
                .setCustomContentView(rv)
                .setCustomBigContentView(rv1)
                .setCustomHeadsUpContentView(rv2)
                .build();
        Notification n = new Notification.Builder(mContext, "test")
                .setSmallIcon(android.R.drawable.sym_def_app_icon)
                .setContentText("test")
                .setCustomContentView(rv3)
                .setCustomBigContentView(rv4)
                .setCustomHeadsUpContentView(rv5)
                .setPublicVersion(np)
                .build();

        assertNotNull(np.contentView);
        assertNotNull(np.bigContentView);
        assertNotNull(np.headsUpContentView);

        assertTrue(n.publicVersion.extras.containsKey(Notification.EXTRA_CONTAINS_CUSTOM_VIEW));
        assertNotNull(n.publicVersion.contentView);
        assertNotNull(n.publicVersion.bigContentView);
        assertNotNull(n.publicVersion.headsUpContentView);

        mService.fixNotification(n, PKG, "tag", 9, 0);

        assertNull(n.contentView);
        assertNull(n.bigContentView);
        assertNotNull(n.headsUpContentView);
        assertNull(n.publicVersion.contentView);
        assertNull(n.publicVersion.bigContentView);
        assertNull(n.publicVersion.headsUpContentView);

        verify(mUsageStats, times(5)).registerImageRemoved(PKG);
    }
}