mirror of https://github.com/tasks/tasks
Updated ImageDiskCache sources to unmodified LGPL versions
parent
e5de043fe7
commit
fee4b83a79
@ -0,0 +1,630 @@
|
||||
package edu.mit.mobile.android.imagecache;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2013 MIT Mobile Experience Lab
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.StatFs;
|
||||
import android.util.Log;
|
||||
|
||||
import com.timsu.astrid.BuildConfig;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A simple disk cache.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* By default, the maximum size of the cache is automatically set based on the amount of free space
|
||||
* available to the cache. Alternatively, a fixed size can be specified using
|
||||
* {@link #setCacheMaxSize(long)}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* By default, the cache will automatically maintain its size by periodically checking to see if it
|
||||
* estimates that a trim is needed and if it is, proceeding to running {@link #trim()} on a worker
|
||||
* thread. This feature can be controlled by {@link #setAutoTrimFrequency(int)}.
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="mailto:spomeroy@mit.edu">Steve Pomeroy</a>
|
||||
*
|
||||
* @param <K>
|
||||
* the key to store/retrieve the value
|
||||
* @param <V>
|
||||
* the value that will be stored to disk
|
||||
*/
|
||||
public abstract class DiskCache<K, V> {
|
||||
private static final String TAG = "DiskCache";
|
||||
|
||||
/**
|
||||
* Automatically determines the maximum size of the cache based on available free space.
|
||||
*/
|
||||
public static final int AUTO_MAX_CACHE_SIZE = 0;
|
||||
|
||||
/**
|
||||
* The default number of cache hits before {@link #trim()} is automatically triggered. See
|
||||
* {@link #setAutoTrimFrequency(int)}.
|
||||
*/
|
||||
public static final int DEFAULT_AUTO_TRIM_FREQUENCY = 10;
|
||||
|
||||
/**
|
||||
* Pass to {@link #setAutoTrimFrequency(int)} to disable automatic trimming. See {@link #trim()}
|
||||
* .
|
||||
*/
|
||||
public static final int AUTO_TRIM_DISABLED = 0;
|
||||
|
||||
// /////////////////////////////////////////////
|
||||
|
||||
private long mMaxDiskUsage = AUTO_MAX_CACHE_SIZE;
|
||||
|
||||
private MessageDigest hash;
|
||||
|
||||
private final File mCacheBase;
|
||||
private final String mCachePrefix, mCacheSuffix;
|
||||
|
||||
private final ConcurrentLinkedQueue<File> mQueue = new ConcurrentLinkedQueue<File>();
|
||||
|
||||
/**
|
||||
* In auto max cache mode, the maximum is set to the total free space divided by this amount.
|
||||
*/
|
||||
private static final int AUTO_MAX_CACHE_SIZE_DIVISOR = 10;
|
||||
|
||||
private int mAutoTrimFrequency = DEFAULT_AUTO_TRIM_FREQUENCY;
|
||||
|
||||
private final ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(1, 5, 60, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<Runnable>());
|
||||
|
||||
private int mAutoTrimHitCount = 1;
|
||||
|
||||
private long mEstimatedDiskUsage;
|
||||
|
||||
private long mEstimatedFreeSpace;
|
||||
|
||||
/**
|
||||
* Creates a new disk cache with no cachePrefix or cacheSuffix
|
||||
*
|
||||
* @param cacheBase
|
||||
*/
|
||||
public DiskCache(File cacheBase) {
|
||||
this(cacheBase, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new disk cache.
|
||||
*
|
||||
* @param cacheBase
|
||||
* The base directory within which all the cache files will be stored.
|
||||
* @param cachePrefix
|
||||
* If you want a prefix to the filenames, place one here. Otherwise, pass null.
|
||||
* @param cacheSuffix
|
||||
* A suffix to the cache filename. Null is also ok here.
|
||||
*/
|
||||
public DiskCache(File cacheBase, String cachePrefix, String cacheSuffix) {
|
||||
mCacheBase = cacheBase;
|
||||
mCachePrefix = cachePrefix;
|
||||
mCacheSuffix = cacheSuffix;
|
||||
|
||||
try {
|
||||
hash = MessageDigest.getInstance("SHA-1");
|
||||
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
try {
|
||||
hash = MessageDigest.getInstance("MD5");
|
||||
} catch (final NoSuchAlgorithmException e2) {
|
||||
final RuntimeException re = new RuntimeException("No available hashing algorithm");
|
||||
re.initCause(e2);
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
|
||||
updateDiskUsageInBg();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum size of the cache, in bytes. The default is to automatically manage the max
|
||||
* size based on the available disk space. This can be explicitly set by passing this
|
||||
* {@link #AUTO_MAX_CACHE_SIZE}.
|
||||
*
|
||||
* @param maxSize
|
||||
* maximum size of the cache, in bytes.
|
||||
*/
|
||||
public void setCacheMaxSize(long maxSize) {
|
||||
mMaxDiskUsage = maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* After this many puts, if it looks like there's a low space condition, {@link #trim()} will
|
||||
* automatically be called.
|
||||
*
|
||||
* @param autoTrimFrequency
|
||||
* Set to {@link #AUTO_TRIM_DISABLED} to turn off auto trim. The default is
|
||||
* {@link #DEFAULT_AUTO_TRIM_FREQUENCY}.
|
||||
*/
|
||||
public void setAutoTrimFrequency(int autoTrimFrequency) {
|
||||
mAutoTrimFrequency = autoTrimFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates cached estimates on the
|
||||
*/
|
||||
private void updateDiskUsageEstimates() {
|
||||
final long diskUsage = getCacheDiskUsage();
|
||||
|
||||
final long availableSpace = getFreeSpace();
|
||||
|
||||
synchronized (this) {
|
||||
mEstimatedDiskUsage = diskUsage;
|
||||
mEstimatedFreeSpace = availableSpace;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDiskUsageInBg() {
|
||||
mExecutor.execute(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
updateDiskUsageEstimates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amount of space free on the cache volume.
|
||||
*
|
||||
* @return free space in bytes.
|
||||
*/
|
||||
private long getFreeSpace() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
|
||||
return mCacheBase.getUsableSpace();
|
||||
} else {
|
||||
// maybe make singleton
|
||||
final StatFs stat = new StatFs(mCacheBase.getAbsolutePath());
|
||||
return (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache filename for the given key.
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
protected File getFile(K key) {
|
||||
return new File(mCacheBase, (mCachePrefix != null ? mCachePrefix : "") + hash(key)
|
||||
+ (mCacheSuffix != null ? mCacheSuffix : ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the value stored in the cache to disk by calling
|
||||
* {@link #toDisk(Object, Object, OutputStream)}.
|
||||
*
|
||||
* @param key
|
||||
* The key to find the value.
|
||||
* @param value
|
||||
* the data to be written to disk.
|
||||
*/
|
||||
public final synchronized void put(K key, V value) throws IOException, FileNotFoundException {
|
||||
final File saveHere = getFile(key);
|
||||
|
||||
final OutputStream os = new FileOutputStream(saveHere);
|
||||
toDisk(key, value, os);
|
||||
os.close();
|
||||
|
||||
mEstimatedDiskUsage += saveHere.length();
|
||||
|
||||
touchEntry(saveHere);
|
||||
|
||||
autotrim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the contents of the InputStream straight to disk. It is the caller's responsibility to
|
||||
* ensure it's the same type as what would be written with
|
||||
* {@link #toDisk(Object, Object, OutputStream)}
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
* @throws IOException
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public final void putRaw(K key, InputStream value) throws IOException, FileNotFoundException {
|
||||
|
||||
final File saveHere = getFile(key);
|
||||
|
||||
final File tempFile = new File(saveHere.getAbsolutePath() + ".temp");
|
||||
|
||||
boolean allGood = false;
|
||||
try {
|
||||
final OutputStream os = new FileOutputStream(tempFile);
|
||||
|
||||
inputStreamToOutputStream(value, os);
|
||||
os.close();
|
||||
|
||||
synchronized (this) {
|
||||
// overwrite
|
||||
saveHere.delete();
|
||||
tempFile.renameTo(saveHere);
|
||||
}
|
||||
allGood = true;
|
||||
} finally {
|
||||
// clean up on any exception
|
||||
if (!allGood) {
|
||||
saveHere.delete();
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
if (allGood) {
|
||||
mEstimatedDiskUsage += saveHere.length();
|
||||
|
||||
touchEntry(saveHere);
|
||||
|
||||
autotrim();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the key at the end of the queue, removing it if it's already present. This will cause it
|
||||
* to be removed last when {@link #trim()} is called.
|
||||
*
|
||||
* @param cacheFile
|
||||
*/
|
||||
private void touchEntry(File cacheFile) {
|
||||
if (mQueue.contains(cacheFile)) {
|
||||
mQueue.remove(cacheFile);
|
||||
}
|
||||
mQueue.add(cacheFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given key as accessed recently. This will deprioritize it from automatically being
|
||||
* purged upon {@link #trim()}.
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
protected void touchKey(K key) {
|
||||
touchEntry(getFile(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this every time you may be able to start a trim in the background. This implicitly runs
|
||||
* {@link #updateDiskUsageInBg()} each time it's called.
|
||||
*/
|
||||
private void autotrim() {
|
||||
if (mAutoTrimFrequency == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
mAutoTrimHitCount = (mAutoTrimHitCount + 1) % mAutoTrimFrequency;
|
||||
|
||||
if (mAutoTrimHitCount == 0
|
||||
&& mEstimatedDiskUsage > Math.min(mEstimatedFreeSpace, mMaxDiskUsage)) {
|
||||
|
||||
mExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDiskUsageInBg();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads from an inputstream, dumps to an outputstream
|
||||
*
|
||||
* @param is
|
||||
* @param os
|
||||
* @throws IOException
|
||||
*/
|
||||
static public void inputStreamToOutputStream(InputStream is, OutputStream os)
|
||||
throws IOException {
|
||||
final int bufsize = 8196 * 10;
|
||||
final byte[] cbuf = new byte[bufsize];
|
||||
|
||||
for (int readBytes = is.read(cbuf, 0, bufsize); readBytes > 0; readBytes = is.read(cbuf, 0,
|
||||
bufsize)) {
|
||||
os.write(cbuf, 0, readBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the value from disk using {@link #fromDisk(Object, InputStream)}.
|
||||
*
|
||||
* @param key
|
||||
* @return The value for key or null if the key doesn't map to any existing entries.
|
||||
*/
|
||||
public final synchronized V get(K key) throws IOException {
|
||||
final File readFrom = getFile(key);
|
||||
|
||||
if (!readFrom.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final InputStream is = new FileInputStream(readFrom);
|
||||
final V out = fromDisk(key, is);
|
||||
is.close();
|
||||
|
||||
touchEntry(readFrom);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the disk cache for a given key.
|
||||
*
|
||||
* @param key
|
||||
* @return true if the disk cache contains the given key
|
||||
*/
|
||||
public final synchronized boolean contains(K key) {
|
||||
final File readFrom = getFile(key);
|
||||
|
||||
return readFrom.exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the item from the disk cache.
|
||||
*
|
||||
* @param key
|
||||
* @return true if the cached item has been removed or was already removed, false if it was not
|
||||
* able to be removed.
|
||||
*/
|
||||
public synchronized boolean clear(K key) {
|
||||
final File readFrom = getFile(key);
|
||||
|
||||
if (!readFrom.exists()) {
|
||||
return true;
|
||||
}
|
||||
final long size = readFrom.length();
|
||||
|
||||
final boolean success = readFrom.delete();
|
||||
|
||||
if (success) {
|
||||
mEstimatedDiskUsage -= size;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the item from the disk cache.
|
||||
*
|
||||
* @param cacheFile
|
||||
* @return true if the cached item has been removed or was already removed, false if it was not
|
||||
* able to be removed.
|
||||
*/
|
||||
private synchronized boolean clear(File cacheFile) {
|
||||
|
||||
if (!cacheFile.exists()) {
|
||||
return true;
|
||||
}
|
||||
final long size = cacheFile.length();
|
||||
|
||||
final boolean success = cacheFile.delete();
|
||||
|
||||
if (success) {
|
||||
mEstimatedDiskUsage -= size;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache files from disk.
|
||||
*
|
||||
* Note: this only clears files that match the given prefix/suffix.
|
||||
*
|
||||
* @return true if the operation succeeded without error. It is possible that it will fail and
|
||||
* the cache ends up being partially cleared.
|
||||
*/
|
||||
public synchronized boolean clear() {
|
||||
boolean success = true;
|
||||
|
||||
for (final File cacheFile : mCacheBase.listFiles(mCacheFileFilter)) {
|
||||
if (!cacheFile.delete()) {
|
||||
Log.e(TAG, "error deleting " + cacheFile);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of files in the cache
|
||||
* @deprecated please use {@link #getCacheEntryCount()} or {@link #getCacheDiskUsage()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public int getCacheSize() {
|
||||
return getCacheEntryCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of files in the cache as it is on disk.
|
||||
*/
|
||||
public int getCacheEntryCount() {
|
||||
return mCacheBase.listFiles(mCacheFileFilter).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the size of the cache in bytes, as it is on disk.
|
||||
*/
|
||||
public long getCacheDiskUsage() {
|
||||
long usage = 0;
|
||||
for (final File cacheFile : mCacheBase.listFiles(mCacheFileFilter)) {
|
||||
usage += cacheFile.length();
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
||||
private final CacheFileFilter mCacheFileFilter = new CacheFileFilter();
|
||||
|
||||
private class CacheFileFilter implements FileFilter {
|
||||
@Override
|
||||
public boolean accept(File pathname) {
|
||||
final String path = pathname.getName();
|
||||
return (mCachePrefix != null ? path.startsWith(mCachePrefix) : true)
|
||||
&& (mCacheSuffix != null ? path.endsWith(mCacheSuffix) : true);
|
||||
}
|
||||
};
|
||||
|
||||
private final Comparator<File> mLastModifiedOldestFirstComparator = new Comparator<File>() {
|
||||
|
||||
@Override
|
||||
public int compare(File lhs, File rhs) {
|
||||
return Long.valueOf(lhs.lastModified()).compareTo(rhs.lastModified());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears out cache entries in order to reduce the on-disk usage to the desired maximum size.
|
||||
* This is a somewhat expensive operation, so it should be done on a background thread.
|
||||
*
|
||||
* @return the number of bytes worth of files that were trimmed.
|
||||
* @see #setCacheMaxSize(long)
|
||||
*/
|
||||
public synchronized long trim() {
|
||||
|
||||
long desiredSize;
|
||||
final long freeSpace = getFreeSpace();
|
||||
|
||||
if (mMaxDiskUsage > 0) {
|
||||
desiredSize = mMaxDiskUsage;
|
||||
} else {
|
||||
desiredSize = getFreeSpace() / AUTO_MAX_CACHE_SIZE_DIVISOR;
|
||||
}
|
||||
|
||||
desiredSize = Math.min(freeSpace, desiredSize);
|
||||
|
||||
final long sizeToTrim = Math.max(0, getCacheDiskUsage() - desiredSize);
|
||||
|
||||
if (sizeToTrim == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long trimmed = 0;
|
||||
|
||||
final List<File> sorted = Arrays.asList(mCacheBase.listFiles(mCacheFileFilter));
|
||||
Collections.sort(sorted, mLastModifiedOldestFirstComparator);
|
||||
|
||||
// first clear out any files that aren't in the queue
|
||||
for (final File cacheFile : sorted) {
|
||||
if (mQueue.contains(cacheFile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final long size = cacheFile.length();
|
||||
if (clear(cacheFile)) {
|
||||
trimmed += size;
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "trimmed unqueued " + cacheFile.getName() + " from cache.");
|
||||
}
|
||||
}
|
||||
|
||||
if (trimmed >= sizeToTrim) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (trimmed < sizeToTrim && !mQueue.isEmpty()) {
|
||||
final File cacheFile = mQueue.poll();
|
||||
|
||||
// shouldn't happen due to the check above, but just in case...
|
||||
if (cacheFile == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
final long size = cacheFile.length();
|
||||
|
||||
if (clear(cacheFile)) {
|
||||
trimmed += size;
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "trimmed " + cacheFile.getName() + " from cache.");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "error deleting " + cacheFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "trimmed a total of " + trimmed + " bytes from cache.");
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this to do the actual disk writing. Do not close the OutputStream; it will be
|
||||
* closed for you.
|
||||
*
|
||||
* @param key
|
||||
* @param in
|
||||
* @param out
|
||||
*/
|
||||
protected abstract void toDisk(K key, V in, OutputStream out);
|
||||
|
||||
/**
|
||||
* Implement this to do the actual disk reading.
|
||||
*
|
||||
* @param key
|
||||
* @param in
|
||||
* @return a new instance of {@link V} containing the contents of in.
|
||||
*/
|
||||
protected abstract V fromDisk(K key, InputStream in);
|
||||
|
||||
/**
|
||||
* Using the key's {@link Object#toString() toString()} method, generates a string suitable for
|
||||
* using as a filename.
|
||||
*
|
||||
* @param key
|
||||
* @return a string uniquely representing the the key.
|
||||
*/
|
||||
public String hash(K key) {
|
||||
final byte[] ba;
|
||||
|
||||
// MessageDigest isn't threadsafe, so we need to ensure it doesn't tread on itself.
|
||||
synchronized (hash) {
|
||||
hash.update(key.toString().getBytes());
|
||||
ba = hash.digest();
|
||||
}
|
||||
final BigInteger bi = new BigInteger(1, ba);
|
||||
final String result = bi.toString(16);
|
||||
if (result.length() % 2 != 0) {
|
||||
return "0" + result;
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package edu.mit.mobile.android.imagecache;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2012 MIT Mobile Experience Lab
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v4.util.LruCache;
|
||||
|
||||
public class DrawableMemCache<T> extends LruCache<T, Drawable> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = DrawableMemCache.class.getSimpleName();
|
||||
|
||||
public DrawableMemCache(int maxSize) {
|
||||
super(maxSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int sizeOf(T key, Drawable value) {
|
||||
int size = 0;
|
||||
if (value instanceof BitmapDrawable) {
|
||||
final Bitmap b = ((BitmapDrawable) value).getBitmap();
|
||||
if (b != null) {
|
||||
size = b.getRowBytes() * b.getHeight();
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
@ -0,0 +1,842 @@
|
||||
package edu.mit.mobile.android.imagecache;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2012 MIT Mobile Experience Lab
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.HttpResponseException;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.params.ClientPNames;
|
||||
import org.apache.http.conn.scheme.PlainSocketFactory;
|
||||
import org.apache.http.conn.scheme.Scheme;
|
||||
import org.apache.http.conn.scheme.SchemeRegistry;
|
||||
import org.apache.http.conn.ssl.SSLSocketFactory;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
|
||||
import org.apache.http.params.CoreConnectionPNames;
|
||||
import org.apache.http.params.HttpParams;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.ImageView;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* An image download-and-cacher that also knows how to efficiently generate thumbnails of various
|
||||
* sizes.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The cache is shared with the entire process, so make sure you
|
||||
* {@link #registerOnImageLoadListener(OnImageLoadListener)} and
|
||||
* {@link #unregisterOnImageLoadListener(OnImageLoadListener)} any load listeners in your
|
||||
* activities.
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="mailto:spomeroy@mit.edu">Steve Pomeroy</a>
|
||||
*
|
||||
*/
|
||||
public class ImageCache extends DiskCache<String, Bitmap> {
|
||||
private static final String TAG = ImageCache.class.getSimpleName();
|
||||
|
||||
static final boolean DEBUG = false;
|
||||
|
||||
// whether to use Apache HttpClient or URL.openConnection()
|
||||
private static final boolean USE_APACHE_NC = true;
|
||||
|
||||
// the below settings are copied from AsyncTask.java
|
||||
private static final int CORE_POOL_SIZE = 5; // thread
|
||||
private static final int MAXIMUM_POOL_SIZE = 128; // thread
|
||||
private static final int KEEP_ALIVE_TIME = 1; // second
|
||||
|
||||
private final HashSet<OnImageLoadListener> mImageLoadListeners = new HashSet<ImageCache.OnImageLoadListener>();
|
||||
|
||||
public static final int DEFAULT_CACHE_SIZE = (24 /* MiB */* 1024 * 1024); // in bytes
|
||||
|
||||
private DrawableMemCache<String> mMemCache = new DrawableMemCache<String>(DEFAULT_CACHE_SIZE);
|
||||
|
||||
private Integer mIDCounter = 0;
|
||||
|
||||
private static ImageCache mInstance;
|
||||
|
||||
// this is a custom Executor, as we want to have the tasks loaded in FILO order. FILO works
|
||||
// particularly well when scrolling with a ListView.
|
||||
private final ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
|
||||
MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
|
||||
new PriorityBlockingQueue<Runnable>());
|
||||
|
||||
// ignored as SparseArray isn't thread-safe
|
||||
@SuppressLint("UseSparseArrays")
|
||||
private final Map<Integer, Runnable> jobs = Collections
|
||||
.synchronizedMap(new HashMap<Integer, Runnable>());
|
||||
|
||||
private final HttpClient hc;
|
||||
|
||||
private CompressFormat mCompressFormat;
|
||||
private int mQuality;
|
||||
|
||||
private final Resources mRes;
|
||||
|
||||
private static final int MSG_IMAGE_LOADED = 100;
|
||||
|
||||
private final KeyedLock<String> mDownloading = new KeyedLock<String>();
|
||||
|
||||
private static class ImageLoadHandler extends Handler {
|
||||
private final ImageCache mCache;
|
||||
|
||||
public ImageLoadHandler(ImageCache cache) {
|
||||
super();
|
||||
mCache = cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_IMAGE_LOADED:
|
||||
mCache.notifyListeners((LoadResult) msg.obj);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private final ImageLoadHandler mHandler = new ImageLoadHandler(this);
|
||||
|
||||
// TODO make it so this is customizable on the instance level.
|
||||
/**
|
||||
* Gets an instance of the cache.
|
||||
*
|
||||
* @param context
|
||||
* @return an instance of the cache
|
||||
*/
|
||||
public static ImageCache getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new ImageCache(context, CompressFormat.JPEG, 85);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generally, it's best to use the shared image cache using {@link #getInstance(Context)}. Use
|
||||
* this if you want to customize a cache or keep it separate.
|
||||
*
|
||||
* @param context
|
||||
* @param format
|
||||
* @param quality
|
||||
*/
|
||||
public ImageCache(Context context, CompressFormat format, int quality) {
|
||||
super(context.getCacheDir(), null, getExtension(format));
|
||||
if (USE_APACHE_NC) {
|
||||
hc = getHttpClient();
|
||||
} else {
|
||||
hc = null;
|
||||
}
|
||||
|
||||
mRes = context.getResources();
|
||||
|
||||
mCompressFormat = format;
|
||||
mQuality = quality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the compression format for resized images.
|
||||
*
|
||||
* @param format
|
||||
*/
|
||||
public void setCompressFormat(CompressFormat format) {
|
||||
mCompressFormat = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the image quality. Hint to the compressor, 0-100. 0 meaning compress for small size, 100
|
||||
* meaning compress for max quality. Some formats, like PNG which is lossless, will ignore the
|
||||
* quality setting
|
||||
*
|
||||
* @param quality
|
||||
*/
|
||||
public void setQuality(int quality) {
|
||||
mQuality = quality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum size of the memory cache. Note, this will clear the memory cache.
|
||||
*
|
||||
* @param maxSize
|
||||
* the maximum size of the memory cache in bytes.
|
||||
*/
|
||||
public void setMemCacheMaxSize(int maxSize) {
|
||||
mMemCache = new DrawableMemCache<String>(maxSize);
|
||||
}
|
||||
|
||||
private static String getExtension(CompressFormat format) {
|
||||
String extension;
|
||||
switch (format) {
|
||||
case JPEG:
|
||||
extension = ".jpg";
|
||||
break;
|
||||
|
||||
case PNG:
|
||||
extension = ".png";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* If loading a number of images where you don't have a unique ID to represent the individual
|
||||
* load, this can be used to generate a sequential ID.
|
||||
*
|
||||
* @return a new unique ID
|
||||
*/
|
||||
public int getNewID() {
|
||||
synchronized (mIDCounter) {
|
||||
return mIDCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap fromDisk(String key, InputStream in) {
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "disk cache hit for key " + key);
|
||||
}
|
||||
try {
|
||||
final Bitmap image = BitmapFactory.decodeStream(in);
|
||||
return image;
|
||||
|
||||
} catch (final OutOfMemoryError oom) {
|
||||
oomClear();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void toDisk(String key, Bitmap image, OutputStream out) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "disk cache write for key " + key);
|
||||
}
|
||||
if (image != null) {
|
||||
if (!image.compress(mCompressFormat, mQuality, out)) {
|
||||
Log.e(TAG, "error writing compressed image to disk for key " + key);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Ignoring attempt to write null image to disk cache");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of AndroidHttpClient if the devices has it (it was introduced in 2.2), or
|
||||
* falls back on a http client that should work reasonably well.
|
||||
*
|
||||
* @return a working instance of an HttpClient
|
||||
*/
|
||||
private HttpClient getHttpClient() {
|
||||
HttpClient ahc;
|
||||
try {
|
||||
final Class<?> ahcClass = Class.forName("android.net.http.AndroidHttpClient");
|
||||
final Method newInstance = ahcClass.getMethod("newInstance", String.class);
|
||||
ahc = (HttpClient) newInstance.invoke(null, "ImageCache");
|
||||
|
||||
} catch (final ClassNotFoundException e) {
|
||||
DefaultHttpClient dhc = new DefaultHttpClient();
|
||||
final HttpParams params = dhc.getParams();
|
||||
dhc = null;
|
||||
|
||||
params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 20 * 1000);
|
||||
|
||||
final SchemeRegistry registry = new SchemeRegistry();
|
||||
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
||||
registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
|
||||
|
||||
final ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(params,
|
||||
registry);
|
||||
ahc = new DefaultHttpClient(manager, params);
|
||||
|
||||
} catch (final NoSuchMethodException e) {
|
||||
|
||||
final RuntimeException re = new RuntimeException("Programming error");
|
||||
re.initCause(e);
|
||||
throw re;
|
||||
|
||||
} catch (final IllegalAccessException e) {
|
||||
final RuntimeException re = new RuntimeException("Programming error");
|
||||
re.initCause(e);
|
||||
throw re;
|
||||
|
||||
} catch (final InvocationTargetException e) {
|
||||
final RuntimeException re = new RuntimeException("Programming error");
|
||||
re.initCause(e);
|
||||
throw re;
|
||||
}
|
||||
return ahc;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Registers an {@link OnImageLoadListener} with the cache. When an image is loaded
|
||||
* asynchronously either directly by way of {@link #scheduleLoadImage(int, Uri, int, int)} or
|
||||
* indirectly by {@link #loadImage(int, Uri, int, int)}, any registered listeners will get
|
||||
* called.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This should probably be called from {@link Activity#onResume()}.
|
||||
* </p>
|
||||
*
|
||||
* @param onImageLoadListener
|
||||
*/
|
||||
public void registerOnImageLoadListener(OnImageLoadListener onImageLoadListener) {
|
||||
mImageLoadListeners.add(onImageLoadListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Unregisters the listener with the cache. This will not cancel any pending load requests.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This should probably be called from {@link Activity#onPause()}.
|
||||
* </p>
|
||||
*
|
||||
* @param onImageLoadListener
|
||||
*/
|
||||
public void unregisterOnImageLoadListener(OnImageLoadListener onImageLoadListener) {
|
||||
mImageLoadListeners.remove(onImageLoadListener);
|
||||
}
|
||||
|
||||
private class LoadResult {
|
||||
public LoadResult(int id, Uri image, Drawable drawable) {
|
||||
this.id = id;
|
||||
this.drawable = drawable;
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
final Uri image;
|
||||
final int id;
|
||||
final Drawable drawable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri
|
||||
* the image uri
|
||||
* @return a key unique to the given uri
|
||||
*/
|
||||
public String getKey(Uri uri) {
|
||||
return uri.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given key as a drawable, retrieving it from memory cache if it's present.
|
||||
*
|
||||
* @param key
|
||||
* a key generated by {@link #getKey(Uri)} or {@link #getKey(Uri, int, int)}
|
||||
* @return the drawable if it's in the memory cache or null.
|
||||
*/
|
||||
public Drawable getDrawable(String key) {
|
||||
final Drawable img = mMemCache.get(key);
|
||||
if (img != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "mem cache hit for key " + key);
|
||||
}
|
||||
touchKey(key);
|
||||
return img;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a drawable into memory cache.
|
||||
*
|
||||
* @param key
|
||||
* a key generated by {@link #getKey(Uri)} or {@link #getKey(Uri, int, int)}
|
||||
* @param drawable
|
||||
*/
|
||||
public void putDrawable(String key, Drawable drawable) {
|
||||
mMemCache.put(key, drawable);
|
||||
}
|
||||
|
||||
/**
|
||||
* A blocking call to get an image. If it's in the cache, it'll return the drawable immediately.
|
||||
* Otherwise it will download, scale, and cache the image before returning it. For non-blocking
|
||||
* use, see {@link #loadImage(int, Uri, int, int)}
|
||||
*
|
||||
* @param uri
|
||||
* @param width
|
||||
* @param height
|
||||
* @return
|
||||
* @throws ClientProtocolException
|
||||
* @throws IOException
|
||||
* @throws ImageCacheException
|
||||
*/
|
||||
public Drawable getImage(Uri uri, int width, int height) throws ClientProtocolException,
|
||||
IOException, ImageCacheException {
|
||||
|
||||
final String scaledKey = getKey(uri, width, height);
|
||||
|
||||
mDownloading.lock(scaledKey);
|
||||
|
||||
try {
|
||||
Drawable d = getDrawable(scaledKey);
|
||||
if (d != null) {
|
||||
return d;
|
||||
}
|
||||
|
||||
Bitmap bmp = get(scaledKey);
|
||||
|
||||
if (bmp == null) {
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
bmp = scaleLocalImage(new File(uri.getPath()), width, height);
|
||||
} else {
|
||||
final String sourceKey = getKey(uri);
|
||||
|
||||
mDownloading.lock(sourceKey);
|
||||
|
||||
try {
|
||||
if (!contains(sourceKey)) {
|
||||
downloadImage(sourceKey, uri);
|
||||
}
|
||||
} finally {
|
||||
mDownloading.unlock(sourceKey);
|
||||
}
|
||||
|
||||
bmp = scaleLocalImage(getFile(sourceKey), width, height);
|
||||
if (bmp == null) {
|
||||
clear(sourceKey);
|
||||
}
|
||||
}
|
||||
put(scaledKey, bmp);
|
||||
|
||||
}
|
||||
if (bmp == null) {
|
||||
throw new ImageCacheException("got null bitmap from request to scale");
|
||||
|
||||
}
|
||||
d = new BitmapDrawable(mRes, bmp);
|
||||
putDrawable(scaledKey, d);
|
||||
|
||||
return d;
|
||||
|
||||
} finally {
|
||||
mDownloading.unlock(scaledKey);
|
||||
}
|
||||
}
|
||||
|
||||
private final SparseArray<String> mKeyCache = new SparseArray<String>();
|
||||
|
||||
/**
|
||||
* Returns an opaque cache key representing the given uri, width and height.
|
||||
*
|
||||
* @param uri
|
||||
* an image uri
|
||||
* @param width
|
||||
* the desired image max width
|
||||
* @param height
|
||||
* the desired image max height
|
||||
* @return a cache key unique to the given parameters
|
||||
*/
|
||||
public String getKey(Uri uri, int width, int height) {
|
||||
// collisions are possible, but unlikely.
|
||||
final int hashId = uri.hashCode() + width + height * 10000;
|
||||
|
||||
String key = mKeyCache.get(hashId);
|
||||
if (key == null) {
|
||||
key = uri.buildUpon().appendQueryParameter("width", String.valueOf(width))
|
||||
.appendQueryParameter("height", String.valueOf(height)).build().toString();
|
||||
mKeyCache.put(hashId, key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean clear() {
|
||||
final boolean success = super.clear();
|
||||
|
||||
mMemCache.evictAll();
|
||||
|
||||
mKeyCache.clear();
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean clear(String key) {
|
||||
final boolean success = super.clear(key);
|
||||
|
||||
mMemCache.remove(key);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private class ImageLoadTask implements Runnable, Comparable<ImageLoadTask> {
|
||||
private final int id;
|
||||
private final Uri uri;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final long when = System.nanoTime();
|
||||
|
||||
public ImageLoadTask(int id, Uri image, int width, int height) {
|
||||
this.id = id;
|
||||
this.uri = image;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "ImageLoadTask.doInBackground(" + id + ", " + uri + ", " + width + ", "
|
||||
+ height + ")");
|
||||
}
|
||||
|
||||
try {
|
||||
final LoadResult result = new LoadResult(id, uri, getImage(uri, width, height));
|
||||
synchronized (jobs) {
|
||||
if (jobs.containsKey(id)) {
|
||||
// Job still valid.
|
||||
jobs.remove(id);
|
||||
mHandler.obtainMessage(MSG_IMAGE_LOADED, result).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this exception came about, no idea why:
|
||||
// java.lang.IllegalArgumentException: Parser may not be null
|
||||
} catch (final IllegalArgumentException e) {
|
||||
Log.e(TAG, e.getLocalizedMessage(), e);
|
||||
} catch (final OutOfMemoryError oom) {
|
||||
oomClear();
|
||||
} catch (final ClientProtocolException e) {
|
||||
Log.e(TAG, e.getLocalizedMessage(), e);
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, e.getLocalizedMessage(), e);
|
||||
} catch (final ImageCacheException e) {
|
||||
Log.e(TAG, e.getLocalizedMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ImageLoadTask another) {
|
||||
return Long.valueOf(another.when).compareTo(when);
|
||||
};
|
||||
}
|
||||
|
||||
private void oomClear() {
|
||||
Log.w(TAG, "out of memory, clearing mem cache");
|
||||
mMemCache.evictAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the cache for an image matching the given criteria and returns it. If it isn't
|
||||
* immediately available, calls {@link #scheduleLoadImage}.
|
||||
*
|
||||
* @param id
|
||||
* An ID to keep track of image load requests. For one-off loads, this can just be
|
||||
* the ID of the {@link ImageView}. Otherwise, an unique ID can be acquired using
|
||||
* {@link #getNewID()}.
|
||||
*
|
||||
* @param image
|
||||
* the image to be loaded. Can be a local file or a network resource.
|
||||
* @param width
|
||||
* the maximum width of the resulting image
|
||||
* @param height
|
||||
* the maximum height of the resulting image
|
||||
* @return the cached bitmap if it's available immediately or null if it needs to be loaded
|
||||
* asynchronously.
|
||||
*/
|
||||
public Drawable loadImage(int id, Uri image, int width, int height) throws IOException {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "loadImage(" + id + ", " + image + ", " + width + ", " + height + ")");
|
||||
}
|
||||
final Drawable res = getDrawable(getKey(image, width, height));
|
||||
if (res == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG,
|
||||
"Image not found in memory cache. Scheduling load from network / disk...");
|
||||
}
|
||||
scheduleLoadImage(id, image, width, height);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated to make IDs ints instead of longs. See {@link #loadImage(int, Uri, int, int)}.
|
||||
*
|
||||
* @param id
|
||||
* @param image
|
||||
* @param width
|
||||
* @param height
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
@Deprecated
|
||||
public Drawable loadImage(long id, Uri image, int width, int height) throws IOException {
|
||||
return loadImage(id, image, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a load of the given image. When the image has finished loading and scaling, all
|
||||
* registered {@link OnImageLoadListener}s will be called.
|
||||
*
|
||||
* @param id
|
||||
* An ID to keep track of image load requests. For one-off loads, this can just be
|
||||
* the ID of the {@link ImageView}. Otherwise, an unique ID can be acquired using
|
||||
* {@link #getNewID()}.
|
||||
*
|
||||
* @param image
|
||||
* the image to be loaded. Can be a local file or a network resource.
|
||||
* @param width
|
||||
* the maximum width of the resulting image
|
||||
* @param height
|
||||
* the maximum height of the resulting image
|
||||
*/
|
||||
public void scheduleLoadImage(int id, Uri image, int width, int height) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "executing new ImageLoadTask in background...");
|
||||
}
|
||||
final ImageLoadTask imt = new ImageLoadTask(id, image, width, height);
|
||||
|
||||
jobs.put(id, imt);
|
||||
mExecutor.execute(imt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated in favour of {@link #scheduleLoadImage(int, Uri, int, int)}.
|
||||
*
|
||||
* @param id
|
||||
* @param image
|
||||
* @param width
|
||||
* @param height
|
||||
*/
|
||||
@Deprecated
|
||||
public void scheduleLoadImage(long id, Uri image, int width, int height) {
|
||||
scheduleLoadImage(id, image, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all the asynchronous image loads. Note: currently does not function properly.
|
||||
*
|
||||
*/
|
||||
public void cancelLoads() {
|
||||
jobs.clear();
|
||||
mExecutor.getQueue().clear();
|
||||
}
|
||||
|
||||
public void cancel(int id) {
|
||||
synchronized (jobs) {
|
||||
final Runnable job = jobs.get(id);
|
||||
if (job != null) {
|
||||
jobs.remove(id);
|
||||
mExecutor.remove(job);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removed load id " + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated in favour of {@link #cancel(int)}.
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
@Deprecated
|
||||
public void cancel(long id) {
|
||||
cancel(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocking call to scale a local file. Scales using preserving aspect ratio
|
||||
*
|
||||
* @param localFile
|
||||
* local image file to be scaled
|
||||
* @param width
|
||||
* maximum width
|
||||
* @param height
|
||||
* maximum height
|
||||
* @return the scaled image
|
||||
* @throws ClientProtocolException
|
||||
* @throws IOException
|
||||
*/
|
||||
private static Bitmap scaleLocalImage(File localFile, int width, int height)
|
||||
throws ClientProtocolException, IOException {
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "scaleLocalImage(" + localFile + ", " + width + ", " + height + ")");
|
||||
}
|
||||
|
||||
if (!localFile.exists()) {
|
||||
throw new IOException("local file does not exist: " + localFile);
|
||||
}
|
||||
if (!localFile.canRead()) {
|
||||
throw new IOException("cannot read from local file: " + localFile);
|
||||
}
|
||||
|
||||
// the below borrowed from:
|
||||
// https://github.com/thest1/LazyList/blob/master/src/com/fedorvlasov/lazylist/ImageLoader.java
|
||||
|
||||
// decode image size
|
||||
final BitmapFactory.Options o = new BitmapFactory.Options();
|
||||
o.inJustDecodeBounds = true;
|
||||
|
||||
BitmapFactory.decodeStream(new FileInputStream(localFile), null, o);
|
||||
|
||||
// Find the correct scale value. It should be the power of 2.
|
||||
//final int REQUIRED_WIDTH = width, REQUIRED_HEIGHT = height;
|
||||
int width_tmp = o.outWidth, height_tmp = o.outHeight;
|
||||
int scale = 1;
|
||||
while (true) {
|
||||
if (width_tmp / 2 <= width || height_tmp / 2 <= height) {
|
||||
break;
|
||||
}
|
||||
width_tmp /= 2;
|
||||
height_tmp /= 2;
|
||||
scale *= 2;
|
||||
}
|
||||
|
||||
// decode with inSampleSize
|
||||
final BitmapFactory.Options o2 = new BitmapFactory.Options();
|
||||
o2.inSampleSize = scale;
|
||||
final Bitmap prescale = BitmapFactory
|
||||
.decodeStream(new FileInputStream(localFile), null, o2);
|
||||
|
||||
if (prescale == null) {
|
||||
Log.e(TAG, localFile + " could not be decoded");
|
||||
} else if (DEBUG) {
|
||||
Log.d(TAG, "Successfully completed scaling of " + localFile + " to " + width + "x"
|
||||
+ height);
|
||||
}
|
||||
|
||||
return prescale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocking call to download an image. The image is placed directly into the disk cache at the
|
||||
* given key.
|
||||
*
|
||||
* @param uri
|
||||
* the location of the image
|
||||
* @return a decoded bitmap
|
||||
* @throws ClientProtocolException
|
||||
* if the HTTP response code wasn't 200 or any other HTTP errors
|
||||
* @throws IOException
|
||||
*/
|
||||
protected void downloadImage(String key, Uri uri) throws ClientProtocolException, IOException {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "downloadImage(" + key + ", " + uri + ")");
|
||||
}
|
||||
if (USE_APACHE_NC) {
|
||||
final HttpGet get = new HttpGet(uri.toString());
|
||||
final HttpParams params = get.getParams();
|
||||
params.setParameter(ClientPNames.HANDLE_REDIRECTS, true);
|
||||
|
||||
final HttpResponse hr = hc.execute(get);
|
||||
final StatusLine hs = hr.getStatusLine();
|
||||
if (hs.getStatusCode() != 200) {
|
||||
throw new HttpResponseException(hs.getStatusCode(), hs.getReasonPhrase());
|
||||
}
|
||||
|
||||
final HttpEntity ent = hr.getEntity();
|
||||
|
||||
// TODO I think this means that the source file must be a jpeg. fix this.
|
||||
try {
|
||||
|
||||
putRaw(key, ent.getContent());
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "source file of " + uri + " saved to disk cache at location "
|
||||
+ getFile(key).getAbsolutePath());
|
||||
}
|
||||
} finally {
|
||||
ent.consumeContent();
|
||||
}
|
||||
} else {
|
||||
final URLConnection con = new URL(uri.toString()).openConnection();
|
||||
putRaw(key, con.getInputStream());
|
||||
if (DEBUG) {
|
||||
Log.d(TAG,
|
||||
"source file of " + uri + " saved to disk cache at location "
|
||||
+ getFile(key).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void notifyListeners(LoadResult result) {
|
||||
for (final OnImageLoadListener listener : mImageLoadListeners) {
|
||||
listener.onImageLoaded(result.id, result.image, result.drawable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this and register it using
|
||||
* {@link ImageCache#registerOnImageLoadListener(OnImageLoadListener)} to be notified when
|
||||
* asynchronous image loads have completed.
|
||||
*
|
||||
* @author <a href="mailto:spomeroy@mit.edu">Steve Pomeroy</a>
|
||||
*
|
||||
*/
|
||||
public interface OnImageLoadListener {
|
||||
/**
|
||||
* Called when the image has been loaded and scaled.
|
||||
*
|
||||
* @param id
|
||||
* the ID provided by {@link ImageCache#loadImage(int, Uri, int, int)} or
|
||||
* {@link ImageCache#scheduleLoadImage(int, Uri, int, int)}
|
||||
* @param imageUri
|
||||
* the uri of the image that was originally requested
|
||||
* @param image
|
||||
* the loaded and scaled image
|
||||
*/
|
||||
public void onImageLoaded(int id, Uri imageUri, Drawable image);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package edu.mit.mobile.android.imagecache;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2012 MIT Mobile Experience Lab
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
public class ImageCacheException extends Exception {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 997874306474290980L;
|
||||
|
||||
public ImageCacheException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package edu.mit.mobile.android.imagecache;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A synchronization lock that creates a separate lock for each key.
|
||||
*
|
||||
* @author <a href="mailto:spomeroy@mit.edu">Steve Pomeroy</a>
|
||||
*
|
||||
* @param <K>
|
||||
*/
|
||||
public class KeyedLock<K> {
|
||||
private static final String TAG = KeyedLock.class.getSimpleName();
|
||||
|
||||
private final Map<K, ReentrantLock> mLocks = new HashMap<K, ReentrantLock>();
|
||||
|
||||
private static boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* @param key
|
||||
*/
|
||||
public void lock(K key) {
|
||||
if (DEBUG) {
|
||||
log("acquiring lock for key " + key);
|
||||
}
|
||||
|
||||
ReentrantLock lock;
|
||||
synchronized (mLocks) {
|
||||
lock = mLocks.get(key);
|
||||
if (lock == null) {
|
||||
lock = new ReentrantLock();
|
||||
mLocks.put(key, lock);
|
||||
if (DEBUG) {
|
||||
log(lock + " created new lock and added it to map");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key
|
||||
*/
|
||||
public void unlock(K key) {
|
||||
if (DEBUG) {
|
||||
log("unlocking lock for key " + key);
|
||||
}
|
||||
ReentrantLock lock;
|
||||
|
||||
synchronized (mLocks) {
|
||||
lock = mLocks.get(key);
|
||||
if (lock == null) {
|
||||
Log.e(TAG, "Attempting to unlock lock for key " + key + " which has no entry");
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
log(lock + " has queued threads " + lock.hasQueuedThreads() + " for key " + key);
|
||||
}
|
||||
// maybe entries should be removed when there are no queued threads. This would
|
||||
// occasionally fail...
|
||||
// final boolean queued = lock.hasQueuedThreads();
|
||||
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void log(String message) {
|
||||
|
||||
Log.d(TAG, Thread.currentThread().getId() + "\t" + message);
|
||||
|
||||
}
|
||||
}
|
@ -1,285 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.helper;
|
||||
/*
|
||||
* Copyright (C) 2011 MIT Mobile Experience Lab
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A simple disk cache.
|
||||
*
|
||||
* @author <a href="mailto:spomeroy@mit.edu">Steve Pomeroy</a>
|
||||
*
|
||||
* @param <K> the key to store/retrieve the value
|
||||
* @param <V> the value that will be stored to disk
|
||||
*/
|
||||
// TODO add automatic cache cleanup so low disk conditions can be met
|
||||
@SuppressWarnings("nls")
|
||||
public abstract class DiskCache<K, V> {
|
||||
private static final String TAG = "DiskCache";
|
||||
|
||||
private MessageDigest hash;
|
||||
|
||||
private final File mCacheBase;
|
||||
private final String mCachePrefix, mCacheSuffix;
|
||||
|
||||
/**
|
||||
* Creates a new disk cache with no cachePrefix or cacheSuffix
|
||||
*
|
||||
* @param cacheBase
|
||||
*/
|
||||
public DiskCache(File cacheBase) {
|
||||
this(cacheBase, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new disk cache.
|
||||
*
|
||||
* @param cacheBase The base directory within which all the cache files will be stored.
|
||||
* @param cachePrefix If you want a prefix to the filenames, place one here. Otherwise, pass null.
|
||||
* @param cacheSuffix A suffix to the cache filename. Null is also ok here.
|
||||
*/
|
||||
public DiskCache(File cacheBase, String cachePrefix, String cacheSuffix) {
|
||||
mCacheBase = cacheBase;
|
||||
mCachePrefix = cachePrefix;
|
||||
mCacheSuffix = cacheSuffix;
|
||||
|
||||
try {
|
||||
hash = MessageDigest.getInstance("SHA-1");
|
||||
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
try {
|
||||
hash = MessageDigest.getInstance("MD5");
|
||||
} catch (final NoSuchAlgorithmException e2) {
|
||||
final RuntimeException re = new RuntimeException("No available hashing algorithm");
|
||||
re.initCause(e2);
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache filename for the given key.
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
protected File getFile(K key){
|
||||
return new File(mCacheBase,
|
||||
(mCachePrefix != null ? mCachePrefix :"" )
|
||||
+ hash(key)
|
||||
+ (mCacheSuffix != null ? mCacheSuffix : "")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the value stored in the cache to disk by calling {@link #toDisk(Object, Object, OutputStream)}.
|
||||
*
|
||||
* @param key The key to find the value.
|
||||
* @param value the data to be written to disk.
|
||||
*/
|
||||
public void put(K key, V value) throws IOException, FileNotFoundException {
|
||||
final File saveHere = getFile(key);
|
||||
|
||||
final OutputStream os = new FileOutputStream(saveHere);
|
||||
toDisk(key, value, os);
|
||||
os.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the contents of the InputStream straight to disk. It is the
|
||||
* caller's responsibility to ensure it's the same type as what would be
|
||||
* written with {@link #toDisk(Object, Object, OutputStream)}
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
* @throws IOException
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public void putRaw(K key, InputStream value) throws IOException, FileNotFoundException {
|
||||
final File saveHere = getFile(key);
|
||||
|
||||
final OutputStream os = new FileOutputStream(saveHere);
|
||||
|
||||
inputStreamToOutputStream(value, os);
|
||||
os.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads from an inputstream, dumps to an outputstream
|
||||
* @param is
|
||||
* @param os
|
||||
* @throws IOException
|
||||
*/
|
||||
static public void inputStreamToOutputStream(InputStream is, OutputStream os) throws IOException {
|
||||
final int bufsize = 8196 * 10;
|
||||
final byte[] cbuf = new byte[bufsize];
|
||||
|
||||
for (int readBytes = is.read(cbuf, 0, bufsize);
|
||||
readBytes > 0;
|
||||
readBytes = is.read(cbuf, 0, bufsize)) {
|
||||
os.write(cbuf, 0, readBytes);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean move(K from, K to) {
|
||||
final File moveFrom = getFile(from);
|
||||
final File moveTo = getFile(to);
|
||||
moveFrom.renameTo(moveTo);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads the value from disk using {@link #fromDisk(Object, InputStream)}.
|
||||
*
|
||||
* @param key
|
||||
* @return The value for key or null if the key doesn't map to any existing entries.
|
||||
*/
|
||||
public V get(K key) throws IOException {
|
||||
final File readFrom = getFile(key);
|
||||
|
||||
if (!readFrom.exists()){
|
||||
return null;
|
||||
}
|
||||
|
||||
final InputStream is = new FileInputStream(readFrom);
|
||||
final V out = fromDisk(key, is);
|
||||
is.close();
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the disk cache for a given key.
|
||||
*
|
||||
* @param key
|
||||
* @return true if the disk cache contains the given key
|
||||
*/
|
||||
public boolean contains(K key) {
|
||||
final File readFrom = getFile(key);
|
||||
|
||||
return readFrom.exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the item from the disk cache.
|
||||
*
|
||||
* @param key
|
||||
* @return true if the cached item has been removed or was already removed, false if it was not able to be removed.
|
||||
*/
|
||||
public boolean clear(K key){
|
||||
final File readFrom = getFile(key);
|
||||
|
||||
if (!readFrom.exists()){
|
||||
return true;
|
||||
}
|
||||
|
||||
return readFrom.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache files from disk.
|
||||
*
|
||||
* Note: this only clears files that match the given prefix/suffix.
|
||||
*
|
||||
* @return true if the operation succeeded without error. It is possible that it will fail and the cache ends up being partially cleared.
|
||||
*/
|
||||
public boolean clear() {
|
||||
boolean success = true;
|
||||
|
||||
for (final File cacheFile : mCacheBase.listFiles(mCacheFileFilter)){
|
||||
if (!cacheFile.delete()){
|
||||
// throw new IOException("cannot delete cache file");
|
||||
Log.e(TAG, "error deleting "+ cacheFile);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the size of the cache as it is on disk.
|
||||
*/
|
||||
public int getCacheSize(){
|
||||
return mCacheBase.listFiles(mCacheFileFilter).length;
|
||||
}
|
||||
|
||||
private final CacheFileFilter mCacheFileFilter = new CacheFileFilter();
|
||||
|
||||
private class CacheFileFilter implements FileFilter {
|
||||
@Override
|
||||
public boolean accept(File pathname) {
|
||||
final String path = pathname.getName();
|
||||
return (mCachePrefix != null ? path.startsWith(mCachePrefix) : true)
|
||||
&& (mCacheSuffix != null ? path.endsWith(mCacheSuffix) : true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Implement this to do the actual disk writing. Do not close the OutputStream; it will be closed for you.
|
||||
*
|
||||
* @param key
|
||||
* @param in
|
||||
* @param out
|
||||
*/
|
||||
protected abstract void toDisk(K key, V in, OutputStream out);
|
||||
|
||||
/**
|
||||
* Implement this to do the actual disk reading.
|
||||
* @param key
|
||||
* @param in
|
||||
* @return a new instance of {@link V} containing the contents of in.
|
||||
*/
|
||||
protected abstract V fromDisk(K key, InputStream in);
|
||||
|
||||
/**
|
||||
* Using the key's {@link Object#toString() toString()} method, generates a string suitable for using as a filename.
|
||||
*
|
||||
* @param key
|
||||
* @return a string uniquely representing the the key.
|
||||
*/
|
||||
public String hash(K key){
|
||||
final byte[] ba;
|
||||
synchronized (hash) {
|
||||
hash.update(key.toString().getBytes());
|
||||
ba = hash.digest();
|
||||
}
|
||||
final BigInteger bi = new BigInteger(1, ba);
|
||||
final String result = bi.toString(16);
|
||||
if (result.length() % 2 != 0) {
|
||||
return "0" + result;
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue