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