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