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