From 640966b46d157f827f4783bfc0c0f0beac7b97bd Mon Sep 17 00:00:00 2001 From: Valentin Iftime Date: Fri, 3 Mar 2023 14:16:41 +0100 Subject: [PATCH] [DO NOT MERGE] Limit Icon Drawables to MAX_BITMAP_SIZE Fixes canvas drawing exceptions caused by unsuported image sizes. Prevent RemoteActions crashing SystemUi. Test: Add a RemoteAction for PiP with a very large icon (5k x 5k image) Test: atest IconTest Bug: 271544782 Change-Id: Ifa36c2c721dd2fee336423060b600aee8f2ad1ee (cherry picked from commit 28c379e413ad2619f04f98622f673363a178429b) --- .../res/drawable-nodpi/test_too_big.png | Bin 0 -> 154573 bytes .../android/graphics/drawable/IconTest.java | 85 ++++++++++++++++++ .../java/android/graphics/drawable/Icon.java | 56 ++++++++++-- 3 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 core/tests/coretests/res/drawable-nodpi/test_too_big.png diff --git a/core/tests/coretests/res/drawable-nodpi/test_too_big.png b/core/tests/coretests/res/drawable-nodpi/test_too_big.png new file mode 100644 index 0000000000000000000000000000000000000000..3754072b8e31e229531ec6844e479508b3761068 GIT binary patch literal 154573 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 literal 0 HcmV?d00001 diff --git a/core/tests/coretests/src/android/graphics/drawable/IconTest.java b/core/tests/coretests/src/android/graphics/drawable/IconTest.java index 75390a282af9a..5d922961aa8be 100644 --- a/core/tests/coretests/src/android/graphics/drawable/IconTest.java +++ b/core/tests/coretests/src/android/graphics/drawable/IconTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.RecordingCanvas; import android.graphics.Region; import android.os.Handler; import android.os.HandlerThread; @@ -371,6 +372,90 @@ public class IconTest extends AndroidTestCase { } } + private int getMaxWidth(int origWidth, int origHeight, int maxNumPixels) { + float aspRatio = (float) origWidth / (float) origHeight; + int newHeight = (int) Math.sqrt(maxNumPixels / aspRatio); + return (int) (newHeight * aspRatio); + } + + private int getMaxHeight(int origWidth, int origHeight, int maxNumPixels) { + float aspRatio = (float) origWidth / (float) origHeight; + return (int) Math.sqrt(maxNumPixels / aspRatio); + } + + @SmallTest + public void testScaleDownMaxSizeWithBitmap() throws Exception { + final int bmpWidth = 13_000; + final int bmpHeight = 10_000; + final int bmpBpp = 4; + final int maxNumPixels = RecordingCanvas.MAX_BITMAP_SIZE / bmpBpp; + final int maxWidth = getMaxWidth(bmpWidth, bmpHeight, maxNumPixels); + final int maxHeight = getMaxHeight(bmpWidth, bmpHeight, maxNumPixels); + + final Bitmap bm = Bitmap.createBitmap(bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888); + final Icon ic = Icon.createWithBitmap(bm); + final Drawable drawable = ic.loadDrawable(mContext); + + assertThat(drawable.getIntrinsicWidth()).isEqualTo(maxWidth); + assertThat(drawable.getIntrinsicHeight()).isEqualTo(maxHeight); + } + + @SmallTest + public void testScaleDownMaxSizeWithAdaptiveBitmap() throws Exception { + final int bmpWidth = 20_000; + final int bmpHeight = 10_000; + final int bmpBpp = 4; + final int maxNumPixels = RecordingCanvas.MAX_BITMAP_SIZE / bmpBpp; + final int maxWidth = getMaxWidth(bmpWidth, bmpHeight, maxNumPixels); + final int maxHeight = getMaxHeight(bmpWidth, bmpHeight, maxNumPixels); + + final Bitmap bm = Bitmap.createBitmap(bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888); + final Icon ic = Icon.createWithAdaptiveBitmap(bm); + final AdaptiveIconDrawable adaptiveDrawable = (AdaptiveIconDrawable) ic.loadDrawable( + mContext); + final Drawable drawable = adaptiveDrawable.getForeground(); + + assertThat(drawable.getIntrinsicWidth()).isEqualTo(maxWidth); + assertThat(drawable.getIntrinsicHeight()).isEqualTo(maxHeight); + } + + @SmallTest + public void testScaleDownMaxSizeWithResource() throws Exception { + final Icon ic = Icon.createWithResource(getContext(), R.drawable.test_too_big); + final BitmapDrawable drawable = (BitmapDrawable) ic.loadDrawable(mContext); + + assertThat(drawable.getBitmap().getByteCount()).isAtMost(RecordingCanvas.MAX_BITMAP_SIZE); + } + + @SmallTest + public void testScaleDownMaxSizeWithFile() throws Exception { + final Bitmap bit1 = ((BitmapDrawable) getContext().getDrawable(R.drawable.test_too_big)) + .getBitmap(); + final File dir = getContext().getExternalFilesDir(null); + final File file1 = new File(dir, "file1-too-big.png"); + bit1.compress(Bitmap.CompressFormat.PNG, 100, + new FileOutputStream(file1)); + + final Icon ic = Icon.createWithFilePath(file1.toString()); + final BitmapDrawable drawable = (BitmapDrawable) ic.loadDrawable(mContext); + + assertThat(drawable.getBitmap().getByteCount()).isAtMost(RecordingCanvas.MAX_BITMAP_SIZE); + } + + @SmallTest + public void testScaleDownMaxSizeWithData() throws Exception { + final int bmpBpp = 4; + final Bitmap originalBits = ((BitmapDrawable) getContext().getDrawable( + R.drawable.test_too_big)).getBitmap(); + final ByteArrayOutputStream ostream = new ByteArrayOutputStream( + originalBits.getWidth() * originalBits.getHeight() * bmpBpp); + originalBits.compress(Bitmap.CompressFormat.PNG, 100, ostream); + final byte[] pngdata = ostream.toByteArray(); + final Icon ic = Icon.createWithData(pngdata, 0, pngdata.length); + final BitmapDrawable drawable = (BitmapDrawable) ic.loadDrawable(mContext); + + assertThat(drawable.getBitmap().getByteCount()).isAtMost(RecordingCanvas.MAX_BITMAP_SIZE); + } // ======== utils ======== diff --git a/graphics/java/android/graphics/drawable/Icon.java b/graphics/java/android/graphics/drawable/Icon.java index a76d74edc0f4d..708feeb9e421c 100644 --- a/graphics/java/android/graphics/drawable/Icon.java +++ b/graphics/java/android/graphics/drawable/Icon.java @@ -35,6 +35,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BlendMode; import android.graphics.PorterDuff; +import android.graphics.RecordingCanvas; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -70,6 +71,7 @@ import java.util.Objects; public final class Icon implements Parcelable { private static final String TAG = "Icon"; + private static final boolean DEBUG = false; /** * An icon that was created using {@link Icon#createWithBitmap(Bitmap)}. @@ -360,16 +362,53 @@ public final class Icon implements Parcelable { return result; } + /** + * Resizes image if size too large for Canvas to draw + * @param bitmap Bitmap to be resized if size > {@link RecordingCanvas.MAX_BITMAP_SIZE} + * @return resized bitmap + */ + private Bitmap fixMaxBitmapSize(Bitmap bitmap) { + if (bitmap != null && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) { + int bytesPerPixel = bitmap.getRowBytes() / bitmap.getWidth(); + int maxNumPixels = RecordingCanvas.MAX_BITMAP_SIZE / bytesPerPixel; + float aspRatio = (float) bitmap.getWidth() / (float) bitmap.getHeight(); + int newHeight = (int) Math.sqrt(maxNumPixels / aspRatio); + int newWidth = (int) (newHeight * aspRatio); + + if (DEBUG) { + Log.d(TAG, + "Image size too large: " + bitmap.getByteCount() + ". Resizing bitmap to: " + + newWidth + " " + newHeight); + } + + return scaleDownIfNecessary(bitmap, newWidth, newHeight); + } + return bitmap; + } + + /** + * Resizes BitmapDrawable if size too large for Canvas to draw + * @param drawable Drawable to be resized if size > {@link RecordingCanvas.MAX_BITMAP_SIZE} + * @return resized Drawable + */ + private Drawable fixMaxBitmapSize(Resources res, Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + Bitmap scaledBmp = fixMaxBitmapSize(((BitmapDrawable) drawable).getBitmap()); + return new BitmapDrawable(res, scaledBmp); + } + return drawable; + } + /** * Do the heavy lifting of loading the drawable, but stop short of applying any tint. */ private Drawable loadDrawableInner(Context context) { switch (mType) { case TYPE_BITMAP: - return new BitmapDrawable(context.getResources(), getBitmap()); + return new BitmapDrawable(context.getResources(), fixMaxBitmapSize(getBitmap())); case TYPE_ADAPTIVE_BITMAP: return new AdaptiveIconDrawable(null, - new BitmapDrawable(context.getResources(), getBitmap())); + new BitmapDrawable(context.getResources(), fixMaxBitmapSize(getBitmap()))); case TYPE_RESOURCE: if (getResources() == null) { // figure out where to load resources from @@ -400,7 +439,8 @@ public final class Icon implements Parcelable { } } try { - return getResources().getDrawable(getResId(), context.getTheme()); + return fixMaxBitmapSize(getResources(), + getResources().getDrawable(getResId(), context.getTheme())); } catch (RuntimeException e) { Log.e(TAG, String.format("Unable to load resource 0x%08x from pkg=%s", getResId(), @@ -409,21 +449,21 @@ public final class Icon implements Parcelable { } break; case TYPE_DATA: - return new BitmapDrawable(context.getResources(), - BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength()) - ); + return new BitmapDrawable(context.getResources(), fixMaxBitmapSize( + BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), + getDataLength()))); case TYPE_URI: InputStream is = getUriInputStream(context); if (is != null) { return new BitmapDrawable(context.getResources(), - BitmapFactory.decodeStream(is)); + fixMaxBitmapSize(BitmapFactory.decodeStream(is))); } break; case TYPE_URI_ADAPTIVE_BITMAP: is = getUriInputStream(context); if (is != null) { return new AdaptiveIconDrawable(null, new BitmapDrawable(context.getResources(), - BitmapFactory.decodeStream(is))); + fixMaxBitmapSize(BitmapFactory.decodeStream(is)))); } break; }