Restrict and validatee small icon sizes of notifications

This makes StatusBarIconView downscale notification icons if they exceed notification_small_icon_size. This avoids possibility of large memory usage if the app passes a huge drawable asset as the small notification icon.
This restriction currenctly doesn't apply for animated or stateful drawables.

Bug: 209044537
Bug: 193720474

Test: Manually
Change-Id: I1bf19e9d2b1d9fef67e1653e601b11fd74f72e73
This commit is contained in:
Jernej Virag
2022-01-27 14:45:57 +00:00
parent 214667438f
commit f54a688342
4 changed files with 218 additions and 28 deletions

View File

@@ -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;

View File

@@ -0,0 +1,123 @@
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
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,73 @@
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)
}
}