Loading packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +13 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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") { Loading Loading @@ -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); } Loading Loading @@ -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) { Loading @@ -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; Loading packages/SystemUI/src/com/android/systemui/util/drawable/DrawableSize.kt 0 → 100644 +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 packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java +9 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 Loading packages/SystemUI/tests/src/com/android/systemui/util/drawable/DrawableSizeTest.kt 0 → 100644 +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 Loading
packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +13 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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") { Loading Loading @@ -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); } Loading Loading @@ -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) { Loading @@ -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; Loading
packages/SystemUI/src/com/android/systemui/util/drawable/DrawableSize.kt 0 → 100644 +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
packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java +9 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 Loading
packages/SystemUI/tests/src/com/android/systemui/util/drawable/DrawableSizeTest.kt 0 → 100644 +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