blob: 9ef7ce38fc09a023afd283f848d6cfcc6475b324 [file] [log] [blame]
/*
* Copyright (C) 2018 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.annotation.DrawableRes;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
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.text.TextUtils;
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";
/** There's no max size specified, load at original size. */
public static final int NO_MAX_SIZE = -1;
@VisibleForTesting
static final int DEFAULT_MAX_SAFE_ICON_SIZE_PX = 480;
/**
* Resolve an image from the given Uri using {@link ImageDecoder} if it contains a
* bitmap reference.
* Negative or zero dimensions will result in icon loaded in its original size.
*
* @throws IOException if the icon could not be loaded.
*/
@Nullable
public static Drawable resolveImage(Uri uri, Context context) throws IOException {
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 bitmap reference, or
* using {@link Icon#loadDrawable(Context)} otherwise. This will correctly apply the Icon's,
* tint, if present, to the drawable.
* Negative or zero dimensions will result in icon loaded in its original size.
*
* @return drawable or null if the passed icon parameter was null.
* @throws IOException if the icon could not be loaded.
*/
@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.
* Negative or zero dimensions will result in icon loaded in its original size.
*
* @return loaded icon or null if a null icon was passed as a parameter.
* @throws IOException if the icon could not be loaded.
*/
@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:
Resources res = resolveResourcesForIcon(context, icon);
if (res == null) {
// We couldn't resolve resources properly, fall back to icon loading.
return icon.loadDrawable(context);
}
Drawable result = resolveImage(res, icon.getResId(), 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 resolveImage(Resources res, @DrawableRes int resId, int maxWidth,
int maxHeight) {
final ImageDecoder.Source source = ImageDecoder.createSource(res, resId);
return resolveImage(source, maxWidth, maxHeight);
}
@Nullable
private static Drawable resolveBitmapImage(Icon icon, Context context, int maxWidth,
int maxHeight) {
if (maxWidth > 0 && maxHeight > 0) {
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) {
try {
return ImageDecoder.decodeDrawable(source, (decoder, info, unused) -> {
if (maxWidth <= 0 || maxHeight <= 0) {
return;
}
final Size size = info.getSize();
if (size.getWidth() <= maxWidth && size.getHeight() <= maxHeight) {
// We don't want to upscale images needlessly.
return;
}
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.d(TAG, "Couldn't use ImageDecoder for drawable, falling back to non-resized load.");
return null;
}
}
private static int getPowerOfTwoForSampleRatio(double ratio) {
final int k = Integer.highestOneBit((int) Math.floor(ratio));
return Math.max(1, k);
}
private static void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
int maxWidth, int maxHeight) {
final Size size = info.getSize();
final int originalSize = Math.max(size.getHeight(), size.getWidth());
final int maxSize = Math.max(maxWidth, maxHeight);
final double ratio = (originalSize > maxSize)
? originalSize * 1f / maxSize
: 1.0;
decoder.setTargetSampleSize(getPowerOfTwoForSampleRatio(ratio));
}
/**
* Gets the Uri for this icon, assuming the icon can be treated as a pure Uri. Null otherwise.
*/
@Nullable
private static Uri getResolvableUri(@Nullable Icon icon) {
if (icon == null || (icon.getType() != Icon.TYPE_URI
&& icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP)) {
return null;
}
return icon.getUri();
}
/**
* Resolves the correct resources package for a given Icon - it may come from another
* package.
*
* @see Icon#loadDrawableInner(Context)
* @hide
*
* @return resources instance if the operation succeeded, null otherwise
*/
@Nullable
@VisibleForTesting
public static Resources resolveResourcesForIcon(Context context, Icon icon) {
if (icon.getType() != Icon.TYPE_RESOURCE) {
return null;
}
// Icons cache resolved resources, use cache if available.
Resources res = icon.getResources();
if (res != null) {
return res;
}
String resPackage = icon.getResPackage();
// No package means we try to use current context.
if (TextUtils.isEmpty(resPackage) || context.getPackageName().equals(resPackage)) {
return context.getResources();
}
if ("android".equals(resPackage)) {
return Resources.getSystem();
}
final PackageManager pm = context.getPackageManager();
try {
ApplicationInfo ai = pm.getApplicationInfo(resPackage,
PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.GET_SHARED_LIBRARY_FILES);
if (ai != null) {
return pm.getResourcesForApplication(ai);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, String.format("Unable to resolve package %s for icon %s", resPackage, icon));
return null;
}
return null;
}
}