Updated ImageDiskCache sources to unmodified LGPL versions

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

@ -9,6 +9,7 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Date;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; 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 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") @SuppressWarnings("nls")
public static JSONObject savePictureJson(Context context, Bitmap bitmap) { public static JSONObject savePictureJson(Context context, Bitmap bitmap) {
try { 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.dao.UserActivityDao;
import com.todoroo.astrid.data.RemoteModel; import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.UserActivity; 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 com.todoroo.astrid.service.StatisticsService;
import edu.mit.mobile.android.imagecache.ImageCache;
public abstract class CommentsFragment extends SherlockListFragment { public abstract class CommentsFragment extends SherlockListFragment {
// private TagData tagData; // private TagData tagData;
@ -72,7 +74,7 @@ public abstract class CommentsFragment extends SherlockListFragment {
protected static final int MENU_REFRESH_ID = Menu.FIRST; protected static final int MENU_REFRESH_ID = Menu.FIRST;
protected final ImageDiskCache imageCache; protected final ImageCache imageCache;
protected Resources resources; protected Resources resources;
@ -83,7 +85,7 @@ public abstract class CommentsFragment extends SherlockListFragment {
public CommentsFragment() { public CommentsFragment() {
DependencyInjectionService.getInstance().inject(this); DependencyInjectionService.getInstance().inject(this);
imageCache = ImageDiskCache.getInstance(); imageCache = AsyncImageView.getImageCache();
} }
@Override @Override

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

@ -74,7 +74,6 @@ import com.todoroo.astrid.data.TaskAttachment;
import com.todoroo.astrid.data.User; import com.todoroo.astrid.data.User;
import com.todoroo.astrid.data.UserActivity; import com.todoroo.astrid.data.UserActivity;
import com.todoroo.astrid.helper.AsyncImageView; import com.todoroo.astrid.helper.AsyncImageView;
import com.todoroo.astrid.helper.ImageDiskCache;
import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.StartupService; import com.todoroo.astrid.service.StartupService;
import com.todoroo.astrid.service.StatisticsConstants; 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.timers.TimerActionControlSet.TimerActionListener;
import com.todoroo.astrid.utility.ResourceDrawableCache; import com.todoroo.astrid.utility.ResourceDrawableCache;
import edu.mit.mobile.android.imagecache.ImageCache;
public class EditNoteActivity extends LinearLayout implements TimerActionListener { public class EditNoteActivity extends LinearLayout implements TimerActionListener {
public static final String EXTRA_TASK_ID = "task"; //$NON-NLS-1$ 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 Resources resources;
private final ImageDiskCache imageCache; private final ImageCache imageCache;
private final int cameraButton; private final int cameraButton;
private final String linkColor; private final String linkColor;
@ -127,7 +128,7 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
public EditNoteActivity(Fragment fragment, View parent, long t) { public EditNoteActivity(Fragment fragment, View parent, long t) {
super(fragment.getActivity()); super(fragment.getActivity());
imageCache = ImageDiskCache.getInstance(); imageCache = AsyncImageView.getImageCache();
this.fragment = fragment; this.fragment = fragment;
this.activity = (AstridActivity) fragment.getActivity(); 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.User;
import com.todoroo.astrid.data.UserActivity; import com.todoroo.astrid.data.UserActivity;
import com.todoroo.astrid.helper.AsyncImageView; 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 * Adapter for displaying a user's activity
@ -73,7 +74,7 @@ public class UpdateAdapter extends CursorAdapter {
protected final Fragment fragment; protected final Fragment fragment;
private final int resource; private final int resource;
private final LayoutInflater inflater; private final LayoutInflater inflater;
private final ImageDiskCache imageCache; private final ImageCache imageCache;
private final String linkColor; private final String linkColor;
private final String fromView; private final String fromView;
@ -155,7 +156,7 @@ public class UpdateAdapter extends CursorAdapter {
inflater = (LayoutInflater) fragment.getActivity().getSystemService( inflater = (LayoutInflater) fragment.getActivity().getSystemService(
Context.LAYOUT_INFLATER_SERVICE); Context.LAYOUT_INFLATER_SERVICE);
imageCache = ImageDiskCache.getInstance(); imageCache = AsyncImageView.getImageCache();
this.fromView = fromView; this.fromView = fromView;
this.resource = resource; 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, 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$ if ((!TextUtils.isEmpty(pictureThumb) && !"null".equals(pictureThumb)) || updateBitmap != null) { //$NON-NLS-1$
commentPictureView.setVisibility(View.VISIBLE); commentPictureView.setVisibility(View.VISIBLE);
if (updateBitmap != null) if (updateBitmap != null)

@ -9,9 +9,14 @@ import java.io.IOException;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; 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 * Subclass of greendroid.widget.AsyncImageView, so that we can cache the image
* locally when user is offline * locally when user is offline
@ -21,21 +26,20 @@ import android.util.AttributeSet;
*/ */
public class AsyncImageView extends greendroid.widget.AsyncImageView { public class AsyncImageView extends greendroid.widget.AsyncImageView {
private final ImageDiskCache imageDiskCache; private final ImageCache imageDiskCache;
private Bitmap cacheImage; private Bitmap cacheImage;
private String cacheURL = ""; //$NON-NLS-1$ private String cacheURL = ""; //$NON-NLS-1$
public AsyncImageView(Context context) { public AsyncImageView(Context context) {
super(context); super(context);
imageDiskCache = getImageCache();
imageDiskCache = ImageDiskCache.getInstance();
} }
public AsyncImageView(Context context, AttributeSet set) { public AsyncImageView(Context context, AttributeSet set) {
super(context, set); super(context, set);
imageDiskCache = ImageDiskCache.getInstance(); imageDiskCache = getImageCache();
} }
public AsyncImageView(Context context, AttributeSet set, int defStyle) { public AsyncImageView(Context context, AttributeSet set, int defStyle) {
super(context, set, defStyle); super(context, set, defStyle);
imageDiskCache = ImageDiskCache.getInstance(); imageDiskCache = getImageCache();
} }
@Override @Override
public void setUrl(String url) { public void setUrl(String url) {
@ -74,4 +78,17 @@ public class AsyncImageView extends greendroid.widget.AsyncImageView {
return b; 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