1 /*
2  * Copyright (C) 2015 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 package com.android.messaging.datamodel.media;
17 
18 import android.graphics.Bitmap;
19 import android.graphics.BitmapFactory;
20 import android.graphics.Color;
21 import android.os.SystemClock;
22 import androidx.annotation.NonNull;
23 import android.util.SparseArray;
24 
25 import com.android.messaging.Factory;
26 import com.android.messaging.util.Assert;
27 import com.android.messaging.util.LogUtil;
28 
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.util.LinkedList;
32 
33 /**
34  * A media cache that holds image resources, which doubles as a bitmap pool that allows the
35  * consumer to optionally decode image resources using unused bitmaps stored in the cache.
36  */
37 public class PoolableImageCache extends MediaCache<ImageResource> {
38     private static final int MIN_TIME_IN_POOL = 5000;
39 
40     /** Encapsulates bitmap pool representation of the image cache */
41     private final ReusableImageResourcePool mReusablePoolAccessor = new ReusableImageResourcePool();
42 
PoolableImageCache(final int id, final String name)43     public PoolableImageCache(final int id, final String name) {
44         this(DEFAULT_MEDIA_RESOURCE_CACHE_SIZE_IN_KILOBYTES, id, name);
45     }
46 
PoolableImageCache(final int maxSize, final int id, final String name)47     public PoolableImageCache(final int maxSize, final int id, final String name) {
48         super(maxSize, id, name);
49     }
50 
51     /**
52      * Creates a new BitmapFactory.Options for using the self-contained bitmap pool.
53      */
getBitmapOptionsForPool(final boolean scaled, final int inputDensity, final int targetDensity)54     public static BitmapFactory.Options getBitmapOptionsForPool(final boolean scaled,
55             final int inputDensity, final int targetDensity) {
56         final BitmapFactory.Options options = new BitmapFactory.Options();
57         options.inScaled = scaled;
58         options.inDensity = inputDensity;
59         options.inTargetDensity = targetDensity;
60         options.inSampleSize = 1;
61         options.inJustDecodeBounds = false;
62         options.inMutable = true;
63         return options;
64     }
65 
66     @Override
addResourceToCache(final String key, final ImageResource imageResource)67     public synchronized ImageResource addResourceToCache(final String key,
68             final ImageResource imageResource) {
69         mReusablePoolAccessor.onResourceEnterCache(imageResource);
70         return super.addResourceToCache(key, imageResource);
71     }
72 
73     @Override
entryRemoved(final boolean evicted, final String key, final ImageResource oldValue, final ImageResource newValue)74     protected synchronized void entryRemoved(final boolean evicted, final String key,
75             final ImageResource oldValue, final ImageResource newValue) {
76         mReusablePoolAccessor.onResourceLeaveCache(oldValue);
77         super.entryRemoved(evicted, key, oldValue, newValue);
78     }
79 
80     /**
81      * Returns a representation of the image cache as a reusable bitmap pool.
82      */
asReusableBitmapPool()83     public ReusableImageResourcePool asReusableBitmapPool() {
84         return mReusablePoolAccessor;
85     }
86 
87     /**
88      * A bitmap pool representation built on top of the image cache. It treats the image resources
89      * stored in the image cache as a self-contained bitmap pool and is able to create or
90      * reclaim bitmap resource as needed.
91      */
92     public class ReusableImageResourcePool {
93         private static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF;
94         private static final int INVALID_POOL_KEY = 0;
95 
96         /**
97          * Number of reuse failures to skip before reporting.
98          * For debugging purposes, change to a lower number for more frequent reporting.
99          */
100         private static final int FAILED_REPORTING_FREQUENCY = 100;
101 
102         /**
103          * Count of reuse failures which have occurred.
104          */
105         private volatile int mFailedBitmapReuseCount = 0;
106 
107         /**
108          * Count of reuse successes which have occurred.
109          */
110         private volatile int mSucceededBitmapReuseCount = 0;
111 
112         /**
113          * A sparse array from bitmap size to a list of image cache entries that match the
114          * given size. This map is used to quickly retrieve a usable bitmap to be reused by an
115          * incoming ImageRequest. We need to ensure that this sparse array always contains only
116          * elements currently in the image cache with no other consumer.
117          */
118         private final SparseArray<LinkedList<ImageResource>> mImageListSparseArray;
119 
ReusableImageResourcePool()120         public ReusableImageResourcePool() {
121             mImageListSparseArray = new SparseArray<LinkedList<ImageResource>>();
122         }
123 
124         /**
125          * Load an input stream into a bitmap. Uses a bitmap from the pool if possible to reduce
126          * memory turnover.
127          * @param inputStream InputStream load. Cannot be null.
128          * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool().
129          * Cannot be null.
130          * @param width The width of the bitmap.
131          * @param height The height of the bitmap.
132          * @return The decoded Bitmap with the resource drawn in it.
133          * @throws IOException
134          */
decodeSampledBitmapFromInputStream(@onNull final InputStream inputStream, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height)135         public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream,
136                 @NonNull final BitmapFactory.Options optionsTmp,
137                 final int width, final int height) throws IOException {
138             if (width <= 0 || height <= 0) {
139                 // This is an invalid / corrupted image of zero size.
140                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " +
141                         "invalid size");
142                 throw new IOException("Invalid size / corrupted image");
143             }
144             Assert.notNull(inputStream);
145             assignPoolBitmap(optionsTmp, width, height);
146             Bitmap b = null;
147             try {
148                 b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
149                 mSucceededBitmapReuseCount++;
150             } catch (final IllegalArgumentException e) {
151                 // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
152                 if (optionsTmp.inBitmap != null) {
153                     optionsTmp.inBitmap.recycle();
154                     optionsTmp.inBitmap = null;
155                     b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
156                     onFailedToReuse();
157                 }
158             } catch (final OutOfMemoryError e) {
159                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream");
160                 Factory.get().reclaimMemory();
161             }
162             return b;
163         }
164 
165         /**
166          * Turn encoded bytes into a bitmap. Uses a bitmap from the pool if possible to reduce
167          * memory turnover.
168          * @param bytes Encoded bytes to draw on the bitmap. Cannot be null.
169          * @param optionsTmp The bitmap will set here and the input should be generated from
170          * getBitmapOptionsForPool(). Cannot be null.
171          * @param width The width of the bitmap.
172          * @param height The height of the bitmap.
173          * @return A Bitmap with the encoded bytes drawn in it.
174          * @throws IOException
175          */
decodeByteArray(@onNull final byte[] bytes, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height)176         public Bitmap decodeByteArray(@NonNull final byte[] bytes,
177                 @NonNull final BitmapFactory.Options optionsTmp, final int width,
178                 final int height) throws OutOfMemoryError, IOException {
179             if (width <= 0 || height <= 0) {
180                 // This is an invalid / corrupted image of zero size.
181                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " +
182                         "invalid size");
183                 throw new IOException("Invalid size / corrupted image");
184             }
185             Assert.notNull(bytes);
186             Assert.notNull(optionsTmp);
187             assignPoolBitmap(optionsTmp, width, height);
188             Bitmap b = null;
189             try {
190                 b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
191                 mSucceededBitmapReuseCount++;
192             } catch (final IllegalArgumentException e) {
193                 // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
194                 // (i.e. without the bitmap from the pool)
195                 if (optionsTmp.inBitmap != null) {
196                     optionsTmp.inBitmap.recycle();
197                     optionsTmp.inBitmap = null;
198                     b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
199                     onFailedToReuse();
200                 }
201             } catch (final OutOfMemoryError e) {
202                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream");
203                 Factory.get().reclaimMemory();
204             }
205             return b;
206         }
207 
208         /**
209          * Called when a new image resource is added to the cache. We add the resource to the
210          * pool so it's properly keyed into the pool structure.
211          */
onResourceEnterCache(final ImageResource imageResource)212         void onResourceEnterCache(final ImageResource imageResource) {
213             if (getPoolKey(imageResource) != INVALID_POOL_KEY) {
214                 addResourceToPool(imageResource);
215             }
216         }
217 
218         /**
219          * Called when an image resource is evicted from the cache. Bitmap pool's entries are
220          * strictly tied to their presence in the image cache. Once an image is evicted from the
221          * cache, it should be removed from the pool.
222          */
onResourceLeaveCache(final ImageResource imageResource)223         void onResourceLeaveCache(final ImageResource imageResource) {
224             if (getPoolKey(imageResource) != INVALID_POOL_KEY) {
225                 removeResourceFromPool(imageResource);
226             }
227         }
228 
addResourceToPool(final ImageResource imageResource)229         private void addResourceToPool(final ImageResource imageResource) {
230             synchronized (PoolableImageCache.this) {
231                 final int poolKey = getPoolKey(imageResource);
232                 Assert.isTrue(poolKey != INVALID_POOL_KEY);
233                 LinkedList<ImageResource> imageList = mImageListSparseArray.get(poolKey);
234                 if (imageList == null) {
235                     imageList = new LinkedList<ImageResource>();
236                     mImageListSparseArray.put(poolKey, imageList);
237                 }
238                 imageList.addLast(imageResource);
239             }
240         }
241 
removeResourceFromPool(final ImageResource imageResource)242         private void removeResourceFromPool(final ImageResource imageResource) {
243             synchronized (PoolableImageCache.this) {
244                 final int poolKey = getPoolKey(imageResource);
245                 Assert.isTrue(poolKey != INVALID_POOL_KEY);
246                 final LinkedList<ImageResource> imageList = mImageListSparseArray.get(poolKey);
247                 if (imageList != null) {
248                     imageList.remove(imageResource);
249                 }
250             }
251         }
252 
253         /**
254          * Try to get a reusable bitmap from the pool with the given width and height. As a
255          * result of this call, the caller will assume ownership of the returned bitmap.
256          */
getReusableBitmapFromPool(final int width, final int height)257         private Bitmap getReusableBitmapFromPool(final int width, final int height) {
258             synchronized (PoolableImageCache.this) {
259                 final int poolKey = getPoolKey(width, height);
260                 if (poolKey != INVALID_POOL_KEY) {
261                     final LinkedList<ImageResource> images = mImageListSparseArray.get(poolKey);
262                     if (images != null && images.size() > 0) {
263                         // Try to reuse the first available bitmap from the pool list. We start from
264                         // the least recently added cache entry of the given size.
265                         ImageResource imageToUse = null;
266                         for (int i = 0; i < images.size(); i++) {
267                             final ImageResource image = images.get(i);
268                             if (image.getRefCount() == 1) {
269                                 image.acquireLock();
270                                 if (image.getRefCount() == 1) {
271                                     // The image is only used by the cache, so it's reusable.
272                                     imageToUse = images.remove(i);
273                                     break;
274                                 } else {
275                                     // Logically, this shouldn't happen, because as soon as the
276                                     // cache is the only user of this resource, it will not be
277                                     // used by anyone else until the next cache access, but we
278                                     // currently hold on to the cache lock. But technically
279                                     // future changes may violate this assumption, so warn about
280                                     // this.
281                                     LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Image refCount changed " +
282                                             "from 1 in getReusableBitmapFromPool()");
283                                     image.releaseLock();
284                                 }
285                             }
286                         }
287 
288                         if (imageToUse == null) {
289                             return null;
290                         }
291 
292                         try {
293                             imageToUse.assertLockHeldByCurrentThread();
294 
295                             // Only reuse the bitmap if the last time we use was greater than 5s.
296                             // This allows the cache a chance to reuse instead of always taking the
297                             // oldest.
298                             final long timeSinceLastRef = SystemClock.elapsedRealtime() -
299                                     imageToUse.getLastRefAddTimestamp();
300                             if (timeSinceLastRef < MIN_TIME_IN_POOL) {
301                                 if (LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE)) {
302                                     LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "Not reusing reusing " +
303                                             "first available bitmap from the pool because it " +
304                                             "has not been in the pool long enough. " +
305                                             "timeSinceLastRef=" + timeSinceLastRef);
306                                 }
307                                 // Put back the image and return no reuseable bitmap.
308                                 images.addLast(imageToUse);
309                                 return null;
310                             }
311 
312                             // Add a temp ref on the image resource so it won't be GC'd after
313                             // being removed from the cache.
314                             imageToUse.addRef();
315 
316                             // Remove the image resource from the image cache.
317                             final ImageResource removed = remove(imageToUse.getKey());
318                             Assert.isTrue(removed == imageToUse);
319 
320                             // Try to reuse the bitmap from the image resource. This will transfer
321                             // ownership of the bitmap object to the caller of this method.
322                             final Bitmap reusableBitmap = imageToUse.reuseBitmap();
323 
324                             imageToUse.release();
325                             return reusableBitmap;
326                         } finally {
327                             // We are either done with the reuse operation, or decided not to use
328                             // the image. Either way, release the lock.
329                             imageToUse.releaseLock();
330                         }
331                     }
332                 }
333             }
334             return null;
335         }
336 
337         /**
338          * Try to locate and return a reusable bitmap from the pool, or create a new bitmap.
339          * @param width desired bitmap width
340          * @param height desired bitmap height
341          * @return the created or reused mutable bitmap that has its background cleared to
342          * {@value Color#TRANSPARENT}
343          */
createOrReuseBitmap(final int width, final int height)344         public Bitmap createOrReuseBitmap(final int width, final int height) {
345             return createOrReuseBitmap(width, height, Color.TRANSPARENT);
346         }
347 
348         /**
349          * Try to locate and return a reusable bitmap from the pool, or create a new bitmap.
350          * @param width desired bitmap width
351          * @param height desired bitmap height
352          * @param backgroundColor the background color for the returned bitmap
353          * @return the created or reused mutable bitmap with the requested background color
354          */
createOrReuseBitmap(final int width, final int height, final int backgroundColor)355         public Bitmap createOrReuseBitmap(final int width, final int height,
356                 final int backgroundColor) {
357             Bitmap retBitmap = null;
358             try {
359                 final Bitmap poolBitmap = getReusableBitmapFromPool(width, height);
360                 retBitmap = (poolBitmap != null) ? poolBitmap :
361                         Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
362                 retBitmap.eraseColor(backgroundColor);
363             } catch (final OutOfMemoryError e) {
364                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache:try to createOrReuseBitmap");
365                 Factory.get().reclaimMemory();
366             }
367             return retBitmap;
368         }
369 
assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width, final int height)370         private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width,
371                 final int height) {
372             if (optionsTmp.inJustDecodeBounds) {
373                 return;
374             }
375             optionsTmp.inBitmap = getReusableBitmapFromPool(width, height);
376         }
377 
378         /**
379          * @return The pool key for the provided image dimensions or 0 if either width or height is
380          * greater than the max supported image dimension.
381          */
getPoolKey(final int width, final int height)382         private int getPoolKey(final int width, final int height) {
383             if (width > MAX_SUPPORTED_IMAGE_DIMENSION || height > MAX_SUPPORTED_IMAGE_DIMENSION) {
384                 return INVALID_POOL_KEY;
385             }
386             return (width << 16) | height;
387         }
388 
389         /**
390          * @return the pool key for a given image resource.
391          */
getPoolKey(final ImageResource imageResource)392         private int getPoolKey(final ImageResource imageResource) {
393             if (imageResource.supportsBitmapReuse()) {
394                 final Bitmap bitmap = imageResource.getBitmap();
395                 if (bitmap != null && bitmap.isMutable()) {
396                     final int width = bitmap.getWidth();
397                     final int height = bitmap.getHeight();
398                     if (width > 0 && height > 0) {
399                         return getPoolKey(width, height);
400                     }
401                 }
402             }
403             return INVALID_POOL_KEY;
404         }
405 
406         /**
407          * Called when bitmap reuse fails. Conditionally report the failure with statistics.
408          */
onFailedToReuse()409         private void onFailedToReuse() {
410             mFailedBitmapReuseCount++;
411             if (mFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
412                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG,
413                         "Pooled bitmap consistently not being reused. Failure count = " +
414                                 mFailedBitmapReuseCount + ", success count = " +
415                                 mSucceededBitmapReuseCount);
416             }
417         }
418     }
419 }
420