mirror of https://github.com/tasks/tasks
Caching activity images to disk
parent
3c58db0d99
commit
acc6683ea4
@ -0,0 +1,272 @@
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,249 @@
|
||||
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.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* <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;
|
||||
|
||||
|
||||
private long mIDCounter = 0;
|
||||
|
||||
private static ImageCache mInstance;
|
||||
|
||||
|
||||
private final CompressFormat mCompressFormat;
|
||||
private final int mQuality;
|
||||
|
||||
private final Resources mRes;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
private ImageCache(Context context, CompressFormat format, int quality) {
|
||||
super(context.getCacheDir(), null, getExtension(format));
|
||||
mRes = context.getResources();
|
||||
|
||||
mCompressFormat = format;
|
||||
mQuality = quality;
|
||||
}
|
||||
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 synchronized long getNewID() {
|
||||
return mIDCounter++;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap fromDisk(String key, InputStream in) {
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "disk cache hit");
|
||||
}
|
||||
try {
|
||||
final Bitmap image = BitmapFactory.decodeStream(in);
|
||||
return image;
|
||||
|
||||
} catch (final OutOfMemoryError oom) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void toDisk(String key, Bitmap image, OutputStream out) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "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, "attempting to write null image to cache");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param uri
|
||||
* the image uri
|
||||
* @return a key unique to the given uri
|
||||
*/
|
||||
public String getKey(Uri uri) {
|
||||
return uri.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return uri.buildUpon()
|
||||
.appendQueryParameter("width", String.valueOf(width))
|
||||
.appendQueryParameter("height", String.valueOf(height)).build()
|
||||
.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Cancels all the asynchronous image loads.
|
||||
* Note: currently does not function properly.
|
||||
*
|
||||
*/
|
||||
public void cancelLoads() {
|
||||
// TODO actually make it possible to cancel tasks
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
|
||||
return prescale;
|
||||
}
|
||||
private static final boolean USE_APACHE_NC = true;
|
||||
|
||||
}
|
Loading…
Reference in New Issue