/*
* 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.cache;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import com.facebook.common.internal.VisibleForTesting;
import com.android.internal.util.Predicate;
/**
* Map that keeps track of the elements order (according to the LRU policy) and their size.
*/
@ThreadSafe
public class CountingLruMap<K, V> {
private final ValueDescriptor<V> mValueDescriptor;
@GuardedBy("this")
private final LinkedHashMap<K, V> mMap = new LinkedHashMap<>();
@GuardedBy("this")
private int mSizeInBytes = 0;
public CountingLruMap(ValueDescriptor<V> valueDescriptor) {
mValueDescriptor = valueDescriptor;
}
@VisibleForTesting
synchronized ArrayList<K> getKeys() {
return new ArrayList<>(mMap.keySet());
}
@VisibleForTesting
synchronized ArrayList<V> getValues() {
return new ArrayList<>(mMap.values());
}
/** Gets the count of the elements in the map. */
public synchronized int getCount() {
return mMap.size();
}
/** Gets the total size in bytes of the elements in the map. */
public synchronized int getSizeInBytes() {
return mSizeInBytes;
}
/** Gets the key of the first element in the map. */
@Nullable
public synchronized K getFirstKey() {
return mMap.isEmpty() ? null : mMap.keySet().iterator().next();
}
/** Gets the all matching elements. */
public synchronized ArrayList<LinkedHashMap.Entry<K, V>> getMatchingEntries(
@Nullable Predicate<K> predicate) {
ArrayList<LinkedHashMap.Entry<K, V>> matchingEntries = new ArrayList<>();
for (LinkedHashMap.Entry<K, V> entry : mMap.entrySet()) {
if (predicate == null || predicate.apply(entry.getKey())) {
matchingEntries.add(entry);
}
}
return matchingEntries;
}
/** Returns whether the map contains an element with the given key. */
public synchronized boolean contains(K key) {
return mMap.containsKey(key);
}
/** Gets the element from the map. */
@Nullable
public synchronized V get(K key) {
return mMap.get(key);
}
/** Adds the element to the map, and removes the old element with the same key if any. */
@Nullable
public synchronized V put(K key, V value) {
// We do remove and insert instead of just replace, in order to cause a structural change
// to the map, as we always want the latest inserted element to be last in the queue.
V oldValue = mMap.remove(key);
mSizeInBytes -= getValueSizeInBytes(oldValue);
mMap.put(key, value);
mSizeInBytes += getValueSizeInBytes(value);
return oldValue;
}
/** Removes the element from the map. */
@Nullable
public synchronized V remove(K key) {
V oldValue = mMap.remove(key);
mSizeInBytes -= getValueSizeInBytes(oldValue);
return oldValue;
}
/** Removes all the matching elements from the map. */
public synchronized ArrayList<V> removeAll(@Nullable Predicate<K> predicate) {
ArrayList<V> oldValues = new ArrayList<>();
Iterator<LinkedHashMap.Entry<K, V>> iterator = mMap.entrySet().iterator();
while (iterator.hasNext()) {
LinkedHashMap.Entry<K, V> entry = iterator.next();
if (predicate == null || predicate.apply(entry.getKey())) {
oldValues.add(entry.getValue());
mSizeInBytes -= getValueSizeInBytes(entry.getValue());
iterator.remove();
}
}
return oldValues;
}
/** Clears the map. */
public synchronized ArrayList<V> clear() {
ArrayList<V> oldValues = new ArrayList<>(mMap.values());
mMap.clear();
mSizeInBytes = 0;
return oldValues;
}
private int getValueSizeInBytes(V value) {
return (value == null) ? 0 : mValueDescriptor.getSizeInBytes(value);
}
}