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.
Bug:218845090
Bug:210690571
Test: Manually on device - tested on Raven and new Pixel with multiple
notification sizes via notification test apk.
Unit tests
Change-Id: I504694cb27953bc7950669b1b48f90d2e9c6a68d
This commit is contained in:
@@ -16,21 +16,25 @@
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.annotation.DrawableRes;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
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.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;
|
||||
@VisibleForTesting
|
||||
static final int DEFAULT_MAX_SAFE_ICON_SIZE_PX = 480;
|
||||
|
||||
/**
|
||||
* Resolve an image from the given Uri using {@link ImageDecoder}
|
||||
@@ -38,9 +42,9 @@ public class LocalImageResolver {
|
||||
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;
|
||||
return ImageDecoder.decodeDrawable(source,
|
||||
(decoder, info, s) -> LocalImageResolver.onHeaderDecoded(decoder, info,
|
||||
DEFAULT_MAX_SAFE_ICON_SIZE_PX, DEFAULT_MAX_SAFE_ICON_SIZE_PX));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,17 +52,49 @@ public class LocalImageResolver {
|
||||
* using {@link Icon#loadDrawable(Context)} otherwise. This will correctly apply the Icon's,
|
||||
* tint, if present, to the drawable.
|
||||
*/
|
||||
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;
|
||||
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 Uri, or
|
||||
* using {@link Icon#loadDrawable(Context)} otherwise. This will correctly apply the Icon's,
|
||||
* tint, if present, to the drawable.
|
||||
*/
|
||||
@Nullable
|
||||
public static Drawable resolveImage(@Nullable Icon icon, Context context, int maxWidth,
|
||||
int maxHeight)
|
||||
throws IOException {
|
||||
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);
|
||||
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.
|
||||
return icon.loadDrawable(context);
|
||||
}
|
||||
|
||||
@@ -66,7 +102,71 @@ public class LocalImageResolver {
|
||||
throws IOException {
|
||||
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)
|
||||
throws IOException {
|
||||
final ImageDecoder.Source source = ImageDecoder.createSource(context.getResources(), resId);
|
||||
// It's possible that the resource isn't an actual bitmap drawable so this decode can fail.
|
||||
// Return null in that case.
|
||||
try {
|
||||
return resolveImage(source, maxWidth, maxHeight);
|
||||
} catch (ImageDecoder.DecodeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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)
|
||||
throws IOException {
|
||||
return ImageDecoder.decodeDrawable(source, (decoder, info, unused) -> {
|
||||
if (maxWidth <= 0 || maxHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Size size = info.getSize();
|
||||
if (size.getWidth() > size.getHeight()) {
|
||||
if (size.getWidth() > maxWidth) {
|
||||
@@ -88,11 +188,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));
|
||||
}
|
||||
|
||||
BIN
core/tests/coretests/res/drawable/big_a.png
Normal file
BIN
core/tests/coretests/res/drawable/big_a.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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 android.content.Context;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.AdaptiveIconDrawable;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
|
||||
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.android.frameworks.coretests.R;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@RunWith(AndroidJUnit4ClassRunner.class)
|
||||
public class LocalImageResolverTest {
|
||||
|
||||
private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
|
||||
@Test
|
||||
public void resolveImage_largeBitmapIcon_defaultSize_resizeToDefaultSize() throws
|
||||
IOException {
|
||||
Icon icon = Icon.createWithBitmap(
|
||||
BitmapFactory.decodeResource(mContext.getResources(), R.drawable.big_a));
|
||||
Drawable d = LocalImageResolver.resolveImage(icon, mContext);
|
||||
|
||||
assertThat(d).isInstanceOf(BitmapDrawable.class);
|
||||
BitmapDrawable bd = (BitmapDrawable) d;
|
||||
// No isLessOrEqualThan sadly.
|
||||
assertThat(bd.getBitmap().getWidth()).isLessThan(
|
||||
LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
|
||||
assertThat(bd.getBitmap().getHeight()).isLessThan(
|
||||
LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveImage_largeAdaptiveBitmapIcon_defaultSize_resizeToDefaultSize() throws
|
||||
IOException {
|
||||
Icon icon = Icon.createWithAdaptiveBitmap(
|
||||
BitmapFactory.decodeResource(mContext.getResources(), R.drawable.big_a));
|
||||
Drawable d = LocalImageResolver.resolveImage(icon, mContext);
|
||||
|
||||
assertThat(d).isInstanceOf(AdaptiveIconDrawable.class);
|
||||
BitmapDrawable bd = (BitmapDrawable) ((AdaptiveIconDrawable) d).getForeground();
|
||||
// No isLessOrEqualThan sadly.
|
||||
assertThat(bd.getBitmap().getWidth()).isLessThan(
|
||||
LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
|
||||
assertThat(bd.getBitmap().getHeight()).isLessThan(
|
||||
LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveImage_largeResourceIcon_defaultSize_resizeToDefaultSize() throws
|
||||
IOException {
|
||||
Icon icon = Icon.createWithResource(mContext, R.drawable.big_a);
|
||||
Drawable d = LocalImageResolver.resolveImage(icon, mContext);
|
||||
|
||||
assertThat(d).isInstanceOf(BitmapDrawable.class);
|
||||
BitmapDrawable bd = (BitmapDrawable) d;
|
||||
// No isLessOrEqualThan sadly.
|
||||
assertThat(bd.getBitmap().getWidth()).isLessThan(
|
||||
LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
|
||||
assertThat(bd.getBitmap().getHeight()).isLessThan(
|
||||
LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveImage_largeResourceIcon_passedSize_resizeToDefinedSize() throws
|
||||
IOException {
|
||||
Icon icon = Icon.createWithResource(mContext, R.drawable.big_a);
|
||||
Drawable d = LocalImageResolver.resolveImage(icon, mContext, 100, 50);
|
||||
|
||||
assertThat(d).isInstanceOf(BitmapDrawable.class);
|
||||
BitmapDrawable bd = (BitmapDrawable) d;
|
||||
assertThat(bd.getBitmap().getWidth()).isLessThan(101);
|
||||
assertThat(bd.getBitmap().getHeight()).isLessThan(51);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveImage_largeBitmapIcon_passedSize_resizeToDefinedSize() throws
|
||||
IOException {
|
||||
Icon icon = Icon.createWithBitmap(
|
||||
BitmapFactory.decodeResource(mContext.getResources(), R.drawable.big_a));
|
||||
Drawable d = LocalImageResolver.resolveImage(icon, mContext, 100, 50);
|
||||
|
||||
assertThat(d).isInstanceOf(BitmapDrawable.class);
|
||||
BitmapDrawable bd = (BitmapDrawable) d;
|
||||
assertThat(bd.getBitmap().getWidth()).isLessThan(101);
|
||||
assertThat(bd.getBitmap().getHeight()).isLessThan(51);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveImage_largeAdaptiveBitmapIcon_passedSize_resizeToDefinedSize() throws
|
||||
IOException {
|
||||
Icon icon = Icon.createWithAdaptiveBitmap(
|
||||
BitmapFactory.decodeResource(mContext.getResources(), R.drawable.big_a));
|
||||
Drawable d = LocalImageResolver.resolveImage(icon, mContext, 100, 50);
|
||||
|
||||
assertThat(d).isInstanceOf(AdaptiveIconDrawable.class);
|
||||
BitmapDrawable bd = (BitmapDrawable) ((AdaptiveIconDrawable) d).getForeground();
|
||||
assertThat(bd.getBitmap().getWidth()).isLessThan(101);
|
||||
assertThat(bd.getBitmap().getHeight()).isLessThan(51);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void resolveImage_smallResourceIcon_defaultSize_untouched() throws IOException {
|
||||
Icon icon = Icon.createWithResource(mContext, R.drawable.test32x24);
|
||||
Drawable d = LocalImageResolver.resolveImage(icon, mContext);
|
||||
|
||||
assertThat(d).isInstanceOf(BitmapDrawable.class);
|
||||
BitmapDrawable bd = (BitmapDrawable) d;
|
||||
assertThat(bd.getBitmap().getWidth()).isEqualTo(32);
|
||||
assertThat(bd.getBitmap().getHeight()).isEqualTo(24);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveImage_smallBitmapIcon_defaultSize_untouched() throws IOException {
|
||||
Icon icon = Icon.createWithBitmap(
|
||||
BitmapFactory.decodeResource(mContext.getResources(), R.drawable.test32x24));
|
||||
final int originalWidth = icon.getBitmap().getWidth();
|
||||
final int originalHeight = icon.getBitmap().getHeight();
|
||||
|
||||
Drawable d = LocalImageResolver.resolveImage(icon, mContext);
|
||||
|
||||
assertThat(d).isInstanceOf(BitmapDrawable.class);
|
||||
BitmapDrawable bd = (BitmapDrawable) d;
|
||||
assertThat(bd.getBitmap().getWidth()).isEqualTo(originalWidth);
|
||||
assertThat(bd.getBitmap().getHeight()).isEqualTo(originalHeight);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveImage_smallAdaptiveBitmapIcon_defaultSize_untouched() throws IOException {
|
||||
Icon icon = Icon.createWithAdaptiveBitmap(
|
||||
BitmapFactory.decodeResource(mContext.getResources(), R.drawable.test32x24));
|
||||
final int originalWidth = icon.getBitmap().getWidth();
|
||||
final int originalHeight = icon.getBitmap().getHeight();
|
||||
|
||||
Drawable d = LocalImageResolver.resolveImage(icon, mContext);
|
||||
assertThat(d).isInstanceOf(AdaptiveIconDrawable.class);
|
||||
BitmapDrawable bd = (BitmapDrawable) ((AdaptiveIconDrawable) d).getForeground();
|
||||
assertThat(bd.getBitmap().getWidth()).isEqualTo(originalWidth);
|
||||
assertThat(bd.getBitmap().getHeight()).isEqualTo(originalHeight);
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user