From 698325914ed75465b6c80973210acd809c85f131 Mon Sep 17 00:00:00 2001 From: Jernej Virag Date: Wed, 16 Mar 2022 09:25:55 +0000 Subject: [PATCH 1/3] Support downscaling of Drawable icons in LocalImageResolver LocalImageResolver only enforced downscaling for Uri-based Icons at this point. This still allowed app developers to provide large bitmaps as a resource or as a bitmap payload itself. This change also verifies these new types of bitmaps. Also adds an external size parameter to image resolver so different widgets can use it - right now the upper limit was hardcoded in pixels. This also updates error handling - in some cases ImageDecoder would throw NullPointerException or NotFoundException except for promised IOException. This wraps those cases into more consistent API. Bug:218845090 Bug:210690571 Bug:224768026 Test: Manually on device - tested on Raven and new Pixel with multiple notification sizes via notification test apk. atest LocalImageResolver atest NotificationManagerTest Change-Id: I4701d58ac3335d11a93355a39cee9f5c998eb830 --- .../internal/widget/LocalImageResolver.java | 204 +++++++++++++---- core/tests/coretests/res/drawable/big_a.png | Bin 0 -> 11905 bytes .../widget/LocalImageResolverTest.java | 212 ++++++++++++++++++ 3 files changed, 377 insertions(+), 39 deletions(-) create mode 100644 core/tests/coretests/res/drawable/big_a.png create mode 100644 core/tests/coretests/src/com/android/internal/widget/LocalImageResolverTest.java diff --git a/core/java/com/android/internal/widget/LocalImageResolver.java b/core/java/com/android/internal/widget/LocalImageResolver.java index 616b69961b799..ce27b346567b5 100644 --- a/core/java/com/android/internal/widget/LocalImageResolver.java +++ b/core/java/com/android/internal/widget/LocalImageResolver.java @@ -16,70 +16,195 @@ package com.android.internal.widget; +import android.annotation.DrawableRes; import android.annotation.Nullable; import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; import android.graphics.ImageDecoder; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.net.Uri; +import android.util.Log; import android.util.Size; +import com.android.internal.annotations.VisibleForTesting; + import java.io.IOException; /** A class to extract Drawables from a MessagingStyle/ConversationStyle message. */ public class LocalImageResolver { - private static final String TAG = LocalImageResolver.class.getSimpleName(); - private static final int MAX_SAFE_ICON_SIZE_PX = 480; + private static final String TAG = "LocalImageResolver"; + + @VisibleForTesting + static final int DEFAULT_MAX_SAFE_ICON_SIZE_PX = 480; /** - * Resolve an image from the given Uri using {@link ImageDecoder} + * Resolve an image from the given Uri using {@link ImageDecoder} if it contains a + * bitmap reference. */ + @Nullable public static Drawable resolveImage(Uri uri, Context context) throws IOException { - final ImageDecoder.Source source = - ImageDecoder.createSource(context.getContentResolver(), uri); - final Drawable drawable = - ImageDecoder.decodeDrawable(source, LocalImageResolver::onHeaderDecoded); - return drawable; + try { + final ImageDecoder.Source source = + ImageDecoder.createSource(context.getContentResolver(), uri); + return ImageDecoder.decodeDrawable(source, + (decoder, info, s) -> LocalImageResolver.onHeaderDecoded(decoder, info, + DEFAULT_MAX_SAFE_ICON_SIZE_PX, DEFAULT_MAX_SAFE_ICON_SIZE_PX)); + } catch (Exception e) { + // Invalid drawable resource can actually throw either NullPointerException or + // ResourceNotFoundException. This sanitizes to expected output. + throw new IOException(e); + } } /** - * Get the drawable from Icon using {@link ImageDecoder} if it contains a Uri, or + * Get the drawable from Icon using {@link ImageDecoder} if it contains a bitmap reference, or * using {@link Icon#loadDrawable(Context)} otherwise. This will correctly apply the Icon's, * tint, if present, to the drawable. + * + * @return drawable or null if loading failed. */ - public static Drawable resolveImage(Icon icon, Context context) throws IOException { - Uri uri = getResolvableUri(icon); - if (uri != null) { - Drawable result = resolveImage(uri, context); - if (icon.hasTint()) { - result.mutate(); - result.setTintList(icon.getTintList()); - result.setTintBlendMode(icon.getTintBlendMode()); - } - return result; + @Nullable + public static Drawable resolveImage(@Nullable Icon icon, Context context) throws IOException { + return resolveImage(icon, context, DEFAULT_MAX_SAFE_ICON_SIZE_PX, + DEFAULT_MAX_SAFE_ICON_SIZE_PX); + } + + /** + * Get the drawable from Icon using {@link ImageDecoder} if it contains a bitmap reference, or + * using {@link Icon#loadDrawable(Context)} otherwise. This will correctly apply the Icon's, + * tint, if present, to the drawable. + * + * @throws IOException if the icon could not be loaded for whichever reason + */ + @Nullable + public static Drawable resolveImage(@Nullable Icon icon, Context context, int maxWidth, + int maxHeight) { + if (icon == null) { + return null; } + + switch (icon.getType()) { + case Icon.TYPE_URI: + case Icon.TYPE_URI_ADAPTIVE_BITMAP: + Uri uri = getResolvableUri(icon); + if (uri != null) { + Drawable result = resolveImage(uri, context, maxWidth, maxHeight); + if (result != null) { + return tintDrawable(icon, result); + } + } + break; + case Icon.TYPE_RESOURCE: + Drawable result = resolveImage(icon.getResId(), context, maxWidth, maxHeight); + if (result != null) { + return tintDrawable(icon, result); + } + break; + case Icon.TYPE_BITMAP: + case Icon.TYPE_ADAPTIVE_BITMAP: + return resolveBitmapImage(icon, context, maxWidth, maxHeight); + case Icon.TYPE_DATA: // We can't really improve on raw data images. + default: + break; + } + + // Fallback to straight drawable load if we fail with more efficient approach. + try { + return icon.loadDrawable(context); + } catch (Resources.NotFoundException e) { + return null; + } + } + + /** + * Attempts to resolve the resource as a bitmap drawable constrained within max sizes. + */ + @Nullable + public static Drawable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight) { + final ImageDecoder.Source source = + ImageDecoder.createSource(context.getContentResolver(), uri); + return resolveImage(source, maxWidth, maxHeight); + } + + /** + * Attempts to resolve the resource as a bitmap drawable constrained within max sizes. + * + * @return decoded drawable or null if the passed resource is not a straight bitmap + */ + @Nullable + public static Drawable resolveImage(@DrawableRes int resId, Context context, int maxWidth, + int maxHeight) { + final ImageDecoder.Source source = ImageDecoder.createSource(context.getResources(), resId); + return resolveImage(source, maxWidth, maxHeight); + } + + @Nullable + private static Drawable resolveBitmapImage(Icon icon, Context context, int maxWidth, + int maxHeight) { + Bitmap bitmap = icon.getBitmap(); + if (bitmap == null) { + return null; + } + + if (bitmap.getWidth() > maxWidth || bitmap.getHeight() > maxHeight) { + Icon smallerIcon = icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP + ? Icon.createWithAdaptiveBitmap(bitmap) : Icon.createWithBitmap(bitmap); + // We don't want to modify the source icon, create a copy. + smallerIcon.setTintList(icon.getTintList()) + .setTintBlendMode(icon.getTintBlendMode()) + .scaleDownIfNecessary(maxWidth, maxHeight); + return smallerIcon.loadDrawable(context); + } + return icon.loadDrawable(context); } - public static Drawable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight) - throws IOException { - final ImageDecoder.Source source = - ImageDecoder.createSource(context.getContentResolver(), uri); - return ImageDecoder.decodeDrawable(source, (decoder, info, unused) -> { - final Size size = info.getSize(); - if (size.getWidth() > size.getHeight()) { - if (size.getWidth() > maxWidth) { - final int targetHeight = size.getHeight() * maxWidth / size.getWidth(); - decoder.setTargetSize(maxWidth, targetHeight); + @Nullable + private static Drawable tintDrawable(Icon icon, @Nullable Drawable drawable) { + if (drawable == null) { + return null; + } + + if (icon.hasTint()) { + drawable.mutate(); + drawable.setTintList(icon.getTintList()); + drawable.setTintBlendMode(icon.getTintBlendMode()); + } + + return drawable; + } + + private static Drawable resolveImage(ImageDecoder.Source source, int maxWidth, int maxHeight) { + try { + return ImageDecoder.decodeDrawable(source, (decoder, info, unused) -> { + if (maxWidth <= 0 || maxHeight <= 0) { + return; } - } else { - if (size.getHeight() > maxHeight) { - final int targetWidth = size.getWidth() * maxHeight / size.getHeight(); - decoder.setTargetSize(targetWidth, maxHeight); + + final Size size = info.getSize(); + if (size.getWidth() > size.getHeight()) { + if (size.getWidth() > maxWidth) { + final int targetHeight = size.getHeight() * maxWidth / size.getWidth(); + decoder.setTargetSize(maxWidth, targetHeight); + } + } else { + if (size.getHeight() > maxHeight) { + final int targetWidth = size.getWidth() * maxHeight / size.getHeight(); + decoder.setTargetSize(targetWidth, maxHeight); + } } - } - }); + }); + + // ImageDecoder documentation is misleading a bit - it'll throw NotFoundException + // in some cases despite it not saying so. Rethrow it as an IOException to keep + // our API contract. + } catch (IOException | Resources.NotFoundException e) { + Log.e(TAG, "Failed to load image drawable", e); + return null; + } } private static int getPowerOfTwoForSampleRatio(double ratio) { @@ -88,11 +213,12 @@ public class LocalImageResolver { } private static void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, - ImageDecoder.Source source) { + int maxWidth, int maxHeight) { final Size size = info.getSize(); final int originalSize = Math.max(size.getHeight(), size.getWidth()); - final double ratio = (originalSize > MAX_SAFE_ICON_SIZE_PX) - ? originalSize * 1f / MAX_SAFE_ICON_SIZE_PX + final int maxSize = Math.max(maxWidth, maxHeight); + final double ratio = (originalSize > maxSize) + ? originalSize * 1f / maxSize : 1.0; decoder.setTargetSampleSize(getPowerOfTwoForSampleRatio(ratio)); } @@ -101,7 +227,7 @@ public class LocalImageResolver { * Gets the Uri for this icon, assuming the icon can be treated as a pure Uri. Null otherwise. */ @Nullable - public static Uri getResolvableUri(@Nullable Icon icon) { + private static Uri getResolvableUri(@Nullable Icon icon) { if (icon == null || (icon.getType() != Icon.TYPE_URI && icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP)) { return null; diff --git a/core/tests/coretests/res/drawable/big_a.png b/core/tests/coretests/res/drawable/big_a.png new file mode 100644 index 0000000000000000000000000000000000000000..dc059a3557a8becdb07680d8c366d6db8baaf3c6 GIT binary patch literal 11905 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 a27prgbG6 Date: Wed, 16 Mar 2022 09:25:55 +0000 Subject: [PATCH 2/3] Downscale large bitmaps in CachingIconView CachingIconView is used to displayed (smallish) icons in Notifications. Those can accidentally be made very large if big resources are used. This makes CachingIconView use LocalImageResolver to load those images with limited size. This fixes large memory use of notification header icons. Bug: 210690571 Bug: 218845090 Test: atest CachingIconViewTest atest NotificationManagerTest Verified with notification test APK on a Pixel Change-Id: Ia37c1fd31a7720e6ec2fd20d7466d8dcaffa6507 --- .../internal/widget/CachingIconView.java | 110 +++++++- core/res/res/values/attrs.xml | 8 + core/res/res/values/public-staging.xml | 4 + .../caching_icon_view_test_max_size.xml | 24 ++ .../caching_icon_view_test_no_max_size.xml | 22 ++ .../internal/widget/CachingIconViewTest.java | 259 ++++++++++++++++++ 6 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 core/tests/coretests/res/layout/caching_icon_view_test_max_size.xml create mode 100644 core/tests/coretests/res/layout/caching_icon_view_test_no_max_size.xml create mode 100644 core/tests/coretests/src/com/android/internal/widget/CachingIconViewTest.java diff --git a/core/java/com/android/internal/widget/CachingIconView.java b/core/java/com/android/internal/widget/CachingIconView.java index 299cbe12b4d13..d1156979ee072 100644 --- a/core/java/com/android/internal/widget/CachingIconView.java +++ b/core/java/com/android/internal/widget/CachingIconView.java @@ -23,6 +23,7 @@ import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.Configuration; +import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; @@ -35,6 +36,8 @@ import android.view.RemotableViewMethod; import android.widget.ImageView; import android.widget.RemoteViews; +import com.android.internal.R; + import java.util.Objects; import java.util.function.Consumer; @@ -55,9 +58,42 @@ public class CachingIconView extends ImageView { private int mBackgroundColor; private boolean mWillBeForceHidden; + private int mMaxDrawableWidth = -1; + private int mMaxDrawableHeight = -1; + + public CachingIconView(Context context) { + this(context, null, 0, 0); + } + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public CachingIconView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0, 0); + } + + public CachingIconView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CachingIconView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + if (attrs == null) { + return; + } + + TypedArray ta = context.obtainStyledAttributes(attrs, + R.styleable.CachingIconView, defStyleAttr, defStyleRes); + mMaxDrawableWidth = ta.getDimensionPixelSize(R.styleable + .CachingIconView_maxDrawableWidth, -1); + mMaxDrawableHeight = ta.getDimensionPixelSize(R.styleable + .CachingIconView_maxDrawableHeight, -1); + ta.recycle(); } @Override @@ -66,15 +102,27 @@ public class CachingIconView extends ImageView { if (!testAndSetCache(icon)) { mInternalSetDrawable = true; // This calls back to setImageDrawable, make sure we don't clear the cache there. - super.setImageIcon(icon); + Drawable drawable = loadSizeRestrictedIcon(icon); + if (drawable == null) { + super.setImageIcon(icon); + } else { + super.setImageDrawable(drawable); + } mInternalSetDrawable = false; } } + @Nullable + private Drawable loadSizeRestrictedIcon(@Nullable Icon icon) { + return LocalImageResolver.resolveImage(icon, getContext(), mMaxDrawableWidth, + mMaxDrawableHeight); + } + @Override - public Runnable setImageIconAsync(@Nullable Icon icon) { + public Runnable setImageIconAsync(@Nullable final Icon icon) { resetCache(); - return super.setImageIconAsync(icon); + Drawable drawable = loadSizeRestrictedIcon(icon); + return () -> setImageDrawable(drawable); } @Override @@ -83,14 +131,30 @@ public class CachingIconView extends ImageView { if (!testAndSetCache(resId)) { mInternalSetDrawable = true; // This calls back to setImageDrawable, make sure we don't clear the cache there. - super.setImageResource(resId); + Drawable drawable = loadSizeRestrictedDrawable(resId); + if (drawable == null) { + super.setImageResource(resId); + } else { + super.setImageDrawable(drawable); + } mInternalSetDrawable = false; } } + @Nullable + private Drawable loadSizeRestrictedDrawable(@DrawableRes int resId) { + return LocalImageResolver.resolveImage(resId, getContext(), mMaxDrawableWidth, + mMaxDrawableHeight); + } + @Override public Runnable setImageResourceAsync(@DrawableRes int resId) { resetCache(); + Drawable drawable = loadSizeRestrictedDrawable(resId); + if (drawable != null) { + return () -> setImageDrawable(drawable); + } + return super.setImageResourceAsync(resId); } @@ -98,13 +162,31 @@ public class CachingIconView extends ImageView { @RemotableViewMethod(asyncImpl="setImageURIAsync") public void setImageURI(@Nullable Uri uri) { resetCache(); - super.setImageURI(uri); + Drawable drawable = loadSizeRestrictedUri(uri); + if (drawable == null) { + super.setImageURI(uri); + } else { + mInternalSetDrawable = true; + super.setImageDrawable(drawable); + mInternalSetDrawable = false; + } + } + + @Nullable + private Drawable loadSizeRestrictedUri(@Nullable Uri uri) { + return LocalImageResolver.resolveImage(uri, getContext(), mMaxDrawableWidth, + mMaxDrawableHeight); } @Override public Runnable setImageURIAsync(@Nullable Uri uri) { resetCache(); - return super.setImageURIAsync(uri); + Drawable drawable = loadSizeRestrictedUri(uri); + if (drawable == null) { + return super.setImageURIAsync(uri); + } else { + return () -> setImageDrawable(drawable); + } } @Override @@ -307,4 +389,18 @@ public class CachingIconView extends ImageView { public void setWillBeForceHidden(boolean forceHidden) { mWillBeForceHidden = forceHidden; } + + /** + * Returns the set maximum width of drawable in pixels. -1 if not set. + */ + public int getMaxDrawableWidth() { + return mMaxDrawableWidth; + } + + /** + * Returns the set maximum height of drawable in pixels. -1 if not set. + */ + public int getMaxDrawableHeight() { + return mMaxDrawableHeight; + } } diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 2107f651eade6..b3203aebcb993 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -9807,4 +9807,12 @@ of the supported locale. {@link android.app.LocaleConfig} --> + + + + + + + + diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index e35d2e9f488a3..1d7e181b7a8ff 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -147,6 +147,10 @@ + + + + diff --git a/core/tests/coretests/res/layout/caching_icon_view_test_max_size.xml b/core/tests/coretests/res/layout/caching_icon_view_test_max_size.xml new file mode 100644 index 0000000000000..9a034466b0fd3 --- /dev/null +++ b/core/tests/coretests/res/layout/caching_icon_view_test_max_size.xml @@ -0,0 +1,24 @@ + + + + diff --git a/core/tests/coretests/res/layout/caching_icon_view_test_no_max_size.xml b/core/tests/coretests/res/layout/caching_icon_view_test_no_max_size.xml new file mode 100644 index 0000000000000..a213a977761d2 --- /dev/null +++ b/core/tests/coretests/res/layout/caching_icon_view_test_no_max_size.xml @@ -0,0 +1,22 @@ + + + + diff --git a/core/tests/coretests/src/com/android/internal/widget/CachingIconViewTest.java b/core/tests/coretests/src/com/android/internal/widget/CachingIconViewTest.java new file mode 100644 index 0000000000000..aa9620312a912 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/widget/CachingIconViewTest.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import static com.google.common.truth.Truth.assertThat; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.graphics.drawable.InsetDrawable; +import android.net.Uri; +import android.util.TypedValue; +import android.view.LayoutInflater; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.frameworks.coretests.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class CachingIconViewTest { + + private Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + } + + @Test + public void invalidIcon_skipsLoadSuccessfully() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageIcon(Icon.createWithResource(mContext, 0x85743222)); + Drawable drawable = view.getDrawable(); + assertThat(drawable).isNull(); + } + + @Test + public void customDrawable_setImageIcon_skipsResizeSuccessfully() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageIcon(Icon.createWithResource(mContext, R.drawable.custom_drawable)); + Drawable drawable = view.getDrawable(); + assertThat(drawable).isInstanceOf(InsetDrawable.class); + } + + @Test + public void customDrawable_setImageIconAsync_skipsResizeSuccessfully() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageIconAsync(Icon.createWithResource(mContext, R.drawable.custom_drawable)).run(); + Drawable drawable = view.getDrawable(); + assertThat(drawable).isInstanceOf(InsetDrawable.class); + } + + @Test + public void customDrawable_setImageResource_skipsResizeSuccessfully() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageResource(R.drawable.custom_drawable); + Drawable drawable = view.getDrawable(); + assertThat(drawable).isInstanceOf(InsetDrawable.class); + } + + @Test + public void customDrawable_setImageResourceAsync_skipsResizeSuccessfully() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageResourceAsync(R.drawable.custom_drawable).run(); + Drawable drawable = view.getDrawable(); + assertThat(drawable).isInstanceOf(InsetDrawable.class); + } + + @Test + public void customDrawable_setImageUri_skipsResizeSuccessfully() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageURI(Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + R.drawable.custom_drawable)); + Drawable drawable = view.getDrawable(); + assertThat(drawable).isInstanceOf(InsetDrawable.class); + } + + @Test + public void customDrawable_setImageUriAsync_skipsResizeSuccessfully() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageURIAsync(Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + R.drawable.custom_drawable)).run(); + Drawable drawable = view.getDrawable(); + assertThat(drawable).isInstanceOf(InsetDrawable.class); + } + + @Test + public void maxDrawableDimensionsSet_setImageIcon_resizesImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageIcon(Icon.createWithResource(mContext, R.drawable.big_a)); + + assertDrawableResized(view); + } + + @Test + public void maxDrawableWithNoDimensionsSet_setImageIcon_doesNotResizeImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_no_max_size, null); + view.setImageIcon(Icon.createWithResource(mContext, R.drawable.big_a)); + + assertDrawableNotResized(view); + } + + @Test + public void maxDrawableDimensionsSet_setImageIconAsync_resizesImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageIconAsync(Icon.createWithResource(mContext, R.drawable.big_a)).run(); + + assertDrawableResized(view); + } + + @Test + public void maxDrawableWithNoDimensionsSet_setImageIconAsync_doesNotResizeImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_no_max_size, null); + view.setImageIconAsync(Icon.createWithResource(mContext, R.drawable.big_a)).run(); + + assertDrawableNotResized(view); + } + + @Test + public void maxDrawableDimensionsSet_setImageResource_resizesImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageResource(R.drawable.big_a); + + assertDrawableResized(view); + } + + @Test + public void maxDrawableWithNoDimensionsSet_setImageResource_doesNotResizeImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_no_max_size, null); + view.setImageResource(R.drawable.big_a); + + assertDrawableNotResized(view); + } + + @Test + public void maxDrawableDimensionsSet_setImageResourceAsync_resizesImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageResourceAsync(R.drawable.big_a).run(); + + assertDrawableResized(view); + } + + @Test + public void maxDrawableWithNoDimensionsSet_setImageResourceAsync_doesNotResizeImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_no_max_size, null); + view.setImageResourceAsync(R.drawable.big_a).run(); + + assertDrawableNotResized(view); + } + + @Test + public void maxDrawableDimensionsSet_setImageUri_resizesImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageURI(Uri.parse( + "android.resource://com.android.frameworks.coretests/" + R.drawable.big_a)); + + assertDrawableResized(view); + } + + @Test + public void maxDrawableWithNoDimensionsSet_setImageUri_doesNotResizeImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_no_max_size, null); + view.setImageURI(Uri.parse( + "android.resource://com.android.frameworks.coretests/" + R.drawable.big_a)); + + assertDrawableNotResized(view); + } + + @Test + public void maxDrawableDimensionsSet_setImageUriAsync_resizesImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_max_size, null); + view.setImageURIAsync(Uri.parse( + "android.resource://com.android.frameworks.coretests/" + R.drawable.big_a)).run(); + + assertDrawableResized(view); + } + + @Test + public void maxDrawableWithNoDimensionsSet_setImageUriAsync_doesNotResizeImageIcon() { + CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate( + R.layout.caching_icon_view_test_no_max_size, null); + view.setImageURIAsync(Uri.parse( + "android.resource://com.android.frameworks.coretests/" + R.drawable.big_a)).run(); + + assertDrawableNotResized(view); + } + + + private void assertDrawableResized(@Nullable CachingIconView view) { + assertThat(view).isNotNull(); + int maxSize = + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80f, + mContext.getResources().getDisplayMetrics()); + assertThat(view.getMaxDrawableHeight()).isEqualTo(maxSize); + assertThat(view.getMaxDrawableWidth()).isEqualTo(maxSize); + + Drawable drawable = view.getDrawable(); + assertThat(drawable).isInstanceOf(BitmapDrawable.class); + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + assertThat(bitmapDrawable.getBitmap().getWidth()).isLessThan(maxSize + 1); + assertThat(bitmapDrawable.getBitmap().getHeight()).isLessThan(maxSize + 1); + } + + private void assertDrawableNotResized(@Nullable CachingIconView view) { + assertThat(view).isNotNull(); + int maxSize = + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80f, + mContext.getResources().getDisplayMetrics()); + assertThat(view.getMaxDrawableHeight()).isEqualTo(-1); + assertThat(view.getMaxDrawableWidth()).isEqualTo(-1); + + Drawable drawable = view.getDrawable(); + assertThat(drawable).isInstanceOf(BitmapDrawable.class); + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + assertThat(bitmapDrawable.getBitmap().getWidth()).isGreaterThan(maxSize); + assertThat(bitmapDrawable.getBitmap().getHeight()).isGreaterThan(maxSize); + } +} From 5943bac5cfc1bb654714bfe0660b8b77df443644 Mon Sep 17 00:00:00 2001 From: Jernej Virag Date: Wed, 16 Mar 2022 09:25:55 +0000 Subject: [PATCH 3/3] Size restrict right notification icon size Right now the right notification icon is always loaded at full resolution. This will downsample the icon to displayed size if the user passes a bitmap that's large. Bug:218845090 Bug:210690571 Test: Manually on device Change-Id: Ie759b99519ad77ee2af75c02b60d5db947808ed7 --- core/res/res/layout/notification_template_header.xml | 2 ++ core/res/res/layout/notification_template_material_base.xml | 6 +++++- core/res/res/layout/notification_template_right_icon.xml | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml index 81a79c50c3efe..a7f2aa7cba69a 100644 --- a/core/res/res/layout/notification_template_header.xml +++ b/core/res/res/layout/notification_template_header.xml @@ -49,6 +49,8 @@ android:layout_marginStart="@dimen/notification_icon_circle_start" android:background="@drawable/notification_icon_circle" android:padding="@dimen/notification_icon_circle_padding" + android:maxDrawableWidth="@dimen/notification_icon_circle_size" + android:maxDrawableHeight="@dimen/notification_icon_circle_size" /> diff --git a/core/res/res/layout/notification_template_material_base.xml b/core/res/res/layout/notification_template_material_base.xml index c6983ae5e0453..fd787f6ea470d 100644 --- a/core/res/res/layout/notification_template_material_base.xml +++ b/core/res/res/layout/notification_template_material_base.xml @@ -45,6 +45,8 @@ android:layout_marginStart="@dimen/notification_icon_circle_start" android:background="@drawable/notification_icon_circle" android:padding="@dimen/notification_icon_circle_padding" + android:maxDrawableWidth="@dimen/notification_icon_circle_size" + android:maxDrawableHeight="@dimen/notification_icon_circle_size" /> - -