Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.datamodel.media;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* A media cache that holds image resources, which doubles as a bitmap pool that allows the
|
||||
* consumer to optionally decode image resources using unused bitmaps stored in the cache.
|
||||
*/
|
||||
public class PoolableImageCache extends MediaCache<ImageResource> {
|
||||
private static final int MIN_TIME_IN_POOL = 5000;
|
||||
|
||||
/** Encapsulates bitmap pool representation of the image cache */
|
||||
private final ReusableImageResourcePool mReusablePoolAccessor = new ReusableImageResourcePool();
|
||||
|
||||
public PoolableImageCache(final int id, final String name) {
|
||||
this(DEFAULT_MEDIA_RESOURCE_CACHE_SIZE_IN_KILOBYTES, id, name);
|
||||
}
|
||||
|
||||
public PoolableImageCache(final int maxSize, final int id, final String name) {
|
||||
super(maxSize, id, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new BitmapFactory.Options for using the self-contained bitmap pool.
|
||||
*/
|
||||
public static BitmapFactory.Options getBitmapOptionsForPool(final boolean scaled,
|
||||
final int inputDensity, final int targetDensity) {
|
||||
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inScaled = scaled;
|
||||
options.inDensity = inputDensity;
|
||||
options.inTargetDensity = targetDensity;
|
||||
options.inSampleSize = 1;
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inMutable = true;
|
||||
return options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized ImageResource addResourceToCache(final String key,
|
||||
final ImageResource imageResource) {
|
||||
mReusablePoolAccessor.onResourceEnterCache(imageResource);
|
||||
return super.addResourceToCache(key, imageResource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void entryRemoved(final boolean evicted, final String key,
|
||||
final ImageResource oldValue, final ImageResource newValue) {
|
||||
mReusablePoolAccessor.onResourceLeaveCache(oldValue);
|
||||
super.entryRemoved(evicted, key, oldValue, newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a representation of the image cache as a reusable bitmap pool.
|
||||
*/
|
||||
public ReusableImageResourcePool asReusableBitmapPool() {
|
||||
return mReusablePoolAccessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitmap pool representation built on top of the image cache. It treats the image resources
|
||||
* stored in the image cache as a self-contained bitmap pool and is able to create or
|
||||
* reclaim bitmap resource as needed.
|
||||
*/
|
||||
public class ReusableImageResourcePool {
|
||||
private static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF;
|
||||
private static final int INVALID_POOL_KEY = 0;
|
||||
|
||||
/**
|
||||
* Number of reuse failures to skip before reporting.
|
||||
* For debugging purposes, change to a lower number for more frequent reporting.
|
||||
*/
|
||||
private static final int FAILED_REPORTING_FREQUENCY = 100;
|
||||
|
||||
/**
|
||||
* Count of reuse failures which have occurred.
|
||||
*/
|
||||
private volatile int mFailedBitmapReuseCount = 0;
|
||||
|
||||
/**
|
||||
* Count of reuse successes which have occurred.
|
||||
*/
|
||||
private volatile int mSucceededBitmapReuseCount = 0;
|
||||
|
||||
/**
|
||||
* A sparse array from bitmap size to a list of image cache entries that match the
|
||||
* given size. This map is used to quickly retrieve a usable bitmap to be reused by an
|
||||
* incoming ImageRequest. We need to ensure that this sparse array always contains only
|
||||
* elements currently in the image cache with no other consumer.
|
||||
*/
|
||||
private final SparseArray<LinkedList<ImageResource>> mImageListSparseArray;
|
||||
|
||||
public ReusableImageResourcePool() {
|
||||
mImageListSparseArray = new SparseArray<LinkedList<ImageResource>>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an input stream into a bitmap. Uses a bitmap from the pool if possible to reduce
|
||||
* memory turnover.
|
||||
* @param inputStream InputStream load. Cannot be null.
|
||||
* @param optionsTmp Should be the same options returned from getBitmapOptionsForPool().
|
||||
* Cannot be null.
|
||||
* @param width The width of the bitmap.
|
||||
* @param height The height of the bitmap.
|
||||
* @return The decoded Bitmap with the resource drawn in it.
|
||||
* @throws IOException
|
||||
*/
|
||||
public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream,
|
||||
@NonNull final BitmapFactory.Options optionsTmp,
|
||||
final int width, final int height) throws IOException {
|
||||
if (width <= 0 || height <= 0) {
|
||||
// This is an invalid / corrupted image of zero size.
|
||||
LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " +
|
||||
"invalid size");
|
||||
throw new IOException("Invalid size / corrupted image");
|
||||
}
|
||||
Assert.notNull(inputStream);
|
||||
assignPoolBitmap(optionsTmp, width, height);
|
||||
Bitmap b = null;
|
||||
try {
|
||||
b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
|
||||
mSucceededBitmapReuseCount++;
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
|
||||
if (optionsTmp.inBitmap != null) {
|
||||
optionsTmp.inBitmap.recycle();
|
||||
optionsTmp.inBitmap = null;
|
||||
b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
|
||||
onFailedToReuse();
|
||||
}
|
||||
} catch (final OutOfMemoryError e) {
|
||||
LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream");
|
||||
Factory.get().reclaimMemory();
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn encoded bytes into a bitmap. Uses a bitmap from the pool if possible to reduce
|
||||
* memory turnover.
|
||||
* @param bytes Encoded bytes to draw on the bitmap. Cannot be null.
|
||||
* @param optionsTmp The bitmap will set here and the input should be generated from
|
||||
* getBitmapOptionsForPool(). Cannot be null.
|
||||
* @param width The width of the bitmap.
|
||||
* @param height The height of the bitmap.
|
||||
* @return A Bitmap with the encoded bytes drawn in it.
|
||||
* @throws IOException
|
||||
*/
|
||||
public Bitmap decodeByteArray(@NonNull final byte[] bytes,
|
||||
@NonNull final BitmapFactory.Options optionsTmp, final int width,
|
||||
final int height) throws OutOfMemoryError, IOException {
|
||||
if (width <= 0 || height <= 0) {
|
||||
// This is an invalid / corrupted image of zero size.
|
||||
LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " +
|
||||
"invalid size");
|
||||
throw new IOException("Invalid size / corrupted image");
|
||||
}
|
||||
Assert.notNull(bytes);
|
||||
Assert.notNull(optionsTmp);
|
||||
assignPoolBitmap(optionsTmp, width, height);
|
||||
Bitmap b = null;
|
||||
try {
|
||||
b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
|
||||
mSucceededBitmapReuseCount++;
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
|
||||
// (i.e. without the bitmap from the pool)
|
||||
if (optionsTmp.inBitmap != null) {
|
||||
optionsTmp.inBitmap.recycle();
|
||||
optionsTmp.inBitmap = null;
|
||||
b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
|
||||
onFailedToReuse();
|
||||
}
|
||||
} catch (final OutOfMemoryError e) {
|
||||
LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream");
|
||||
Factory.get().reclaimMemory();
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new image resource is added to the cache. We add the resource to the
|
||||
* pool so it's properly keyed into the pool structure.
|
||||
*/
|
||||
void onResourceEnterCache(final ImageResource imageResource) {
|
||||
if (getPoolKey(imageResource) != INVALID_POOL_KEY) {
|
||||
addResourceToPool(imageResource);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an image resource is evicted from the cache. Bitmap pool's entries are
|
||||
* strictly tied to their presence in the image cache. Once an image is evicted from the
|
||||
* cache, it should be removed from the pool.
|
||||
*/
|
||||
void onResourceLeaveCache(final ImageResource imageResource) {
|
||||
if (getPoolKey(imageResource) != INVALID_POOL_KEY) {
|
||||
removeResourceFromPool(imageResource);
|
||||
}
|
||||
}
|
||||
|
||||
private void addResourceToPool(final ImageResource imageResource) {
|
||||
synchronized (PoolableImageCache.this) {
|
||||
final int poolKey = getPoolKey(imageResource);
|
||||
Assert.isTrue(poolKey != INVALID_POOL_KEY);
|
||||
LinkedList<ImageResource> imageList = mImageListSparseArray.get(poolKey);
|
||||
if (imageList == null) {
|
||||
imageList = new LinkedList<ImageResource>();
|
||||
mImageListSparseArray.put(poolKey, imageList);
|
||||
}
|
||||
imageList.addLast(imageResource);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeResourceFromPool(final ImageResource imageResource) {
|
||||
synchronized (PoolableImageCache.this) {
|
||||
final int poolKey = getPoolKey(imageResource);
|
||||
Assert.isTrue(poolKey != INVALID_POOL_KEY);
|
||||
final LinkedList<ImageResource> imageList = mImageListSparseArray.get(poolKey);
|
||||
if (imageList != null) {
|
||||
imageList.remove(imageResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get a reusable bitmap from the pool with the given width and height. As a
|
||||
* result of this call, the caller will assume ownership of the returned bitmap.
|
||||
*/
|
||||
private Bitmap getReusableBitmapFromPool(final int width, final int height) {
|
||||
synchronized (PoolableImageCache.this) {
|
||||
final int poolKey = getPoolKey(width, height);
|
||||
if (poolKey != INVALID_POOL_KEY) {
|
||||
final LinkedList<ImageResource> images = mImageListSparseArray.get(poolKey);
|
||||
if (images != null && images.size() > 0) {
|
||||
// Try to reuse the first available bitmap from the pool list. We start from
|
||||
// the least recently added cache entry of the given size.
|
||||
ImageResource imageToUse = null;
|
||||
for (int i = 0; i < images.size(); i++) {
|
||||
final ImageResource image = images.get(i);
|
||||
if (image.getRefCount() == 1) {
|
||||
image.acquireLock();
|
||||
if (image.getRefCount() == 1) {
|
||||
// The image is only used by the cache, so it's reusable.
|
||||
imageToUse = images.remove(i);
|
||||
break;
|
||||
} else {
|
||||
// Logically, this shouldn't happen, because as soon as the
|
||||
// cache is the only user of this resource, it will not be
|
||||
// used by anyone else until the next cache access, but we
|
||||
// currently hold on to the cache lock. But technically
|
||||
// future changes may violate this assumption, so warn about
|
||||
// this.
|
||||
LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Image refCount changed " +
|
||||
"from 1 in getReusableBitmapFromPool()");
|
||||
image.releaseLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageToUse == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
imageToUse.assertLockHeldByCurrentThread();
|
||||
|
||||
// Only reuse the bitmap if the last time we use was greater than 5s.
|
||||
// This allows the cache a chance to reuse instead of always taking the
|
||||
// oldest.
|
||||
final long timeSinceLastRef = SystemClock.elapsedRealtime() -
|
||||
imageToUse.getLastRefAddTimestamp();
|
||||
if (timeSinceLastRef < MIN_TIME_IN_POOL) {
|
||||
if (LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "Not reusing reusing " +
|
||||
"first available bitmap from the pool because it " +
|
||||
"has not been in the pool long enough. " +
|
||||
"timeSinceLastRef=" + timeSinceLastRef);
|
||||
}
|
||||
// Put back the image and return no reuseable bitmap.
|
||||
images.addLast(imageToUse);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add a temp ref on the image resource so it won't be GC'd after
|
||||
// being removed from the cache.
|
||||
imageToUse.addRef();
|
||||
|
||||
// Remove the image resource from the image cache.
|
||||
final ImageResource removed = remove(imageToUse.getKey());
|
||||
Assert.isTrue(removed == imageToUse);
|
||||
|
||||
// Try to reuse the bitmap from the image resource. This will transfer
|
||||
// ownership of the bitmap object to the caller of this method.
|
||||
final Bitmap reusableBitmap = imageToUse.reuseBitmap();
|
||||
|
||||
imageToUse.release();
|
||||
return reusableBitmap;
|
||||
} finally {
|
||||
// We are either done with the reuse operation, or decided not to use
|
||||
// the image. Either way, release the lock.
|
||||
imageToUse.releaseLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to locate and return a reusable bitmap from the pool, or create a new bitmap.
|
||||
* @param width desired bitmap width
|
||||
* @param height desired bitmap height
|
||||
* @return the created or reused mutable bitmap that has its background cleared to
|
||||
* {@value Color#TRANSPARENT}
|
||||
*/
|
||||
public Bitmap createOrReuseBitmap(final int width, final int height) {
|
||||
return createOrReuseBitmap(width, height, Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to locate and return a reusable bitmap from the pool, or create a new bitmap.
|
||||
* @param width desired bitmap width
|
||||
* @param height desired bitmap height
|
||||
* @param backgroundColor the background color for the returned bitmap
|
||||
* @return the created or reused mutable bitmap with the requested background color
|
||||
*/
|
||||
public Bitmap createOrReuseBitmap(final int width, final int height,
|
||||
final int backgroundColor) {
|
||||
Bitmap retBitmap = null;
|
||||
try {
|
||||
final Bitmap poolBitmap = getReusableBitmapFromPool(width, height);
|
||||
retBitmap = (poolBitmap != null) ? poolBitmap :
|
||||
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
retBitmap.eraseColor(backgroundColor);
|
||||
} catch (final OutOfMemoryError e) {
|
||||
LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache:try to createOrReuseBitmap");
|
||||
Factory.get().reclaimMemory();
|
||||
}
|
||||
return retBitmap;
|
||||
}
|
||||
|
||||
private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width,
|
||||
final int height) {
|
||||
if (optionsTmp.inJustDecodeBounds) {
|
||||
return;
|
||||
}
|
||||
optionsTmp.inBitmap = getReusableBitmapFromPool(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The pool key for the provided image dimensions or 0 if either width or height is
|
||||
* greater than the max supported image dimension.
|
||||
*/
|
||||
private int getPoolKey(final int width, final int height) {
|
||||
if (width > MAX_SUPPORTED_IMAGE_DIMENSION || height > MAX_SUPPORTED_IMAGE_DIMENSION) {
|
||||
return INVALID_POOL_KEY;
|
||||
}
|
||||
return (width << 16) | height;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the pool key for a given image resource.
|
||||
*/
|
||||
private int getPoolKey(final ImageResource imageResource) {
|
||||
if (imageResource.supportsBitmapReuse()) {
|
||||
final Bitmap bitmap = imageResource.getBitmap();
|
||||
if (bitmap != null && bitmap.isMutable()) {
|
||||
final int width = bitmap.getWidth();
|
||||
final int height = bitmap.getHeight();
|
||||
if (width > 0 && height > 0) {
|
||||
return getPoolKey(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
return INVALID_POOL_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when bitmap reuse fails. Conditionally report the failure with statistics.
|
||||
*/
|
||||
private void onFailedToReuse() {
|
||||
mFailedBitmapReuseCount++;
|
||||
if (mFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
|
||||
LogUtil.w(LogUtil.BUGLE_IMAGE_TAG,
|
||||
"Pooled bitmap consistently not being reused. Failure count = " +
|
||||
mFailedBitmapReuseCount + ", success count = " +
|
||||
mSucceededBitmapReuseCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user