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 final Drawable iconDrawable = icon.loadDrawable(context); 136 if (iconDrawable == null) { 137 Log.w(TAG, "Couldn't load drawable for icon: " + icon); 138 } 139 return iconDrawable; 140 } catch (Resources.NotFoundException e) { 141 return null; 142 } 143 } 144 145 /** 146 * Attempts to resolve the resource as a bitmap drawable constrained within max sizes. 147 */ 148 @Nullable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight)149 public static Drawable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight) { 150 final ImageDecoder.Source source = 151 ImageDecoder.createSource(context.getContentResolver(), uri); 152 return resolveImage(source, maxWidth, maxHeight); 153 } 154 155 /** 156 * Attempts to resolve the resource as a bitmap drawable constrained within max sizes. 157 * 158 * @return decoded drawable or null if the passed resource is not a straight bitmap 159 */ 160 @Nullable resolveImage(@rawableRes int resId, Context context, int maxWidth, int maxHeight)161 public static Drawable resolveImage(@DrawableRes int resId, Context context, int maxWidth, 162 int maxHeight) { 163 final ImageDecoder.Source source = ImageDecoder.createSource(context.getResources(), resId); 164 return resolveImage(source, maxWidth, maxHeight); 165 } 166 167 @Nullable resolveImage(Resources res, @DrawableRes int resId, int maxWidth, int maxHeight)168 private static Drawable resolveImage(Resources res, @DrawableRes int resId, int maxWidth, 169 int maxHeight) { 170 final ImageDecoder.Source source = ImageDecoder.createSource(res, resId); 171 return resolveImage(source, maxWidth, maxHeight); 172 } 173 174 @Nullable resolveBitmapImage(Icon icon, Context context, int maxWidth, int maxHeight)175 private static Drawable resolveBitmapImage(Icon icon, Context context, int maxWidth, 176 int maxHeight) { 177 178 if (maxWidth > 0 && maxHeight > 0) { 179 Bitmap bitmap = icon.getBitmap(); 180 if (bitmap == null) { 181 return null; 182 } 183 184 if (bitmap.getWidth() > maxWidth || bitmap.getHeight() > maxHeight) { 185 Icon smallerIcon = icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP 186 ? Icon.createWithAdaptiveBitmap(bitmap) : Icon.createWithBitmap(bitmap); 187 // We don't want to modify the source icon, create a copy. 188 smallerIcon.setTintList(icon.getTintList()) 189 .setTintBlendMode(icon.getTintBlendMode()) 190 .scaleDownIfNecessary(maxWidth, maxHeight); 191 return smallerIcon.loadDrawable(context); 192 } 193 } 194 195 return icon.loadDrawable(context); 196 } 197 198 @Nullable tintDrawable(Icon icon, @Nullable Drawable drawable)199 private static Drawable tintDrawable(Icon icon, @Nullable Drawable drawable) { 200 if (drawable == null) { 201 return null; 202 } 203 204 if (icon.hasTint()) { 205 drawable.mutate(); 206 drawable.setTintList(icon.getTintList()); 207 drawable.setTintBlendMode(icon.getTintBlendMode()); 208 } 209 210 return drawable; 211 } 212 resolveImage(ImageDecoder.Source source, int maxWidth, int maxHeight)213 private static Drawable resolveImage(ImageDecoder.Source source, int maxWidth, int maxHeight) { 214 try { 215 return ImageDecoder.decodeDrawable(source, (decoder, info, unused) -> { 216 if (maxWidth <= 0 || maxHeight <= 0) { 217 return; 218 } 219 220 final Size size = info.getSize(); 221 if (size.getWidth() <= maxWidth && size.getHeight() <= maxHeight) { 222 // We don't want to upscale images needlessly. 223 return; 224 } 225 226 if (size.getWidth() > size.getHeight()) { 227 if (size.getWidth() > maxWidth) { 228 final int targetHeight = size.getHeight() * maxWidth / size.getWidth(); 229 decoder.setTargetSize(maxWidth, targetHeight); 230 } 231 } else { 232 if (size.getHeight() > maxHeight) { 233 final int targetWidth = size.getWidth() * maxHeight / size.getHeight(); 234 decoder.setTargetSize(targetWidth, maxHeight); 235 } 236 } 237 }); 238 239 // ImageDecoder documentation is misleading a bit - it'll throw NotFoundException 240 // in some cases despite it not saying so. 241 } catch (IOException | Resources.NotFoundException e) { 242 Log.d(TAG, "Couldn't use ImageDecoder for drawable, falling back to non-resized load."); 243 return null; 244 } 245 } 246 247 private static int getPowerOfTwoForSampleRatio(double ratio) { 248 final int k = Integer.highestOneBit((int) Math.floor(ratio)); 249 return Math.max(1, k); 250 } 251 252 private static void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 253 int maxWidth, int maxHeight) { 254 final Size size = info.getSize(); 255 final int originalSize = Math.max(size.getHeight(), size.getWidth()); 256 final int maxSize = Math.max(maxWidth, maxHeight); 257 final double ratio = (originalSize > maxSize) 258 ? originalSize * 1f / maxSize 259 : 1.0; 260 decoder.setTargetSampleSize(getPowerOfTwoForSampleRatio(ratio)); 261 } 262 263 /** 264 * Gets the Uri for this icon, assuming the icon can be treated as a pure Uri. Null otherwise. 265 */ 266 @Nullable 267 private static Uri getResolvableUri(@Nullable Icon icon) { 268 if (icon == null || (icon.getType() != Icon.TYPE_URI 269 && icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP)) { 270 return null; 271 } 272 return icon.getUri(); 273 } 274 275 /** 276 * Resolves the correct resources package for a given Icon - it may come from another 277 * package. 278 * 279 * @see Icon#loadDrawableInner(Context) 280 * @hide 281 * 282 * @return resources instance if the operation succeeded, null otherwise 283 */ 284 @Nullable 285 @VisibleForTesting 286 public static Resources resolveResourcesForIcon(Context context, Icon icon) { 287 if (icon.getType() != Icon.TYPE_RESOURCE) { 288 return null; 289 } 290 291 // Icons cache resolved resources, use cache if available. 292 Resources res = icon.getResources(); 293 if (res != null) { 294 return res; 295 } 296 297 String resPackage = icon.getResPackage(); 298 // No package means we try to use current context. 299 if (TextUtils.isEmpty(resPackage) || context.getPackageName().equals(resPackage)) { 300 return context.getResources(); 301 } 302 303 if ("android".equals(resPackage)) { 304 return Resources.getSystem(); 305 } 306 307 final PackageManager pm = context.getPackageManager(); 308 try { 309 ApplicationInfo ai = pm.getApplicationInfo(resPackage, 310 PackageManager.MATCH_UNINSTALLED_PACKAGES 311 | PackageManager.GET_SHARED_LIBRARY_FILES); 312 if (ai != null) { 313 return pm.getResourcesForApplication(ai); 314 } 315 } catch (PackageManager.NameNotFoundException e) { 316 Log.e(TAG, String.format("Unable to resolve package %s for icon %s", resPackage, icon)); 317 return null; 318 } 319 320 return null; 321 } 322 } 323