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

Commit 82e8048c authored by Jernej Virag's avatar Jernej Virag Committed by Android (Google) Code Review
Browse files

Merge "Restrict and validatee small icon sizes of notifications"

parents 08136e81 f54a6883
Loading
Loading
Loading
Loading
+13 −27
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.app.Notification;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -33,7 +34,6 @@ import android.graphics.Color;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Parcelable;
@@ -57,6 +57,7 @@ import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.statusbar.notification.NotificationIconDozeHelper;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.util.drawable.DrawableSize;

import java.text.NumberFormat;
import java.util.Arrays;
@@ -84,16 +85,6 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi
    public static final int STATE_DOT = 1;
    public static final int STATE_HIDDEN = 2;

    /**
     * Maximum allowed byte count for an icon bitmap
     * @see android.graphics.RecordingCanvas.MAX_BITMAP_SIZE
     */
    private static final int MAX_BITMAP_SIZE = 100 * 1024 * 1024; // 100 MB
    /**
     * Maximum allowed width or height for an icon drawable, if we can't get byte count
     */
    private static final int MAX_IMAGE_SIZE = 5000;

    private static final String TAG = "StatusBarIconView";
    private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT
            = new FloatProperty<StatusBarIconView>("iconAppearAmount") {
@@ -390,21 +381,6 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi
            return false;
        }

        if (drawable instanceof BitmapDrawable && ((BitmapDrawable) drawable).getBitmap() != null) {
            // If it's a bitmap we can check the size directly
            int byteCount = ((BitmapDrawable) drawable).getBitmap().getByteCount();
            if (byteCount > MAX_BITMAP_SIZE) {
                Log.w(TAG, "Drawable is too large (" + byteCount + " bytes) " + mIcon);
                return false;
            }
        } else if (drawable.getIntrinsicWidth() > MAX_IMAGE_SIZE
                || drawable.getIntrinsicHeight() > MAX_IMAGE_SIZE) {
            // Otherwise, check dimensions
            Log.w(TAG, "Drawable is too large (" + drawable.getIntrinsicWidth() + "x"
                    + drawable.getIntrinsicHeight() + ") " + mIcon);
            return false;
        }

        if (withClear) {
            setImageDrawable(null);
        }
@@ -432,7 +408,7 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi
     * @return Drawable for this item, or null if the package or item could not
     *         be found
     */
    public static Drawable getIcon(Context sysuiContext,
    private Drawable getIcon(Context sysuiContext,
            Context context, StatusBarIcon statusBarIcon) {
        int userId = statusBarIcon.user.getIdentifier();
        if (userId == UserHandle.USER_ALL) {
@@ -446,6 +422,16 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi
                typedValue, true);
        float scaleFactor = typedValue.getFloat();

        // We downscale the loaded drawable to reasonable size to protect against applications
        // using too much memory. The size can be tweaked in config.xml. Drawables
        // that are already sized properly won't be touched.
        boolean isLowRamDevice = ActivityManager.isLowRamDeviceStatic();
        Resources res = sysuiContext.getResources();
        int maxIconSize = res.getDimensionPixelSize(isLowRamDevice
                                ? com.android.internal.R.dimen.notification_small_icon_size_low_ram
                                : com.android.internal.R.dimen.notification_small_icon_size);
        icon = DrawableSize.downscaleToSize(res, icon, maxIconSize, maxIconSize);

        // No need to scale the icon, so return it as is.
        if (scaleFactor == 1.f) {
            return icon;
+123 −0
Original line number Diff line number Diff line
package com.android.systemui.util.drawable

import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.Animatable
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.AnimatedRotateDrawable
import android.graphics.drawable.AnimatedStateListDrawable
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.annotation.Px
import com.android.systemui.util.traceSection

class DrawableSize {

    companion object {

        const val TAG = "SysUiDrawableSize"

        /**
         * Downscales passed Drawable to set maximum width and height. This will only
         * be done for Drawables that can be downscaled non-destructively - e.g. animated
         * and stateful drawables will no be downscaled.
         *
         * Downscaling will keep the aspect ratio.
         * This method will not touch drawables that already fit into size specification.
         *
         * @param resources Resources on which to base the density of resized drawable.
         * @param drawable Drawable to downscale.
         * @param maxWidth Maximum width of the downscaled drawable.
         * @param maxHeight Maximum height of the downscaled drawable.
         *
         * @return returns downscaled drawable if it's possible to downscale it or original if it's
         *         not.
         */
        @JvmStatic
        fun downscaleToSize(
            res: Resources,
            drawable: Drawable,
            @Px maxWidth: Int,
            @Px maxHeight: Int
        ): Drawable {
            traceSection("DrawableSize#downscaleToSize") {
                // Bitmap drawables can contain big bitmaps as their content while sneaking it past
                // us using density scaling. Inspect inside the Bitmap drawables for actual bitmap
                // size for those.
                val originalWidth = (drawable as? BitmapDrawable)?.bitmap?.width
                                    ?: drawable.intrinsicWidth
                val originalHeight = (drawable as? BitmapDrawable)?.bitmap?.height
                                    ?: drawable.intrinsicHeight

                // Don't touch drawable if we can't resolve sizes for whatever reason.
                if (originalWidth <= 0 || originalHeight <= 0) {
                    return drawable
                }

                // Do not touch drawables that are already within bounds.
                if (originalWidth < maxWidth && originalHeight < maxHeight) {
                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                        Log.d(TAG, "Not resizing $originalWidth x $originalHeight" + " " +
                                "to $maxWidth x $maxHeight")
                    }

                    return drawable
                }

                if (!isSimpleBitmap(drawable)) {
                    return drawable
                }

                val scaleWidth = maxWidth.toFloat() / originalWidth.toFloat()
                val scaleHeight = maxHeight.toFloat() / originalHeight.toFloat()
                val scale = minOf(scaleHeight, scaleWidth)

                val width = (originalWidth * scale).toInt()
                val height = (originalHeight * scale).toInt()

                if (width <= 0 || height <= 0) {
                    Log.w(TAG, "Attempted to resize ${drawable.javaClass.simpleName} " +
                            "from $originalWidth x $originalHeight to invalid $width x $height.")
                    return drawable
                }

                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Resizing large drawable (${drawable.javaClass.simpleName}) " +
                            "from $originalWidth x $originalHeight to $width x $height")
                }

                // We want to keep existing config if it's more efficient than 32-bit RGB.
                val config = (drawable as? BitmapDrawable)?.bitmap?.config
                        ?: Bitmap.Config.ARGB_8888
                val scaledDrawableBitmap = Bitmap.createBitmap(width, height, config)
                val canvas = Canvas(scaledDrawableBitmap)

                val originalBounds = drawable.bounds
                drawable.setBounds(0, 0, width, height)
                drawable.draw(canvas)
                drawable.bounds = originalBounds

                return BitmapDrawable(res, scaledDrawableBitmap)
            }
        }

        private fun isSimpleBitmap(drawable: Drawable): Boolean {
            return !(drawable.isStateful || isAnimated(drawable))
        }

        private fun isAnimated(drawable: Drawable): Boolean {
            if (drawable is Animatable || drawable is Animatable2) {
                return true
            }

            return drawable is AnimatedImageDrawable ||
                drawable is AnimatedRotateDrawable ||
                drawable is AnimatedStateListDrawable ||
                drawable is AnimatedVectorDrawable
        }
    }
}
 No newline at end of file
+9 −1
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.statusbar;

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

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
@@ -37,6 +39,7 @@ import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Icon;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
@@ -130,7 +133,12 @@ public class StatusBarIconViewTest extends SysuiTestCase {
        Icon icon = Icon.createWithBitmap(largeBitmap);
        StatusBarIcon largeIcon = new StatusBarIcon(UserHandle.ALL, "mockPackage",
                icon, 0, 0, "");
        assertFalse(mIconView.set(largeIcon));
        assertTrue(mIconView.set(largeIcon));

        // The view should downscale the bitmap.
        BitmapDrawable drawable = (BitmapDrawable) mIconView.getDrawable();
        assertThat(drawable.getBitmap().getWidth()).isLessThan(1000);
        assertThat(drawable.getBitmap().getHeight()).isLessThan(1000);
    }

    @Test
+73 −0
Original line number Diff line number Diff line
package com.android.systemui.util.drawable

import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ShapeDrawable
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidTestingRunner::class)
@SmallTest
class DrawableSizeTest : SysuiTestCase() {

    lateinit var resources: Resources

    @Before
    fun setUp() {
        resources = context.resources
    }

    @Test
    fun testDownscaleToSize_drawableZeroSize_unchanged() {
        val drawable = ShapeDrawable()
        val result = DrawableSize.downscaleToSize(resources, drawable, 100, 100)
        assertThat(result).isSameInstanceAs(drawable)
    }

    @Test
    fun testDownscaleToSize_drawableSmallerThanRequirement_unchanged() {
        val drawable = BitmapDrawable(resources,
                Bitmap.createBitmap(
                        resources.displayMetrics,
                        150,
                        150,
                        Bitmap.Config.ARGB_8888
                )
        )
        val result = DrawableSize.downscaleToSize(resources, drawable, 300, 300)
        assertThat(result).isSameInstanceAs(drawable)
    }

    @Test
    fun testDownscaleToSize_drawableLargerThanRequirementWithDensity_resized() {
        // This bitmap would actually fail to resize if the method doesn't check for
        // bitmap dimensions inside drawable.
        val drawable = BitmapDrawable(resources,
                Bitmap.createBitmap(
                        resources.displayMetrics,
                        150,
                        75,
                        Bitmap.Config.ARGB_8888
                )
        )

        val result = DrawableSize.downscaleToSize(resources, drawable, 75, 75)
        assertThat(result).isNotSameInstanceAs(drawable)
        assertThat(result.intrinsicWidth).isEqualTo(75)
        assertThat(result.intrinsicHeight).isEqualTo(37)
    }

    @Test
    fun testDownscaleToSize_drawableAnimated_unchanged() {
        val drawable = resources.getDrawable(android.R.drawable.stat_sys_download,
                resources.newTheme())
        val result = DrawableSize.downscaleToSize(resources, drawable, 1, 1)
        assertThat(result).isSameInstanceAs(drawable)
    }
}
 No newline at end of file