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