323 lines
12 KiB
Java
323 lines
12 KiB
Java
/*
|
|
* 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 {
|
|
final Drawable iconDrawable = icon.loadDrawable(context);
|
|
if (iconDrawable == null) {
|
|
Log.w(TAG, "Couldn't load drawable for icon: " + icon);
|
|
}
|
|
return iconDrawable;
|
|
} 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.
|
|
} 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;
|
|
}
|
|
}
|