package org.freemp.droid;
/*
* Copyright (C) 2011 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.
*/
import android.content.Context;
import android.util.Log;
import java.lang.ref.SoftReference;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Static library version of {@code android.util.LruCache}. Used to write apps
* that run on API levels prior to 12. When running on API level 12 or above,
* this implementation is still used; it does not try to switch to the
* framework's implementation. See the framework SDK documentation for a class
* overview.
*/
public class LruSoftCache<K, V> {
private static final String LOG_TAG = LruSoftCache.class.getSimpleName();
private final LinkedHashMap<K, SoftReference<V>> map;
/**
* Size of this cache in units. Not necessarily the number of elements.
*/
private int size;
private int maxSize;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is the
* maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this
* cache.
*/
public LruSoftCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, SoftReference<V>>(0, 0.75f, true);
}
/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created.
*/
public final V get(Context context, K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
SoftReference<V> mapValue;
V mapReferent = null;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
mapReferent = mapValue.get();
}
if (mapReferent != null) {
hitCount++;
return mapReferent;
}
if (mapValue != null) {
size -= safeSizeOf(key, null);
}
map.remove(key);
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map may
* be different when create() returns. If a conflicting value was added
* to the map while create() was working, we leave that value in the map
* and release the created value.
*/
V createdReferent = create(context, key);
if (createdReferent == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, new SoftReference<V>(createdReferent));
mapReferent = mapValue.get();
if (mapValue != null && mapReferent != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdReferent);
}
}
if (mapValue != null && mapReferent != null) {
entryRemoved(false, key, createdReferent, mapReferent);
return mapReferent;
} else {
trimToSize(maxSize);
return createdReferent;
}
}
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
public final V put(K key, V referent) {
if (key == null || referent == null) {
throw new NullPointerException("key == null || value == null");
}
SoftReference<V> value = new SoftReference<V>(referent);
SoftReference<V> previousValue;
V previousReferent = null;
synchronized (this) {
putCount++;
size += safeSizeOf(key, referent);
previousValue = map.put(key, value);
if (previousValue != null) {
previousReferent = previousValue.get();
size -= safeSizeOf(key, previousReferent);
}
}
if (previousValue != null) {
entryRemoved(false, key, previousReferent, referent);
}
trimToSize(maxSize);
return previousReferent;
}
/**
* @param maxSize the maximum size of the cache before returning. May be -1 to
* evict even 0-sized elements.
*/
private void trimToSize(int maxSize) {
while (true) {
K key;
SoftReference<V> value;
V referent;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
size = 0;
map.clear();
Log.e(LOG_TAG,
getClass().getName()
+ ".sizeOf() is reporting inconsistent results! size: "
+ size + ", maxSize: " + maxSize);
break;
}
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<K, SoftReference<V>> toEvict = map.entrySet()
.iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
referent = (value != null) ? value.get() : null;
map.remove(key);
size -= safeSizeOf(key, referent);
evictionCount++;
}
entryRemoved(true, key, referent, null);
}
}
/**
* Removes the entry for {@code key} if it exists.
*
* @return the previous value mapped by {@code key}.
*/
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
SoftReference<V> previousValue;
V previousReferent = null;
synchronized (this) {
previousValue = map.remove(key);
if (previousValue != null) {
previousReferent = previousValue.get();
size -= safeSizeOf(key, previousReferent);
}
}
if (previousValue != null) {
entryRemoved(false, key, previousReferent, null);
}
return previousReferent;
}
/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
* {@link #remove}, or replaced by a call to {@link #put}. The default
* implementation does nothing.
* <p>
* <p>
* The method is called without synchronization: other threads may access
* the cache while this method is executing.
*
* @param evicted true if the entry is being removed to make space, false if the
* removal was caused by a {@link #put} or {@link #remove}.
* @param newValue the new value for {@code key}, if it exists. If non-null, this
* removal was caused by a {@link #put}. Otherwise it was caused
* by an eviction or a {@link #remove}.
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
/**
* Called after a cache miss to compute a value for the corresponding key.
* Returns the computed value or null if no value can be computed. The
* default implementation returns null.
* <p>
* <p>
* The method is called without synchronization: other threads may access
* the cache while this method is executing.
* <p>
* <p>
* If a value for {@code key} exists in the cache when this method returns,
* the created value will be released with {@link #entryRemoved} and
* discarded. This can occur when multiple threads request the same key at
* the same time (causing multiple values to be created), or when one thread
* calls {@link #put} while another is creating a value for the same key.
*/
protected V create(Context context, K key) {
return null;
}
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "="
+ value);
}
return result;
}
/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units. The default implementation returns 1 so that size is
* the number of entries and max size is the maximum number of entries.
* <p>
* <p>
* An entry's size must not change while it is in the cache.
*/
private final int sizeOf(K key, V value) {
return 1;
}
/**
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
/**
* For caches that do not override {@link #sizeOf}, this returns the number
* of entries in the cache. For all other caches, this returns the sum of
* the sizes of the entries in this cache.
*/
public synchronized final int size() {
return size;
}
/**
* For caches that do not override {@link #sizeOf}, this returns the maximum
* number of entries in the cache. For all other caches, this returns the
* maximum sum of the sizes of the entries in this cache.
*/
public synchronized final int maxSize() {
return maxSize;
}
/**
* Returns the number of times {@link #get} returned a value.
*/
public synchronized final int hitCount() {
return hitCount;
}
/**
* Returns the number of times {@link #get} returned null or required a new
* value to be created.
*/
public synchronized final int missCount() {
return missCount;
}
/**
* Returns the number of times returned a value.
*/
public synchronized final int createCount() {
return createCount;
}
/**
* Returns the number of times {@link #put} was called.
*/
public synchronized final int putCount() {
return putCount;
}
/**
* Returns the number of values that have been evicted.
*/
public synchronized final int evictionCount() {
return evictionCount;
}
@Override
public synchronized final String toString() {
int accesses = hitCount + missCount;
int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
return String
.format("LruCache[size=%d,mapSize=%d,maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
size, map.size(), maxSize, hitCount, missCount,
hitPercent);
}
}