Test: make messaging Bug: 76692459 Change-Id: I9103450622f129311545648ac75f5b2f9fc2d04a Merged-In: I9103450622f129311545648ac75f5b2f9fc2d04a
365 lines
14 KiB
Java
365 lines
14 KiB
Java
/*
|
|
* 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;
|
|
|
|
import android.content.res.Resources;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.BitmapFactory;
|
|
import androidx.annotation.NonNull;
|
|
import android.text.TextUtils;
|
|
import android.util.SparseArray;
|
|
|
|
import com.android.messaging.datamodel.MemoryCacheManager.MemoryCache;
|
|
import com.android.messaging.util.Assert;
|
|
import com.android.messaging.util.LogUtil;
|
|
|
|
import java.io.InputStream;
|
|
|
|
/**
|
|
* Class for creating / loading / reusing bitmaps. This class allow the user to create a new bitmap,
|
|
* reuse an bitmap from the pool and to return a bitmap for future reuse. The pool of bitmaps
|
|
* allows for faster decode and more efficient memory usage.
|
|
* Note: consumers should not create BitmapPool directly, but instead get the pool they want from
|
|
* the BitmapPoolManager.
|
|
*/
|
|
public class BitmapPool implements MemoryCache {
|
|
public static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF;
|
|
|
|
protected static final boolean VERBOSE = false;
|
|
|
|
/**
|
|
* Number of reuse failures to skip before reporting.
|
|
*/
|
|
private static final int FAILED_REPORTING_FREQUENCY = 100;
|
|
|
|
/**
|
|
* Count of reuse failures which have occurred.
|
|
*/
|
|
private static volatile int sFailedBitmapReuseCount = 0;
|
|
|
|
/**
|
|
* Overall pool data structure which currently only supports rectangular bitmaps. The size of
|
|
* one of the sides is used to index into the SparseArray.
|
|
*/
|
|
private final SparseArray<SingleSizePool> mPool;
|
|
private final Object mPoolLock = new Object();
|
|
private final String mPoolName;
|
|
private final int mMaxSize;
|
|
|
|
/**
|
|
* Inner structure which holds a pool of bitmaps all the same size (i.e. all have the same
|
|
* width as each other and height as each other, but not necessarily the same).
|
|
*/
|
|
private class SingleSizePool {
|
|
int mNumItems;
|
|
final Bitmap[] mBitmaps;
|
|
|
|
SingleSizePool(final int maxPoolSize) {
|
|
mNumItems = 0;
|
|
mBitmaps = new Bitmap[maxPoolSize];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a pool of reused bitmaps with helper decode methods which will attempt to use the
|
|
* reclaimed bitmaps. This will help speed up the creation of bitmaps by using already allocated
|
|
* bitmaps.
|
|
* @param maxSize The overall max size of the pool. When the pool exceeds this size, all calls
|
|
* to reclaimBitmap(Bitmap) will result in recycling the bitmap.
|
|
* @param name Name of the bitmap pool and only used for logging. Can not be null.
|
|
*/
|
|
BitmapPool(final int maxSize, @NonNull final String name) {
|
|
Assert.isTrue(maxSize > 0);
|
|
Assert.isTrue(!TextUtils.isEmpty(name));
|
|
mPoolName = name;
|
|
mMaxSize = maxSize;
|
|
mPool = new SparseArray<SingleSizePool>();
|
|
}
|
|
|
|
@Override
|
|
public void reclaim() {
|
|
synchronized (mPoolLock) {
|
|
for (int p = 0; p < mPool.size(); p++) {
|
|
final SingleSizePool singleSizePool = mPool.valueAt(p);
|
|
for (int i = 0; i < singleSizePool.mNumItems; i++) {
|
|
singleSizePool.mBitmaps[i].recycle();
|
|
singleSizePool.mBitmaps[i] = null;
|
|
}
|
|
singleSizePool.mNumItems = 0;
|
|
}
|
|
mPool.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new BitmapFactory.Options.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @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 0;
|
|
}
|
|
return (width << 16) | height;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @return A bitmap in the pool with the specified dimensions or null if no bitmap with the
|
|
* specified dimension is available.
|
|
*/
|
|
private Bitmap findPoolBitmap(final int width, final int height) {
|
|
final int poolKey = getPoolKey(width, height);
|
|
if (poolKey != 0) {
|
|
synchronized (mPoolLock) {
|
|
// Take a bitmap from the pool if one is available
|
|
final SingleSizePool singlePool = mPool.get(poolKey);
|
|
if (singlePool != null && singlePool.mNumItems > 0) {
|
|
singlePool.mNumItems--;
|
|
final Bitmap foundBitmap = singlePool.mBitmaps[singlePool.mNumItems];
|
|
singlePool.mBitmaps[singlePool.mNumItems] = null;
|
|
return foundBitmap;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Internal function to try and find a bitmap in the pool which matches the desired width and
|
|
* height and then set that in the bitmap options properly.
|
|
*
|
|
* TODO: Why do we take a width/height? Shouldn't this already be in the
|
|
* BitmapFactory.Options instance? Can we assert that they match?
|
|
* @param optionsTmp The BitmapFactory.Options to update with the bitmap for the system to try
|
|
* to reuse.
|
|
* @param width The width of the reusable bitmap.
|
|
* @param height The height of the reusable bitmap.
|
|
*/
|
|
private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width,
|
|
final int height) {
|
|
if (optionsTmp.inJustDecodeBounds) {
|
|
return;
|
|
}
|
|
optionsTmp.inBitmap = findPoolBitmap(width, height);
|
|
}
|
|
|
|
/**
|
|
* Load a resource into a bitmap. Uses a bitmap from the pool if possible to reduce memory
|
|
* turnover.
|
|
* @param resourceId Resource id to load.
|
|
* @param resources Application resources. 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.
|
|
*/
|
|
public Bitmap decodeSampledBitmapFromResource(final int resourceId,
|
|
@NonNull final Resources resources, @NonNull final BitmapFactory.Options optionsTmp,
|
|
final int width, final int height) {
|
|
Assert.notNull(resources);
|
|
Assert.notNull(optionsTmp);
|
|
Assert.isTrue(width > 0);
|
|
Assert.isTrue(height > 0);
|
|
assignPoolBitmap(optionsTmp, width, height);
|
|
Bitmap b = null;
|
|
try {
|
|
b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
|
|
} catch (final IllegalArgumentException e) {
|
|
// BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
|
|
if (optionsTmp.inBitmap != null) {
|
|
optionsTmp.inBitmap = null;
|
|
b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
|
|
sFailedBitmapReuseCount++;
|
|
if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
|
|
LogUtil.w(LogUtil.BUGLE_TAG,
|
|
"Pooled bitmap consistently not being reused count = " +
|
|
sFailedBitmapReuseCount);
|
|
}
|
|
}
|
|
} catch (final OutOfMemoryError e) {
|
|
LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding resource " + resourceId);
|
|
reclaim();
|
|
}
|
|
return b;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream,
|
|
@NonNull final BitmapFactory.Options optionsTmp,
|
|
final int width, final int height) {
|
|
Assert.notNull(inputStream);
|
|
Assert.isTrue(width > 0);
|
|
Assert.isTrue(height > 0);
|
|
assignPoolBitmap(optionsTmp, width, height);
|
|
Bitmap b = null;
|
|
try {
|
|
b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
|
|
} catch (final IllegalArgumentException e) {
|
|
// BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
|
|
if (optionsTmp.inBitmap != null) {
|
|
optionsTmp.inBitmap = null;
|
|
b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
|
|
sFailedBitmapReuseCount++;
|
|
if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
|
|
LogUtil.w(LogUtil.BUGLE_TAG,
|
|
"Pooled bitmap consistently not being reused count = " +
|
|
sFailedBitmapReuseCount);
|
|
}
|
|
}
|
|
} catch (final OutOfMemoryError e) {
|
|
LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding inputStream");
|
|
reclaim();
|
|
}
|
|
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.
|
|
*/
|
|
public Bitmap decodeByteArray(@NonNull final byte[] bytes,
|
|
@NonNull final BitmapFactory.Options optionsTmp, final int width,
|
|
final int height) throws OutOfMemoryError {
|
|
Assert.notNull(bytes);
|
|
Assert.notNull(optionsTmp);
|
|
Assert.isTrue(width > 0);
|
|
Assert.isTrue(height > 0);
|
|
assignPoolBitmap(optionsTmp, width, height);
|
|
Bitmap b = null;
|
|
try {
|
|
b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
|
|
} catch (final IllegalArgumentException e) {
|
|
if (VERBOSE) {
|
|
LogUtil.v(LogUtil.BUGLE_TAG, "BitmapPool(" + mPoolName +
|
|
") Unable to use pool bitmap");
|
|
}
|
|
// 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 = null;
|
|
b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
|
|
sFailedBitmapReuseCount++;
|
|
if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
|
|
LogUtil.w(LogUtil.BUGLE_TAG,
|
|
"Pooled bitmap consistently not being reused count = " +
|
|
sFailedBitmapReuseCount);
|
|
}
|
|
}
|
|
}
|
|
return b;
|
|
}
|
|
|
|
/**
|
|
* Creates a bitmap with the given size, this will reuse a bitmap in the pool, if one is
|
|
* available, otherwise this will create a new one.
|
|
* @param width The desired width of the bitmap.
|
|
* @param height The desired height of the bitmap.
|
|
* @return A bitmap with the desired width and height, this maybe a reused bitmap from the pool.
|
|
*/
|
|
public Bitmap createOrReuseBitmap(final int width, final int height) {
|
|
Bitmap b = findPoolBitmap(width, height);
|
|
if (b == null) {
|
|
b = createBitmap(width, height);
|
|
}
|
|
return b;
|
|
}
|
|
|
|
/**
|
|
* This will create a new bitmap regardless of pool state.
|
|
* @param width The desired width of the bitmap.
|
|
* @param height The desired height of the bitmap.
|
|
* @return A bitmap with the desired width and height.
|
|
*/
|
|
private Bitmap createBitmap(final int width, final int height) {
|
|
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
}
|
|
|
|
/**
|
|
* Called when a bitmap is finished being used so that it can be used for another bitmap in the
|
|
* future or recycled. Any bitmaps returned should not be used by the caller again.
|
|
* @param b The bitmap to return to the pool for future usage or recycled. This cannot be null.
|
|
*/
|
|
public void reclaimBitmap(@NonNull final Bitmap b) {
|
|
Assert.notNull(b);
|
|
final int poolKey = getPoolKey(b.getWidth(), b.getHeight());
|
|
if (poolKey == 0 || !b.isMutable()) {
|
|
// Unsupported image dimensions or a immutable bitmap.
|
|
b.recycle();
|
|
return;
|
|
}
|
|
synchronized (mPoolLock) {
|
|
SingleSizePool singleSizePool = mPool.get(poolKey);
|
|
if (singleSizePool == null) {
|
|
singleSizePool = new SingleSizePool(mMaxSize);
|
|
mPool.append(poolKey, singleSizePool);
|
|
}
|
|
if (singleSizePool.mNumItems < singleSizePool.mBitmaps.length) {
|
|
singleSizePool.mBitmaps[singleSizePool.mNumItems] = b;
|
|
singleSizePool.mNumItems++;
|
|
} else {
|
|
b.recycle();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return whether the pool is full for a given width and height.
|
|
*/
|
|
public boolean isFull(final int width, final int height) {
|
|
final int poolKey = getPoolKey(width, height);
|
|
synchronized (mPoolLock) {
|
|
final SingleSizePool singleSizePool = mPool.get(poolKey);
|
|
if (singleSizePool != null &&
|
|
singleSizePool.mNumItems >= singleSizePool.mBitmaps.length) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|