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.systemui.statusbar.notification.row;
18 
19 import android.app.ActivityManager;
20 import android.app.Notification;
21 import android.content.Context;
22 import android.graphics.Bitmap;
23 import android.graphics.drawable.BitmapDrawable;
24 import android.graphics.drawable.Drawable;
25 import android.graphics.drawable.Icon;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.Parcelable;
29 import android.util.Log;
30 
31 import com.android.internal.R;
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.widget.ImageResolver;
34 import com.android.internal.widget.LocalImageResolver;
35 import com.android.internal.widget.MessagingMessage;
36 
37 import java.io.IOException;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Set;
41 
42 /**
43  * Custom resolver with built-in image cache for image messages.
44  *
45  * If the URL points to a bitmap that's larger than the maximum width or height, the bitmap
46  * will be resized down to that maximum size before being cached. See {@link #getMaxImageWidth()},
47  * {@link #getMaxImageHeight()}, and {@link #resolveImage(Uri)} for the downscaling implementation.
48  */
49 public class NotificationInlineImageResolver implements ImageResolver {
50     private static final String TAG = NotificationInlineImageResolver.class.getSimpleName();
51 
52     private final Context mContext;
53     private final ImageCache mImageCache;
54     private Set<Uri> mWantedUriSet;
55 
56     // max allowed bitmap width, in pixels
57     @VisibleForTesting
58     protected int mMaxImageWidth;
59     // max allowed bitmap height, in pixels
60     @VisibleForTesting
61     protected int mMaxImageHeight;
62 
63     /**
64      * Constructor.
65      * @param context    Context.
66      * @param imageCache The implementation of internal cache.
67      */
NotificationInlineImageResolver(Context context, ImageCache imageCache)68     public NotificationInlineImageResolver(Context context, ImageCache imageCache) {
69         mContext = context.getApplicationContext();
70         mImageCache = imageCache;
71 
72         if (mImageCache != null) {
73             mImageCache.setImageResolver(this);
74         }
75 
76         updateMaxImageSizes();
77     }
78 
79     /**
80      * Check if this resolver has its internal cache implementation.
81      * @return True if has its internal cache, false otherwise.
82      */
hasCache()83     public boolean hasCache() {
84         return mImageCache != null && !ActivityManager.isLowRamDeviceStatic();
85     }
86 
isLowRam()87     private boolean isLowRam() {
88         return ActivityManager.isLowRamDeviceStatic();
89     }
90 
91     /**
92      * Update the maximum width and height allowed for bitmaps, ex. after a configuration change.
93      */
updateMaxImageSizes()94     public void updateMaxImageSizes() {
95         mMaxImageWidth = getMaxImageWidth();
96         mMaxImageHeight = getMaxImageHeight();
97     }
98 
99     @VisibleForTesting
getMaxImageWidth()100     protected int getMaxImageWidth() {
101         return mContext.getResources().getDimensionPixelSize(isLowRam()
102                 ? R.dimen.notification_custom_view_max_image_width_low_ram
103                 : R.dimen.notification_custom_view_max_image_width);
104     }
105 
106     @VisibleForTesting
getMaxImageHeight()107     protected int getMaxImageHeight() {
108         return mContext.getResources().getDimensionPixelSize(isLowRam()
109                 ? R.dimen.notification_custom_view_max_image_height_low_ram
110                 : R.dimen.notification_custom_view_max_image_height);
111     }
112 
113     @VisibleForTesting
resolveImageInternal(Uri uri)114     protected BitmapDrawable resolveImageInternal(Uri uri) throws IOException {
115         return (BitmapDrawable) LocalImageResolver.resolveImage(uri, mContext);
116     }
117 
118     /**
119      * To resolve image from specified uri directly. If the resulting image is larger than the
120      * maximum allowed size, scale it down.
121      * @param uri Uri of the image.
122      * @return Drawable of the image.
123      * @throws IOException Throws if failed at resolving the image.
124      */
resolveImage(Uri uri)125     Drawable resolveImage(Uri uri) throws IOException {
126         BitmapDrawable image = resolveImageInternal(uri);
127         if (image == null || image.getBitmap() == null) {
128             throw new IOException("resolveImageInternal returned null for uri: " + uri);
129         }
130         Bitmap bitmap = image.getBitmap();
131         image.setBitmap(Icon.scaleDownIfNecessary(bitmap, mMaxImageWidth, mMaxImageHeight));
132         return image;
133     }
134 
135     @Override
loadImage(Uri uri)136     public Drawable loadImage(Uri uri) {
137         Drawable result = null;
138         try {
139             if (hasCache()) {
140                 // if the uri isn't currently cached, try caching it first
141                 if (!mImageCache.hasEntry(uri)) {
142                     mImageCache.preload((uri));
143                 }
144                 result = mImageCache.get(uri);
145             } else {
146                 result = resolveImage(uri);
147             }
148         } catch (IOException | SecurityException ex) {
149             Log.d(TAG, "loadImage: Can't load image from " + uri, ex);
150         }
151         return result;
152     }
153 
154     /**
155      * Resolve the message list from specified notification and
156      * refresh internal cache according to the result.
157      * @param notification The Notification to be resolved.
158      */
preloadImages(Notification notification)159     public void preloadImages(Notification notification) {
160         if (!hasCache()) {
161             return;
162         }
163 
164         retrieveWantedUriSet(notification);
165         Set<Uri> wantedSet = getWantedUriSet();
166         wantedSet.forEach(uri -> {
167             if (!mImageCache.hasEntry(uri)) {
168                 // The uri is not in the cache, we need trigger a loading task for it.
169                 mImageCache.preload(uri);
170             }
171         });
172     }
173 
174     /**
175      * Try to purge unnecessary cache entries.
176      */
purgeCache()177     public void purgeCache() {
178         if (!hasCache()) {
179             return;
180         }
181         mImageCache.purge();
182     }
183 
retrieveWantedUriSet(Notification notification)184     private void retrieveWantedUriSet(Notification notification) {
185         Parcelable[] messages;
186         Parcelable[] historicMessages;
187         List<Notification.MessagingStyle.Message> messageList;
188         List<Notification.MessagingStyle.Message> historicList;
189         Set<Uri> result = new HashSet<>();
190 
191         Bundle extras = notification.extras;
192         if (extras == null) {
193             return;
194         }
195 
196         messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
197         messageList = messages == null ? null :
198                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
199         if (messageList != null) {
200             for (Notification.MessagingStyle.Message message : messageList) {
201                 if (MessagingMessage.hasImage(message)) {
202                     result.add(message.getDataUri());
203                 }
204             }
205         }
206 
207         historicMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
208         historicList = historicMessages == null ? null :
209                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(historicMessages);
210         if (historicList != null) {
211             for (Notification.MessagingStyle.Message historic : historicList) {
212                 if (MessagingMessage.hasImage(historic)) {
213                     result.add(historic.getDataUri());
214                 }
215             }
216         }
217 
218         mWantedUriSet = result;
219     }
220 
getWantedUriSet()221     Set<Uri> getWantedUriSet() {
222         return mWantedUriSet;
223     }
224 
225     /**
226      * A interface for internal cache implementation of this resolver.
227      */
228     interface ImageCache {
229         /**
230          * Load the image from cache first then resolve from uri if missed the cache.
231          * @param uri The uri of the image.
232          * @return Drawable of the image.
233          */
get(Uri uri)234         Drawable get(Uri uri);
235 
236         /**
237          * Set the image resolver that actually resolves image from specified uri.
238          * @param resolver The resolver implementation that resolves image from specified uri.
239          */
setImageResolver(NotificationInlineImageResolver resolver)240         void setImageResolver(NotificationInlineImageResolver resolver);
241 
242         /**
243          * Check if the uri is in the cache no matter it is loading or loaded.
244          * @param uri The uri to check.
245          * @return True if it is already in the cache; false otherwise.
246          */
hasEntry(Uri uri)247         boolean hasEntry(Uri uri);
248 
249         /**
250          * Start a new loading task for the target uri.
251          * @param uri The target to load.
252          */
preload(Uri uri)253         void preload(Uri uri);
254 
255         /**
256          * Purge unnecessary entries in the cache.
257          */
purge()258         void purge();
259     }
260 
261 }
262