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
17package com.android.messaging.datamodel;
18
19import android.content.res.Resources;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.support.annotation.NonNull;
23import android.text.TextUtils;
24import android.util.SparseArray;
25
26import com.android.messaging.datamodel.MemoryCacheManager.MemoryCache;
27import com.android.messaging.util.Assert;
28import com.android.messaging.util.LogUtil;
29
30import 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 */
39public 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
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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