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