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.os.SystemClock;
27 import android.util.Log;
28 
29 import com.android.internal.R;
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.internal.widget.ImageResolver;
32 import com.android.internal.widget.LocalImageResolver;
33 import com.android.internal.widget.MessagingMessage;
34 
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     // Timeout for loading images from ImageCache when calling from UI thread
50     private static final long MAX_UI_THREAD_TIMEOUT_MS = 100L;
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;
70         mImageCache = imageCache;
71 
72         if (mImageCache != null) {
73             mImageCache.setImageResolver(this);
74         }
75 
76         updateMaxImageSizes();
77     }
78 
79     @VisibleForTesting
getContext()80     public Context getContext() {
81         return mContext;
82     }
83 
84     /**
85      * Check if this resolver has its internal cache implementation.
86      * @return True if has its internal cache, false otherwise.
87      */
hasCache()88     public boolean hasCache() {
89         return mImageCache != null && !isLowRam();
90     }
91 
isLowRam()92     private boolean isLowRam() {
93         return ActivityManager.isLowRamDeviceStatic();
94     }
95 
96     /**
97      * Update the maximum width and height allowed for bitmaps, ex. after a configuration change.
98      */
updateMaxImageSizes()99     public void updateMaxImageSizes() {
100         mMaxImageWidth = getMaxImageWidth();
101         mMaxImageHeight = getMaxImageHeight();
102     }
103 
104     @VisibleForTesting
getMaxImageWidth()105     protected int getMaxImageWidth() {
106         return mContext.getResources().getDimensionPixelSize(isLowRam()
107                 ? R.dimen.notification_custom_view_max_image_width_low_ram
108                 : R.dimen.notification_custom_view_max_image_width);
109     }
110 
111     @VisibleForTesting
getMaxImageHeight()112     protected int getMaxImageHeight() {
113         return mContext.getResources().getDimensionPixelSize(isLowRam()
114                 ? R.dimen.notification_custom_view_max_image_height_low_ram
115                 : R.dimen.notification_custom_view_max_image_height);
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, or null if unable to load.
123      */
resolveImage(Uri uri)124     Drawable resolveImage(Uri uri) {
125         try {
126             return LocalImageResolver.resolveImage(uri, mContext, mMaxImageWidth, mMaxImageHeight);
127         } catch (Exception ex) {
128             // Catch general Exception because ContentResolver can re-throw arbitrary Exception
129             // from remote process as a RuntimeException. See: Parcel#readException
130             Log.d(TAG, "resolveImage: Can't load image from " + uri, ex);
131         }
132         return null;
133     }
134 
135     /**
136      * Loads an image from the Uri.
137      * This method is synchronous and is usually called from the Main thread.
138      * It will time-out after MAX_UI_THREAD_TIMEOUT_MS.
139      *
140      * @param uri Uri of the target image.
141      * @return drawable of the image, null if loading failed/timeout
142      */
143     @Override
loadImage(Uri uri)144     public Drawable loadImage(Uri uri) {
145         return hasCache() ? loadImageFromCache(uri, MAX_UI_THREAD_TIMEOUT_MS) : resolveImage(uri);
146     }
147 
loadImageFromCache(Uri uri, long timeoutMs)148     private Drawable loadImageFromCache(Uri uri, long timeoutMs) {
149         // if the uri isn't currently cached, try caching it first
150         if (!mImageCache.hasEntry(uri)) {
151             mImageCache.preload((uri));
152         }
153         return mImageCache.get(uri, timeoutMs);
154     }
155 
156     /**
157      * Resolve the message list from specified notification and
158      * refresh internal cache according to the result.
159      * @param notification The Notification to be resolved.
160      */
preloadImages(Notification notification)161     public void preloadImages(Notification notification) {
162         if (!hasCache()) {
163             return;
164         }
165 
166         retrieveWantedUriSet(notification);
167         Set<Uri> wantedSet = getWantedUriSet();
168         wantedSet.forEach(uri -> {
169             if (!mImageCache.hasEntry(uri)) {
170                 // The uri is not in the cache, we need trigger a loading task for it.
171                 mImageCache.preload(uri);
172             }
173         });
174     }
175 
176     /**
177      * Try to purge unnecessary cache entries.
178      */
purgeCache()179     public void purgeCache() {
180         if (!hasCache()) {
181             return;
182         }
183         mImageCache.purge();
184     }
185 
retrieveWantedUriSet(Notification notification)186     private void retrieveWantedUriSet(Notification notification) {
187         Parcelable[] messages;
188         Parcelable[] historicMessages;
189         List<Notification.MessagingStyle.Message> messageList;
190         List<Notification.MessagingStyle.Message> historicList;
191         Set<Uri> result = new HashSet<>();
192 
193         Bundle extras = notification.extras;
194         if (extras == null) {
195             return;
196         }
197 
198         messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
199         messageList = messages == null ? null :
200                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
201         if (messageList != null) {
202             for (Notification.MessagingStyle.Message message : messageList) {
203                 if (MessagingMessage.hasImage(message)) {
204                     result.add(message.getDataUri());
205                 }
206             }
207         }
208 
209         historicMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
210         historicList = historicMessages == null ? null :
211                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(historicMessages);
212         if (historicList != null) {
213             for (Notification.MessagingStyle.Message historic : historicList) {
214                 if (MessagingMessage.hasImage(historic)) {
215                     result.add(historic.getDataUri());
216                 }
217             }
218         }
219 
220         mWantedUriSet = result;
221     }
222 
getWantedUriSet()223     Set<Uri> getWantedUriSet() {
224         return mWantedUriSet;
225     }
226 
227     /**
228      * Wait for a maximum timeout for images to finish preloading
229      * @param timeoutMs total timeout time
230      */
waitForPreloadedImages(long timeoutMs)231     void waitForPreloadedImages(long timeoutMs) {
232         if (!hasCache()) {
233             return;
234         }
235         Set<Uri> preloadedUris = getWantedUriSet();
236         if (preloadedUris != null) {
237             // Decrement remaining timeout after each image check
238             long endTimeMs = SystemClock.elapsedRealtime() + timeoutMs;
239             preloadedUris.forEach(
240                     uri -> loadImageFromCache(uri, endTimeMs - SystemClock.elapsedRealtime()));
241         }
242     }
243 
cancelRunningTasks()244     void cancelRunningTasks() {
245         if (!hasCache()) {
246             return;
247         }
248         mImageCache.cancelRunningTasks();
249     }
250 
251     /**
252      * A interface for internal cache implementation of this resolver.
253      */
254     interface ImageCache {
255         /**
256          * Load the image from cache first then resolve from uri if missed the cache.
257          * @param uri The uri of the image.
258          * @return Drawable of the image.
259          */
get(Uri uri, long timeoutMs)260         Drawable get(Uri uri, long timeoutMs);
261 
262         /**
263          * Set the image resolver that actually resolves image from specified uri.
264          * @param resolver The resolver implementation that resolves image from specified uri.
265          */
setImageResolver(NotificationInlineImageResolver resolver)266         void setImageResolver(NotificationInlineImageResolver resolver);
267 
268         /**
269          * Check if the uri is in the cache no matter it is loading or loaded.
270          * @param uri The uri to check.
271          * @return True if it is already in the cache; false otherwise.
272          */
hasEntry(Uri uri)273         boolean hasEntry(Uri uri);
274 
275         /**
276          * Start a new loading task for the target uri.
277          * @param uri The target to load.
278          */
preload(Uri uri)279         void preload(Uri uri);
280 
281         /**
282          * Purge unnecessary entries in the cache.
283          */
purge()284         void purge();
285 
286         /**
287          * Cancel all unfinished image loading tasks
288          */
cancelRunningTasks()289         void cancelRunningTasks();
290     }
291 
292 }
293