1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.internal.widget; 18 19 import android.annotation.DrawableRes; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.content.res.Resources; 25 import android.graphics.Bitmap; 26 import android.graphics.ImageDecoder; 27 import android.graphics.drawable.Drawable; 28 import android.graphics.drawable.Icon; 29 import android.net.Uri; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.util.Size; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 36 import java.io.IOException; 37 38 /** A class to extract Drawables from a MessagingStyle/ConversationStyle message. */ 39 public class LocalImageResolver { 40 41 private static final String TAG = "LocalImageResolver"; 42 43 /** There's no max size specified, load at original size. */ 44 public static final int NO_MAX_SIZE = -1; 45 46 @VisibleForTesting 47 static final int DEFAULT_MAX_SAFE_ICON_SIZE_PX = 480; 48 49 /** 50 * Resolve an image from the given Uri using {@link ImageDecoder} if it contains a 51 * bitmap reference. 52 * Negative or zero dimensions will result in icon loaded in its original size. 53 * 54 * @throws IOException if the icon could not be loaded. 55 */ 56 @Nullable resolveImage(Uri uri, Context context)57 public static Drawable resolveImage(Uri uri, Context context) throws IOException { 58 try { 59 final ImageDecoder.Source source = 60 ImageDecoder.createSource(context.getContentResolver(), uri); 61 return ImageDecoder.decodeDrawable(source, 62 (decoder, info, s) -> LocalImageResolver.onHeaderDecoded(decoder, info, 63 DEFAULT_MAX_SAFE_ICON_SIZE_PX, DEFAULT_MAX_SAFE_ICON_SIZE_PX)); 64 } catch (Exception e) { 65 // Invalid drawable resource can actually throw either NullPointerException or 66 // ResourceNotFoundException. This sanitizes to expected output. 67 throw new IOException(e); 68 } 69 } 70 71 /** 72 * Get the drawable from Icon using {@link ImageDecoder} if it contains a bitmap reference, or 73 * using {@link Icon#loadDrawable(Context)} otherwise. This will correctly apply the Icon's, 74 * tint, if present, to the drawable. 75 * Negative or zero dimensions will result in icon loaded in its original size. 76 * 77 * @return drawable or null if the passed icon parameter was null. 78 * @throws IOException if the icon could not be loaded. 79 */ 80 @Nullable resolveImage(@ullable Icon icon, Context context)81 public static Drawable resolveImage(@Nullable Icon icon, Context context) throws IOException { 82 return resolveImage(icon, context, DEFAULT_MAX_SAFE_ICON_SIZE_PX, 83 DEFAULT_MAX_SAFE_ICON_SIZE_PX); 84 } 85 86 /** 87 * Get the drawable from Icon using {@link ImageDecoder} if it contains a bitmap reference, or 88 * using {@link Icon#loadDrawable(Context)} otherwise. This will correctly apply the Icon's, 89 * tint, if present, to the drawable. 90 * Negative or zero dimensions will result in icon loaded in its original size. 91 * 92 * @return loaded icon or null if a null icon was passed as a parameter. 93 * @throws IOException if the icon could not be loaded. 94 */ 95 @Nullable resolveImage(@ullable Icon icon, Context context, int maxWidth, int maxHeight)96 public static Drawable resolveImage(@Nullable Icon icon, Context context, int maxWidth, 97 int maxHeight) { 98 if (icon == null) { 99 return null; 100 } 101 102 switch (icon.getType()) { 103 case Icon.TYPE_URI: 104 case Icon.TYPE_URI_ADAPTIVE_BITMAP: 105 Uri uri = getResolvableUri(icon); 106 if (uri != null) { 107 Drawable result = resolveImage(uri, context, maxWidth, maxHeight); 108 if (result != null) { 109 return tintDrawable(icon, result); 110 } 111 } 112 break; 113 case Icon.TYPE_RESOURCE: 114 Resources res = resolveResourcesForIcon(context, icon); 115 if (res == null) { 116 // We couldn't resolve resources properly, fall back to icon loading. 117 return icon.loadDrawable(context); 118 } 119 120 Drawable result = resolveImage(res, icon.getResId(), maxWidth, maxHeight); 121 if (result != null) { 122 return tintDrawable(icon, result); 123 } 124 break; 125 case Icon.TYPE_BITMAP: 126 case Icon.TYPE_ADAPTIVE_BITMAP: 127 return resolveBitmapImage(icon, context, maxWidth, maxHeight); 128 case Icon.TYPE_DATA: // We can't really improve on raw data images. 129 default: 130 break; 131 } 132 133 // Fallback to straight drawable load if we fail with more efficient approach. 134 try { 135 return icon.loadDrawable(context); 136 } catch (Resources.NotFoundException e) { 137 return null; 138 } 139 } 140 141 /** 142 * Attempts to resolve the resource as a bitmap drawable constrained within max sizes. 143 */ 144 @Nullable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight)145 public static Drawable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight) { 146 final ImageDecoder.Source source = 147 ImageDecoder.createSource(context.getContentResolver(), uri); 148 return resolveImage(source, maxWidth, maxHeight); 149 } 150 151 /** 152 * Attempts to resolve the resource as a bitmap drawable constrained within max sizes. 153 * 154 * @return decoded drawable or null if the passed resource is not a straight bitmap 155 */ 156 @Nullable resolveImage(@rawableRes int resId, Context context, int maxWidth, int maxHeight)157 public static Drawable resolveImage(@DrawableRes int resId, Context context, int maxWidth, 158 int maxHeight) { 159 final ImageDecoder.Source source = ImageDecoder.createSource(context.getResources(), resId); 160 return resolveImage(source, maxWidth, maxHeight); 161 } 162 163 @Nullable resolveImage(Resources res, @DrawableRes int resId, int maxWidth, int maxHeight)164 private static Drawable resolveImage(Resources res, @DrawableRes int resId, int maxWidth, 165 int maxHeight) { 166 final ImageDecoder.Source source = ImageDecoder.createSource(res, resId); 167 return resolveImage(source, maxWidth, maxHeight); 168 } 169 170 @Nullable resolveBitmapImage(Icon icon, Context context, int maxWidth, int maxHeight)171 private static Drawable resolveBitmapImage(Icon icon, Context context, int maxWidth, 172 int maxHeight) { 173 174 if (maxWidth > 0 && maxHeight > 0) { 175 Bitmap bitmap = icon.getBitmap(); 176 if (bitmap == null) { 177 return null; 178 } 179 180 if (bitmap.getWidth() > maxWidth || bitmap.getHeight() > maxHeight) { 181 Icon smallerIcon = icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP 182 ? Icon.createWithAdaptiveBitmap(bitmap) : Icon.createWithBitmap(bitmap); 183 // We don't want to modify the source icon, create a copy. 184 smallerIcon.setTintList(icon.getTintList()) 185 .setTintBlendMode(icon.getTintBlendMode()) 186 .scaleDownIfNecessary(maxWidth, maxHeight); 187 return smallerIcon.loadDrawable(context); 188 } 189 } 190 191 return icon.loadDrawable(context); 192 } 193 194 @Nullable tintDrawable(Icon icon, @Nullable Drawable drawable)195 private static Drawable tintDrawable(Icon icon, @Nullable Drawable drawable) { 196 if (drawable == null) { 197 return null; 198 } 199 200 if (icon.hasTint()) { 201 drawable.mutate(); 202 drawable.setTintList(icon.getTintList()); 203 drawable.setTintBlendMode(icon.getTintBlendMode()); 204 } 205 206 return drawable; 207 } 208 resolveImage(ImageDecoder.Source source, int maxWidth, int maxHeight)209 private static Drawable resolveImage(ImageDecoder.Source source, int maxWidth, int maxHeight) { 210 try { 211 return ImageDecoder.decodeDrawable(source, (decoder, info, unused) -> { 212 if (maxWidth <= 0 || maxHeight <= 0) { 213 return; 214 } 215 216 final Size size = info.getSize(); 217 if (size.getWidth() <= maxWidth && size.getHeight() <= maxHeight) { 218 // We don't want to upscale images needlessly. 219 return; 220 } 221 222 if (size.getWidth() > size.getHeight()) { 223 if (size.getWidth() > maxWidth) { 224 final int targetHeight = size.getHeight() * maxWidth / size.getWidth(); 225 decoder.setTargetSize(maxWidth, targetHeight); 226 } 227 } else { 228 if (size.getHeight() > maxHeight) { 229 final int targetWidth = size.getWidth() * maxHeight / size.getHeight(); 230 decoder.setTargetSize(targetWidth, maxHeight); 231 } 232 } 233 }); 234 235 // ImageDecoder documentation is misleading a bit - it'll throw NotFoundException 236 // in some cases despite it not saying so. Rethrow it as an IOException to keep 237 // our API contract. 238 } catch (IOException | Resources.NotFoundException e) { 239 Log.d(TAG, "Couldn't use ImageDecoder for drawable, falling back to non-resized load."); 240 return null; 241 } 242 } 243 244 private static int getPowerOfTwoForSampleRatio(double ratio) { 245 final int k = Integer.highestOneBit((int) Math.floor(ratio)); 246 return Math.max(1, k); 247 } 248 249 private static void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 250 int maxWidth, int maxHeight) { 251 final Size size = info.getSize(); 252 final int originalSize = Math.max(size.getHeight(), size.getWidth()); 253 final int maxSize = Math.max(maxWidth, maxHeight); 254 final double ratio = (originalSize > maxSize) 255 ? originalSize * 1f / maxSize 256 : 1.0; 257 decoder.setTargetSampleSize(getPowerOfTwoForSampleRatio(ratio)); 258 } 259 260 /** 261 * Gets the Uri for this icon, assuming the icon can be treated as a pure Uri. Null otherwise. 262 */ 263 @Nullable 264 private static Uri getResolvableUri(@Nullable Icon icon) { 265 if (icon == null || (icon.getType() != Icon.TYPE_URI 266 && icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP)) { 267 return null; 268 } 269 return icon.getUri(); 270 } 271 272 /** 273 * Resolves the correct resources package for a given Icon - it may come from another 274 * package. 275 * 276 * @see Icon#loadDrawableInner(Context) 277 * @hide 278 * 279 * @return resources instance if the operation succeeded, null otherwise 280 */ 281 @Nullable 282 @VisibleForTesting 283 public static Resources resolveResourcesForIcon(Context context, Icon icon) { 284 if (icon.getType() != Icon.TYPE_RESOURCE) { 285 return null; 286 } 287 288 // Icons cache resolved resources, use cache if available. 289 Resources res = icon.getResources(); 290 if (res != null) { 291 return res; 292 } 293 294 String resPackage = icon.getResPackage(); 295 // No package means we try to use current context. 296 if (TextUtils.isEmpty(resPackage) || context.getPackageName().equals(resPackage)) { 297 return context.getResources(); 298 } 299 300 if ("android".equals(resPackage)) { 301 return Resources.getSystem(); 302 } 303 304 final PackageManager pm = context.getPackageManager(); 305 try { 306 ApplicationInfo ai = pm.getApplicationInfo(resPackage, 307 PackageManager.MATCH_UNINSTALLED_PACKAGES 308 | PackageManager.GET_SHARED_LIBRARY_FILES); 309 if (ai != null) { 310 return pm.getResourcesForApplication(ai); 311 } 312 } catch (PackageManager.NameNotFoundException e) { 313 Log.e(TAG, String.format("Unable to resolve package %s for icon %s", resPackage, icon)); 314 return null; 315 } 316 317 return null; 318 } 319 } 320