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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user