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 */
16package com.android.messaging.datamodel.media;
17
18import android.graphics.Bitmap;
19import android.graphics.BitmapFactory;
20import android.graphics.Color;
21import android.os.SystemClock;
22import android.support.annotation.NonNull;
23import android.util.SparseArray;
24
25import com.android.messaging.Factory;
26import com.android.messaging.util.Assert;
27import com.android.messaging.util.LogUtil;
28
29import java.io.IOException;
30import java.io.InputStream;
31import 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 */
37public 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
43    public PoolableImageCache(final int id, final String name) {
44        this(DEFAULT_MEDIA_RESOURCE_CACHE_SIZE_IN_KILOBYTES, id, name);
45    }
46
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     */
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
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
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     */
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
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         */
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         */
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         */
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         */
223        void onResourceLeaveCache(final ImageResource imageResource) {
224            if (getPoolKey(imageResource) != INVALID_POOL_KEY) {
225                removeResourceFromPool(imageResource);
226            }
227        }
228
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
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         */
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         */
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         */
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
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         */
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         */
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         */
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