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.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 && !isLowRam();
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     /**
114      * To resolve image from specified uri directly. If the resulting image is larger than the
115      * maximum allowed size, scale it down.
116      * @param uri Uri of the image.
117      * @return Drawable of the image, or null if unable to load.
118      */
resolveImage(Uri uri)119     Drawable resolveImage(Uri uri) {
120         try {
121             return LocalImageResolver.resolveImage(uri, mContext, mMaxImageWidth, mMaxImageHeight);
122         } catch (Exception ex) {
123             // Catch general Exception because ContentResolver can re-throw arbitrary Exception
124             // from remote process as a RuntimeException. See: Parcel#readException
125             Log.d(TAG, "resolveImage: Can't load image from " + uri, ex);
126         }
127         return null;
128     }
129 
130     /**
131      * Loads an image from the Uri.
132      * This method is synchronous and is usually called from the Main thread.
133      * It will time-out after MAX_UI_THREAD_TIMEOUT_MS.
134      *
135      * @param uri Uri of the target image.
136      * @return drawable of the image, null if loading failed/timeout
137      */
138     @Override
loadImage(Uri uri)139     public Drawable loadImage(Uri uri) {
140         return hasCache() ? loadImageFromCache(uri, MAX_UI_THREAD_TIMEOUT_MS) : resolveImage(uri);
141     }
142 
loadImageFromCache(Uri uri, long timeoutMs)143     private Drawable loadImageFromCache(Uri uri, long timeoutMs) {
144         // if the uri isn't currently cached, try caching it first
145         if (!mImageCache.hasEntry(uri)) {
146             mImageCache.preload((uri));
147         }
148         return mImageCache.get(uri, timeoutMs);
149     }
150 
151     /**
152      * Resolve the message list from specified notification and
153      * refresh internal cache according to the result.
154      * @param notification The Notification to be resolved.
155      */
preloadImages(Notification notification)156     public void preloadImages(Notification notification) {
157         if (!hasCache()) {
158             return;
159         }
160 
161         retrieveWantedUriSet(notification);
162         Set<Uri> wantedSet = getWantedUriSet();
163         wantedSet.forEach(uri -> {
164             if (!mImageCache.hasEntry(uri)) {
165                 // The uri is not in the cache, we need trigger a loading task for it.
166                 mImageCache.preload(uri);
167             }
168         });
169     }
170 
171     /**
172      * Try to purge unnecessary cache entries.
173      */
purgeCache()174     public void purgeCache() {
175         if (!hasCache()) {
176             return;
177         }
178         mImageCache.purge();
179     }
180 
retrieveWantedUriSet(Notification notification)181     private void retrieveWantedUriSet(Notification notification) {
182         Parcelable[] messages;
183         Parcelable[] historicMessages;
184         List<Notification.MessagingStyle.Message> messageList;
185         List<Notification.MessagingStyle.Message> historicList;
186         Set<Uri> result = new HashSet<>();
187 
188         Bundle extras = notification.extras;
189         if (extras == null) {
190             return;
191         }
192 
193         messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
194         messageList = messages == null ? null :
195                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
196         if (messageList != null) {
197             for (Notification.MessagingStyle.Message message : messageList) {
198                 if (MessagingMessage.hasImage(message)) {
199                     result.add(message.getDataUri());
200                 }
201             }
202         }
203 
204         historicMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
205         historicList = historicMessages == null ? null :
206                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(historicMessages);
207         if (historicList != null) {
208             for (Notification.MessagingStyle.Message historic : historicList) {
209                 if (MessagingMessage.hasImage(historic)) {
210                     result.add(historic.getDataUri());
211                 }
212             }
213         }
214 
215         mWantedUriSet = result;
216     }
217 
getWantedUriSet()218     Set<Uri> getWantedUriSet() {
219         return mWantedUriSet;
220     }
221 
222     /**
223      * Wait for a maximum timeout for images to finish preloading
224      * @param timeoutMs total timeout time
225      */
waitForPreloadedImages(long timeoutMs)226     void waitForPreloadedImages(long timeoutMs) {
227         if (!hasCache()) {
228             return;
229         }
230         Set<Uri> preloadedUris = getWantedUriSet();
231         if (preloadedUris != null) {
232             // Decrement remaining timeout after each image check
233             long endTimeMs = SystemClock.elapsedRealtime() + timeoutMs;
234             preloadedUris.forEach(
235                     uri -> loadImageFromCache(uri, endTimeMs - SystemClock.elapsedRealtime()));
236         }
237     }
238 
cancelRunningTasks()239     void cancelRunningTasks() {
240         if (!hasCache()) {
241             return;
242         }
243         mImageCache.cancelRunningTasks();
244     }
245 
246     /**
247      * A interface for internal cache implementation of this resolver.
248      */
249     interface ImageCache {
250         /**
251          * Load the image from cache first then resolve from uri if missed the cache.
252          * @param uri The uri of the image.
253          * @return Drawable of the image.
254          */
get(Uri uri, long timeoutMs)255         Drawable get(Uri uri, long timeoutMs);
256 
257         /**
258          * Set the image resolver that actually resolves image from specified uri.
259          * @param resolver The resolver implementation that resolves image from specified uri.
260          */
setImageResolver(NotificationInlineImageResolver resolver)261         void setImageResolver(NotificationInlineImageResolver resolver);
262 
263         /**
264          * Check if the uri is in the cache no matter it is loading or loaded.
265          * @param uri The uri to check.
266          * @return True if it is already in the cache; false otherwise.
267          */
hasEntry(Uri uri)268         boolean hasEntry(Uri uri);
269 
270         /**
271          * Start a new loading task for the target uri.
272          * @param uri The target to load.
273          */
preload(Uri uri)274         void preload(Uri uri);
275 
276         /**
277          * Purge unnecessary entries in the cache.
278          */
purge()279         void purge();
280 
281         /**
282          * Cancel all unfinished image loading tasks
283          */
cancelRunningTasks()284         void cancelRunningTasks();
285     }
286 
287 }
288