| /* |
| * 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; |
| } |
| } |