diff options
4 files changed, 218 insertions, 28 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index 5302188ccb31..4a7606c316e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/util/drawable/DrawableSize.kt b/packages/SystemUI/src/com/android/systemui/util/drawable/DrawableSize.kt new file mode 100644 index 000000000000..b5068087d905 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/drawable/DrawableSize.kt @@ -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 + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java index 85ea52b6af6a..d13451dc4769 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/drawable/DrawableSizeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/drawable/DrawableSizeTest.kt new file mode 100644 index 000000000000..ac357ea34be0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/drawable/DrawableSizeTest.kt @@ -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) + } +}
\ No newline at end of file |