/*******************************************************************************
* Copyright 2011, 2013 Chris Banes.
* Copyright (C) 2012, 2013 Eyal LEZMY (http://www.eyal.fr)
*
* 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 fr.eyal.datalib.sample.cache;
import fr.eyal.lib.util.Out;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
public class CacheableBitmapDrawable extends BitmapDrawable {
static final String LOG_TAG = "CacheableBitmapDrawable";
// URL Associated with this Bitmap
private String mUrl;
private RecyclePolicy mRecyclePolicy;
// Number of Views currently displaying bitmap
private int mDisplayingCount;
// Has it been displayed yet
private boolean mHasBeenDisplayed;
// Number of caches currently referencing the wrapper
private int mCacheCount;
// The CheckStateRunnable currently being delayed
private Runnable mCheckStateRunnable;
// Handler which may be used later
private static final Handler sHandler = new Handler(Looper.getMainLooper());
private static final long UNUSED_DRAWABLE_RECYCLE_DELAY_MS = 300;
@SuppressWarnings("deprecation")
public CacheableBitmapDrawable(String url, Bitmap bitmap, RecyclePolicy recyclePolicy) {
super(bitmap);
init(url, recyclePolicy);
}
public CacheableBitmapDrawable(Resources resources, String url, Bitmap bitmap, RecyclePolicy recyclePolicy) {
super(resources, bitmap);
init(url, recyclePolicy);
}
private void init(String url, RecyclePolicy recyclePolicy) {
mUrl = url;
mRecyclePolicy = recyclePolicy;
mDisplayingCount = 0;
mCacheCount = 0;
}
/**
* @return Amount of heap size currently being used by {@code Bitmap}
*/
int getMemorySize() {
int size = 0;
final Bitmap bitmap = getBitmap();
if (null != bitmap && !bitmap.isRecycled()) {
size = bitmap.getRowBytes() * bitmap.getHeight();
}
return size;
}
/**
* @return the URL associated with the BitmapDrawable
*/
public String getUrl() {
return mUrl;
}
/**
* Returns true when this wrapper has a bitmap and the bitmap has not been
* recycled.
*
* @return true - if the bitmap has not been recycled.
*/
public synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
if (null != bitmap) {
return !bitmap.isRecycled();
}
return false;
}
/**
* @return true - if the bitmap is currently being displayed by a
* {@link CacheableImageView}.
*/
public synchronized boolean isBeingDisplayed() {
return mDisplayingCount > 0;
}
/**
* @return true - if the wrapper is currently referenced by a cache.
*/
public synchronized boolean isReferencedByCache() {
return mCacheCount > 0;
}
/**
* Used to signal to the Drawable whether it is being used or not.
*
* @param beingUsed - true if being used, false if not.
*/
public synchronized void setBeingUsed(boolean beingUsed) {
if (beingUsed) {
mDisplayingCount++;
mHasBeenDisplayed = true;
} else {
mDisplayingCount--;
}
checkState();
}
/**
* Used to signal to the wrapper whether it is being referenced by a cache
* or not.
*
* @param added - true if the wrapper has been added to a cache, false if
* removed.
*/
synchronized void setCached(boolean added) {
if (added) {
mCacheCount++;
} else {
mCacheCount--;
}
checkState();
}
private void cancelCheckStateCallback() {
if (null != mCheckStateRunnable) {
Out.d(LOG_TAG, "Cancelling checkState() callback for: " + mUrl);
sHandler.removeCallbacks(mCheckStateRunnable);
mCheckStateRunnable = null;
}
}
/**
* Calls {@link #checkState(boolean)} with default parameter of
* <code>false</code>.
*/
private void checkState() {
checkState(false);
}
/**
* Checks whether the wrapper is currently referenced by a cache, and is
* being displayed. If neither of those conditions are met then the bitmap
* is ready to be recycled. Whether this happens now, or is delayed depends
* on whether the Drawable has been displayed or not.
* <ul>
* <li>If it has been displayed, it is recycled straight away.</li>
* <li>If it has not been displayed, and <code>ignoreBeenDisplayed</code> is
* <code>false</code>, a call to <code>checkState(true)</code> is queued to
* be called after a delay.</li>
* <li>If it has not been displayed, and <code>ignoreBeenDisplayed</code> is
* <code>true</code>, it is recycled straight away.</li>
* </ul>
*
* @see Constants#UNUSED_DRAWABLE_RECYCLE_DELAY_MS
*
* @param ignoreBeenDisplayed - Whether to ignore the 'has been displayed'
* flag when deciding whether to recycle() now.
*/
private synchronized void checkState(final boolean ignoreBeenDisplayed) {
Out.d(LOG_TAG, String.format("checkState(). Been Displayed: %b, Displaying: %d, Caching: %d, URL: %s",
mHasBeenDisplayed, mDisplayingCount, mCacheCount, mUrl));
// If the policy doesn't let us recycle, return now
if (!mRecyclePolicy.canRecycle()) {
return;
}
if (mCacheCount <= 0 && mDisplayingCount <= 0) {
// We're not being referenced or used anywhere
// Cancel the callback, if one is queued.
cancelCheckStateCallback();
if (hasValidBitmap()) {
/**
* If we have been displayed or we don't care whether we have
* been or not, then recycle() now. Otherwise, we retry in 1
* second.
*/
if (mHasBeenDisplayed || ignoreBeenDisplayed) {
Out.d(LOG_TAG, "Recycling bitmap with url: " + mUrl);
getBitmap().recycle();
} else {
Out.d(LOG_TAG, "Unused Bitmap which hasn't been displayed, delaying recycle(): " + mUrl);
mCheckStateRunnable = new CheckStateRunnable(this);
sHandler.postDelayed(mCheckStateRunnable, UNUSED_DRAWABLE_RECYCLE_DELAY_MS);
}
}
} else {
// We're being referenced (by either a cache or used somewhere)
/**
* If mCheckStateRunnable isn't null, then a checkState() call has
* been queued previously. As we're being used now, cancel the
* callback.
*/
cancelCheckStateCallback();
}
}
/**
* Runnable which run a {@link CacheableBitmapDrawable#checkState(boolean)
* checkState(false)} call.
*
* @author chrisbanes
*/
private static final class CheckStateRunnable extends WeakReferenceRunnable<CacheableBitmapDrawable> {
public CheckStateRunnable(CacheableBitmapDrawable object) {
super(object);
}
@Override
public void run(CacheableBitmapDrawable object) {
object.checkState(true);
}
}
/**
* The recycle policy controls if the {@link android.graphics.Bitmap#recycle()} is automatically called, when it is
* no longer being used. To set this, use the
* {@link Builder#setRecyclePolicy(uk.co.senab.bitmapcache.BitmapLruCache.RecyclePolicy) Builder.setRecyclePolicy()} method.
*/
public static enum RecyclePolicy {
/**
* The Bitmap is never recycled automatically.
*/
DISABLED,
/**
* The Bitmap is only automatically recycled if running on a device API v10 or earlier.
*/
PRE_HONEYCOMB_ONLY,
/**
* The Bitmap is always recycled when no longer being used. This is the default.
*/
ALWAYS;
boolean canRecycle() {
switch (this) {
case DISABLED:
return false;
case PRE_HONEYCOMB_ONLY:
return Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB;
case ALWAYS:
return true;
}
return false;
}
}
}