/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.imagepipeline.animated.impl;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import java.util.Iterator;
import java.util.LinkedHashSet;
import android.net.Uri;
import com.facebook.cache.common.CacheKey;
import com.facebook.common.internal.Objects;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.cache.CountingMemoryCache;
import com.facebook.imagepipeline.image.CloseableImage;
/**
* Facade to the image memory cache for frames of an animated image.
*
* <p> Each animated image should have its own instance of this class.
*/
public class AnimatedFrameCache {
@VisibleForTesting
static class FrameKey implements CacheKey {
private final CacheKey mImageCacheKey;
private final int mFrameIndex;
public FrameKey(CacheKey imageCacheKey, int frameIndex) {
mImageCacheKey = imageCacheKey;
mFrameIndex = frameIndex;
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("imageCacheKey", mImageCacheKey)
.add("frameIndex", mFrameIndex)
.toString();
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof FrameKey) {
FrameKey that = (FrameKey) o;
return this.mImageCacheKey == that.mImageCacheKey &&
this.mFrameIndex == that.mFrameIndex;
}
return false;
}
@Override
public int hashCode() {
return mImageCacheKey.hashCode() * 1013 + mFrameIndex;
}
@Override
public boolean containsUri(Uri uri) {
return mImageCacheKey.containsUri(uri);
}
@Override
public String getUriString() {
return null;
}
}
private final CacheKey mImageCacheKey;
private final CountingMemoryCache<CacheKey, CloseableImage> mBackingCache;
private final CountingMemoryCache.EntryStateObserver<CacheKey> mEntryStateObserver;
@GuardedBy("this")
private final LinkedHashSet<CacheKey> mFreeItemsPool;
public AnimatedFrameCache(
CacheKey imageCacheKey,
final CountingMemoryCache<CacheKey, CloseableImage> backingCache) {
mImageCacheKey = imageCacheKey;
mBackingCache = backingCache;
mFreeItemsPool = new LinkedHashSet<>();
mEntryStateObserver = new CountingMemoryCache.EntryStateObserver<CacheKey>() {
@Override
public void onExclusivityChanged(CacheKey key, boolean isExclusive) {
AnimatedFrameCache.this.onReusabilityChange(key, isExclusive);
}
};
}
public synchronized void onReusabilityChange(CacheKey key, boolean isReusable) {
if (isReusable) {
mFreeItemsPool.add(key);
} else {
mFreeItemsPool.remove(key);
}
}
/**
* Caches the image for the given frame index.
*
* <p> Important: the client should use the returned reference instead of the original one.
* It is the caller's responsibility to close the returned reference once not needed anymore.
*
* @return the new reference to be used, null if the value cannot be cached
*/
@Nullable
public CloseableReference<CloseableImage> cache(
int frameIndex,
CloseableReference<CloseableImage> imageRef) {
return mBackingCache.cache(keyFor(frameIndex), imageRef, mEntryStateObserver);
}
/**
* Gets the image for the given frame index.
*
* <p> It is the caller's responsibility to close the returned reference once not needed anymore.
*/
@Nullable
public CloseableReference<CloseableImage> get(int frameIndex) {
return mBackingCache.get(keyFor(frameIndex));
}
/**
* Check whether the cache contains an image for the given frame index.
*/
public boolean contains(int frameIndex) {
return mBackingCache.contains(keyFor(frameIndex));
}
/**
* Gets the image to be reused, or null if there is no such image.
*
* <p> The returned image is the least recently used image that has no more clients referencing
* it, and it has not yet been evicted from the cache.
*
* <p> The client can freely modify the bitmap of the returned image and can cache it again
* without any restrictions.
*/
@Nullable
public CloseableReference<CloseableImage> getForReuse() {
while (true) {
CacheKey key = popFirstFreeItemKey();
if (key == null) {
return null;
}
CloseableReference<CloseableImage> imageRef = mBackingCache.reuse(key);
if (imageRef != null) {
return imageRef;
}
}
}
@Nullable
private synchronized CacheKey popFirstFreeItemKey() {
CacheKey cacheKey = null;
Iterator<CacheKey> iterator = mFreeItemsPool.iterator();
if (iterator.hasNext()) {
cacheKey = iterator.next();
iterator.remove();
}
return cacheKey;
}
private FrameKey keyFor(int frameIndex) {
return new FrameKey(mImageCacheKey, frameIndex);
}
}