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 
17 package com.android.messaging.datamodel;
18 
19 import android.content.res.Resources;
20 import android.graphics.Bitmap;
21 import android.graphics.BitmapFactory;
22 import androidx.annotation.NonNull;
23 import android.text.TextUtils;
24 import android.util.SparseArray;
25 
26 import com.android.messaging.datamodel.MemoryCacheManager.MemoryCache;
27 import com.android.messaging.util.Assert;
28 import com.android.messaging.util.LogUtil;
29 
30 import java.io.InputStream;
31 
32 /**
33  * Class for creating / loading / reusing bitmaps. This class allow the user to create a new bitmap,
34  * reuse an bitmap from the pool and to return a bitmap for future reuse.  The pool of bitmaps
35  * allows for faster decode and more efficient memory usage.
36  * Note: consumers should not create BitmapPool directly, but instead get the pool they want from
37  * the BitmapPoolManager.
38  */
39 public class BitmapPool implements MemoryCache {
40     public static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF;
41 
42     protected static final boolean VERBOSE = false;
43 
44     /**
45      * Number of reuse failures to skip before reporting.
46      */
47     private static final int FAILED_REPORTING_FREQUENCY = 100;
48 
49     /**
50      * Count of reuse failures which have occurred.
51      */
52     private static volatile int sFailedBitmapReuseCount = 0;
53 
54     /**
55      * Overall pool data structure which currently only supports rectangular bitmaps. The size of
56      * one of the sides is used to index into the SparseArray.
57      */
58     private final SparseArray<SingleSizePool> mPool;
59     private final Object mPoolLock = new Object();
60     private final String mPoolName;
61     private final int mMaxSize;
62 
63     /**
64      * Inner structure which holds a pool of bitmaps all the same size (i.e. all have the same
65      * width as each other and height as each other, but not necessarily the same).
66      */
67     private class SingleSizePool {
68         int mNumItems;
69         final Bitmap[] mBitmaps;
70 
SingleSizePool(final int maxPoolSize)71         SingleSizePool(final int maxPoolSize) {
72             mNumItems = 0;
73             mBitmaps = new Bitmap[maxPoolSize];
74         }
75     }
76 
77     /**
78      * Creates a pool of reused bitmaps with helper decode methods which will attempt to use the
79      * reclaimed bitmaps. This will help speed up the creation of bitmaps by using already allocated
80      * bitmaps.
81      * @param maxSize The overall max size of the pool. When the pool exceeds this size, all calls
82      * to reclaimBitmap(Bitmap) will result in recycling the bitmap.
83      * @param name Name of the bitmap pool and only used for logging. Can not be null.
84      */
BitmapPool(final int maxSize, @NonNull final String name)85     BitmapPool(final int maxSize, @NonNull final String name) {
86         Assert.isTrue(maxSize > 0);
87         Assert.isTrue(!TextUtils.isEmpty(name));
88         mPoolName = name;
89         mMaxSize = maxSize;
90         mPool = new SparseArray<SingleSizePool>();
91     }
92 
93     @Override
reclaim()94     public void reclaim() {
95         synchronized (mPoolLock) {
96             for (int p = 0; p < mPool.size(); p++) {
97                 final SingleSizePool singleSizePool = mPool.valueAt(p);
98                 for (int i = 0; i < singleSizePool.mNumItems; i++) {
99                     singleSizePool.mBitmaps[i].recycle();
100                     singleSizePool.mBitmaps[i] = null;
101                 }
102                 singleSizePool.mNumItems = 0;
103             }
104             mPool.clear();
105         }
106     }
107 
108     /**
109      * Creates a new BitmapFactory.Options.
110      */
getBitmapOptionsForPool(final boolean scaled, final int inputDensity, final int targetDensity)111     public static BitmapFactory.Options getBitmapOptionsForPool(final boolean scaled,
112             final int inputDensity, final int targetDensity) {
113         final BitmapFactory.Options options = new BitmapFactory.Options();
114         options.inScaled = scaled;
115         options.inDensity = inputDensity;
116         options.inTargetDensity = targetDensity;
117         options.inSampleSize = 1;
118         options.inJustDecodeBounds = false;
119         options.inMutable = true;
120         return options;
121     }
122 
123     /**
124      * @return The pool key for the provided image dimensions or 0 if either width or height is
125      * greater than the max supported image dimension.
126      */
getPoolKey(final int width, final int height)127     private int getPoolKey(final int width, final int height) {
128         if (width > MAX_SUPPORTED_IMAGE_DIMENSION || height > MAX_SUPPORTED_IMAGE_DIMENSION) {
129             return 0;
130         }
131         return (width << 16) | height;
132     }
133 
134     /**
135      *
136      * @return A bitmap in the pool with the specified dimensions or null if no bitmap with the
137      * specified dimension is available.
138      */
findPoolBitmap(final int width, final int height)139     private Bitmap findPoolBitmap(final int width, final int height) {
140         final int poolKey = getPoolKey(width, height);
141         if (poolKey != 0) {
142             synchronized (mPoolLock) {
143                 // Take a bitmap from the pool if one is available
144                 final SingleSizePool singlePool = mPool.get(poolKey);
145                 if (singlePool != null && singlePool.mNumItems > 0) {
146                     singlePool.mNumItems--;
147                     final Bitmap foundBitmap = singlePool.mBitmaps[singlePool.mNumItems];
148                     singlePool.mBitmaps[singlePool.mNumItems] = null;
149                     return foundBitmap;
150                 }
151             }
152         }
153         return null;
154     }
155 
156     /**
157      * Internal function to try and find a bitmap in the pool which matches the desired width and
158      * height and then set that in the bitmap options properly.
159      *
160      * TODO: Why do we take a width/height? Shouldn't this already be in the
161      * BitmapFactory.Options instance? Can we assert that they match?
162      * @param optionsTmp The BitmapFactory.Options to update with the bitmap for the system to try
163      * to reuse.
164      * @param width The width of the reusable bitmap.
165      * @param height The height of the reusable bitmap.
166      */
assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width, final int height)167     private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width,
168             final int height) {
169         if (optionsTmp.inJustDecodeBounds) {
170             return;
171         }
172         optionsTmp.inBitmap = findPoolBitmap(width, height);
173     }
174 
175     /**
176      * Load a resource into a bitmap. Uses a bitmap from the pool if possible to reduce memory
177      * turnover.
178      * @param resourceId Resource id to load.
179      * @param resources Application resources. Cannot be null.
180      * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool(). Cannot
181      * be null.
182      * @param width The width of the bitmap.
183      * @param height The height of the bitmap.
184      * @return The decoded Bitmap with the resource drawn in it.
185      */
decodeSampledBitmapFromResource(final int resourceId, @NonNull final Resources resources, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height)186     public Bitmap decodeSampledBitmapFromResource(final int resourceId,
187             @NonNull final Resources resources, @NonNull final BitmapFactory.Options optionsTmp,
188             final int width, final int height) {
189         Assert.notNull(resources);
190         Assert.notNull(optionsTmp);
191         Assert.isTrue(width > 0);
192         Assert.isTrue(height > 0);
193         assignPoolBitmap(optionsTmp, width, height);
194         Bitmap b = null;
195         try {
196             b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
197         } catch (final IllegalArgumentException e) {
198             // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
199             if (optionsTmp.inBitmap != null) {
200                 optionsTmp.inBitmap = null;
201                 b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
202                 sFailedBitmapReuseCount++;
203                 if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
204                     LogUtil.w(LogUtil.BUGLE_TAG,
205                             "Pooled bitmap consistently not being reused count = " +
206                             sFailedBitmapReuseCount);
207                 }
208             }
209         } catch (final OutOfMemoryError e) {
210             LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding resource " + resourceId);
211             reclaim();
212         }
213         return b;
214     }
215 
216     /**
217      * Load an input stream into a bitmap. Uses a bitmap from the pool if possible to reduce memory
218      * turnover.
219      * @param inputStream InputStream load. Cannot be null.
220      * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool(). Cannot
221      * be null.
222      * @param width The width of the bitmap.
223      * @param height The height of the bitmap.
224      * @return The decoded Bitmap with the resource drawn in it.
225      */
decodeSampledBitmapFromInputStream(@onNull final InputStream inputStream, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height)226     public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream,
227             @NonNull final BitmapFactory.Options optionsTmp,
228             final int width, final int height) {
229         Assert.notNull(inputStream);
230         Assert.isTrue(width > 0);
231         Assert.isTrue(height > 0);
232         assignPoolBitmap(optionsTmp, width, height);
233         Bitmap b = null;
234         try {
235             b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
236         } catch (final IllegalArgumentException e) {
237             // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
238             if (optionsTmp.inBitmap != null) {
239                 optionsTmp.inBitmap = null;
240                 b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
241                 sFailedBitmapReuseCount++;
242                 if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
243                     LogUtil.w(LogUtil.BUGLE_TAG,
244                             "Pooled bitmap consistently not being reused count = " +
245                             sFailedBitmapReuseCount);
246                 }
247             }
248         } catch (final OutOfMemoryError e) {
249             LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding inputStream");
250             reclaim();
251         }
252         return b;
253     }
254 
255     /**
256      * Turn encoded bytes into a bitmap. Uses a bitmap from the pool if possible to reduce memory
257      * turnover.
258      * @param bytes Encoded bytes to draw on the bitmap. Cannot be null.
259      * @param optionsTmp The bitmap will set here and the input should be generated from
260      * getBitmapOptionsForPool(). Cannot be null.
261      * @param width The width of the bitmap.
262      * @param height The height of the bitmap.
263      * @return A Bitmap with the encoded bytes drawn in it.
264      */
decodeByteArray(@onNull final byte[] bytes, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height)265     public Bitmap decodeByteArray(@NonNull final byte[] bytes,
266             @NonNull final BitmapFactory.Options optionsTmp, final int width,
267             final int height) throws OutOfMemoryError {
268         Assert.notNull(bytes);
269         Assert.notNull(optionsTmp);
270         Assert.isTrue(width > 0);
271         Assert.isTrue(height > 0);
272         assignPoolBitmap(optionsTmp, width, height);
273         Bitmap b = null;
274         try {
275             b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
276         } catch (final IllegalArgumentException e) {
277             if (VERBOSE) {
278                 LogUtil.v(LogUtil.BUGLE_TAG, "BitmapPool(" + mPoolName +
279                         ") Unable to use pool bitmap");
280             }
281             // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
282             // (i.e. without the bitmap from the pool)
283             if (optionsTmp.inBitmap != null) {
284                 optionsTmp.inBitmap = null;
285                 b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
286                 sFailedBitmapReuseCount++;
287                 if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
288                     LogUtil.w(LogUtil.BUGLE_TAG,
289                             "Pooled bitmap consistently not being reused count = " +
290                             sFailedBitmapReuseCount);
291                 }
292             }
293         }
294         return b;
295     }
296 
297     /**
298      * Creates a bitmap with the given size, this will reuse a bitmap in the pool, if one is
299      * available, otherwise this will create a new one.
300      * @param width The desired width of the bitmap.
301      * @param height The desired height of the bitmap.
302      * @return A bitmap with the desired width and height, this maybe a reused bitmap from the pool.
303      */
createOrReuseBitmap(final int width, final int height)304     public Bitmap createOrReuseBitmap(final int width, final int height) {
305         Bitmap b = findPoolBitmap(width, height);
306         if (b == null) {
307             b = createBitmap(width, height);
308         }
309         return b;
310     }
311 
312     /**
313      * This will create a new bitmap regardless of pool state.
314      * @param width The desired width of the bitmap.
315      * @param height The desired height of the bitmap.
316      * @return A bitmap with the desired width and height.
317      */
createBitmap(final int width, final int height)318     private Bitmap createBitmap(final int width, final int height) {
319         return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
320     }
321 
322     /**
323      * Called when a bitmap is finished being used so that it can be used for another bitmap in the
324      * future or recycled. Any bitmaps returned should not be used by the caller again.
325      * @param b The bitmap to return to the pool for future usage or recycled. This cannot be null.
326      */
reclaimBitmap(@onNull final Bitmap b)327     public void reclaimBitmap(@NonNull final Bitmap b) {
328         Assert.notNull(b);
329         final int poolKey = getPoolKey(b.getWidth(), b.getHeight());
330         if (poolKey == 0 || !b.isMutable()) {
331             // Unsupported image dimensions or a immutable bitmap.
332             b.recycle();
333             return;
334         }
335         synchronized (mPoolLock) {
336             SingleSizePool singleSizePool = mPool.get(poolKey);
337             if (singleSizePool == null) {
338                 singleSizePool = new SingleSizePool(mMaxSize);
339                 mPool.append(poolKey, singleSizePool);
340             }
341             if (singleSizePool.mNumItems < singleSizePool.mBitmaps.length) {
342                 singleSizePool.mBitmaps[singleSizePool.mNumItems] = b;
343                 singleSizePool.mNumItems++;
344             } else {
345                 b.recycle();
346             }
347         }
348     }
349 
350     /**
351      * @return whether the pool is full for a given width and height.
352      */
isFull(final int width, final int height)353     public boolean isFull(final int width, final int height) {
354         final int poolKey = getPoolKey(width, height);
355         synchronized (mPoolLock) {
356             final SingleSizePool singleSizePool = mPool.get(poolKey);
357             if (singleSizePool != null &&
358                     singleSizePool.mNumItems >= singleSizePool.mBitmaps.length) {
359                 return true;
360             }
361             return false;
362         }
363     }
364 }
365