Updated ImageDiskCache sources to unmodified LGPL versions

pull/14/head
Sam Bosley 12 years ago
parent e5de043fe7
commit fee4b83a79

@ -9,6 +9,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import org.json.JSONException;
import org.json.JSONObject;
@ -117,6 +118,22 @@ abstract public class RemoteModel extends AbstractModel {
public static final String PICTURES_DIRECTORY = "pictures"; //$NON-NLS-1$
public static String getPictureHash(UserActivity update) {
return String.format("cached::%s%s", update.getValue(UserActivity.TARGET_ID), update.getValue(UserActivity.CREATED_AT));
}
public static String getPictureHash(TagData tagData) {
long tag_date = 0;
if (tagData.containsValue(TagData.CREATION_DATE)) {
tag_date = tagData.getValue(TagData.CREATION_DATE);
}
if (tag_date == 0) {
tag_date = DateUtilities.dateToUnixtime(new Date());
}
return String.format("cached::%s%s", tagData.getValue(TagData.NAME), tag_date);
}
@SuppressWarnings("nls")
public static JSONObject savePictureJson(Context context, Bitmap bitmap) {
try {

@ -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);
}
}

@ -51,9 +51,11 @@ import com.todoroo.astrid.adapter.UpdateAdapter;
import com.todoroo.astrid.dao.UserActivityDao;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.UserActivity;
import com.todoroo.astrid.helper.ImageDiskCache;
import com.todoroo.astrid.helper.AsyncImageView;
import com.todoroo.astrid.service.StatisticsService;
import edu.mit.mobile.android.imagecache.ImageCache;
public abstract class CommentsFragment extends SherlockListFragment {
// private TagData tagData;
@ -72,7 +74,7 @@ public abstract class CommentsFragment extends SherlockListFragment {
protected static final int MENU_REFRESH_ID = Menu.FIRST;
protected final ImageDiskCache imageCache;
protected final ImageCache imageCache;
protected Resources resources;
@ -83,7 +85,7 @@ public abstract class CommentsFragment extends SherlockListFragment {
public CommentsFragment() {
DependencyInjectionService.getInstance().inject(this);
imageCache = ImageDiskCache.getInstance();
imageCache = AsyncImageView.getImageCache();
}
@Override

@ -70,6 +70,8 @@ import com.todoroo.astrid.utility.AstridPreferences;
import com.todoroo.astrid.utility.ResourceDrawableCache;
import com.todoroo.astrid.welcome.HelpInfoPopover;
import edu.mit.mobile.android.imagecache.ImageCache;
public class TagSettingsActivity extends SherlockFragmentActivity {
public static final String TOKEN_NEW_FILTER = "newFilter"; //$NON-NLS-1$
@ -108,14 +110,14 @@ public class TagSettingsActivity extends SherlockFragmentActivity {
private EditText tagDescription;
private CheckBox isSilent;
private Bitmap setBitmap;
private final ImageDiskCache imageCache;
private final ImageCache imageCache;
private boolean isNewTag = false;
private boolean isDialog;
public TagSettingsActivity() {
DependencyInjectionService.getInstance().inject(this);
imageCache = ImageDiskCache.getInstance();
imageCache = AsyncImageView.getImageCache();
}
@Override

@ -74,7 +74,6 @@ import com.todoroo.astrid.data.TaskAttachment;
import com.todoroo.astrid.data.User;
import com.todoroo.astrid.data.UserActivity;
import com.todoroo.astrid.helper.AsyncImageView;
import com.todoroo.astrid.helper.ImageDiskCache;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.StartupService;
import com.todoroo.astrid.service.StatisticsConstants;
@ -83,6 +82,8 @@ import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.timers.TimerActionControlSet.TimerActionListener;
import com.todoroo.astrid.utility.ResourceDrawableCache;
import edu.mit.mobile.android.imagecache.ImageCache;
public class EditNoteActivity extends LinearLayout implements TimerActionListener {
public static final String EXTRA_TASK_ID = "task"; //$NON-NLS-1$
@ -111,7 +112,7 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
private final Resources resources;
private final ImageDiskCache imageCache;
private final ImageCache imageCache;
private final int cameraButton;
private final String linkColor;
@ -127,7 +128,7 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
public EditNoteActivity(Fragment fragment, View parent, long t) {
super(fragment.getActivity());
imageCache = ImageDiskCache.getInstance();
imageCache = AsyncImageView.getImageCache();
this.fragment = fragment;
this.activity = (AstridActivity) fragment.getActivity();

@ -58,7 +58,8 @@ import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.User;
import com.todoroo.astrid.data.UserActivity;
import com.todoroo.astrid.helper.AsyncImageView;
import com.todoroo.astrid.helper.ImageDiskCache;
import edu.mit.mobile.android.imagecache.ImageCache;
/**
* Adapter for displaying a user's activity
@ -73,7 +74,7 @@ public class UpdateAdapter extends CursorAdapter {
protected final Fragment fragment;
private final int resource;
private final LayoutInflater inflater;
private final ImageDiskCache imageCache;
private final ImageCache imageCache;
private final String linkColor;
private final String fromView;
@ -155,7 +156,7 @@ public class UpdateAdapter extends CursorAdapter {
inflater = (LayoutInflater) fragment.getActivity().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
imageCache = ImageDiskCache.getInstance();
imageCache = AsyncImageView.getImageCache();
this.fromView = fromView;
this.resource = resource;
@ -316,7 +317,7 @@ public class UpdateAdapter extends CursorAdapter {
}
public static void setupImagePopupForCommentView(View view, AsyncImageView commentPictureView, final String pictureThumb, final String pictureFull, final Bitmap updateBitmap,
final String message, final Fragment fragment, ImageDiskCache imageCache) {
final String message, final Fragment fragment, ImageCache imageCache) {
if ((!TextUtils.isEmpty(pictureThumb) && !"null".equals(pictureThumb)) || updateBitmap != null) { //$NON-NLS-1$
commentPictureView.setVisibility(View.VISIBLE);
if (updateBitmap != null)

@ -9,9 +9,14 @@ import java.io.IOException;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.text.TextUtils;
import android.util.AttributeSet;
import com.todoroo.andlib.service.ContextManager;
import edu.mit.mobile.android.imagecache.ImageCache;
/**
* Subclass of greendroid.widget.AsyncImageView, so that we can cache the image
* locally when user is offline
@ -21,21 +26,20 @@ import android.util.AttributeSet;
*/
public class AsyncImageView extends greendroid.widget.AsyncImageView {
private final ImageDiskCache imageDiskCache;
private final ImageCache imageDiskCache;
private Bitmap cacheImage;
private String cacheURL = ""; //$NON-NLS-1$
public AsyncImageView(Context context) {
super(context);
imageDiskCache = ImageDiskCache.getInstance();
imageDiskCache = getImageCache();
}
public AsyncImageView(Context context, AttributeSet set) {
super(context, set);
imageDiskCache = ImageDiskCache.getInstance();
imageDiskCache = getImageCache();
}
public AsyncImageView(Context context, AttributeSet set, int defStyle) {
super(context, set, defStyle);
imageDiskCache = ImageDiskCache.getInstance();
imageDiskCache = getImageCache();
}
@Override
public void setUrl(String url) {
@ -74,4 +78,17 @@ public class AsyncImageView extends greendroid.widget.AsyncImageView {
return b;
}
private static volatile ImageCache imageCacheInstance = null;
public static ImageCache getImageCache() {
if (imageCacheInstance == null) {
synchronized(AsyncImageView.class) {
if (imageCacheInstance == null) {
imageCacheInstance = new ImageCache(ContextManager.getContext(), CompressFormat.JPEG, 85);
}
}
}
return imageCacheInstance;
}
}

@ -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…
Cancel
Save