Caching activity images to disk

pull/14/head
Andrew Shaw 13 years ago
parent 3c58db0d99
commit acc6683ea4

@ -265,7 +265,12 @@ public final class ActFmSyncService {
else
result = actFmInvoker.post("comment_add", picture, params.toArray(new Object[params.size()]));
update.setValue(Update.REMOTE_ID, result.optLong("id"));
update.setValue(Update.PICTURE, result.optString("picture"));
// ImageCache imageCache = ImageCache.getInstance(getContext());
//TODO figure out a way to replace local image files with the url
if (TextUtils.isEmpty(update.getValue(Update.PICTURE)) || update.getValue(Update.PICTURE).equals(Update.PICTURE_LOADING)) {
update.setValue(Update.PICTURE, result.optString("picture"));
}
updateDao.saveExisting(update);
} catch (IOException e) {
if (notPermanentError(e))

@ -2,6 +2,7 @@ package com.todoroo.astrid.notes;
import greendroid.widget.AsyncImageView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -21,6 +22,7 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.DateUtils;
import android.text.util.Linkify;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
@ -52,6 +54,7 @@ import com.todoroo.astrid.dao.UpdateDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.Update;
import com.todoroo.astrid.helper.ImageCache;
import com.todoroo.astrid.helper.ProgressBarSyncResultCallback;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.StatisticsConstants;
@ -85,6 +88,7 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
private ImageButton pictureButton;
private Bitmap pendingCommentPicture = null;
private final Fragment fragment;
private final ImageCache imageCache;
private final List<UpdatesChangedListener> listeners = new LinkedList<UpdatesChangedListener>();
@ -96,6 +100,7 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
public EditNoteActivity(Fragment fragment, View parent, long t) {
super(fragment.getActivity());
imageCache = ImageCache.getInstance(fragment.getActivity());
this.fragment = fragment;
DependencyInjectionService.getInstance().inject(this);
@ -157,8 +162,8 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
commentField.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
commentButton.setVisibility((s.length() > 0) ? View.VISIBLE : View.GONE);
timerView.setVisibility((s.length() > 0) ? View.GONE : View.VISIBLE);
commentButton.setVisibility((s.length() > 0 || pendingCommentPicture != null) ? View.VISIBLE : View.GONE);
timerView.setVisibility((s.length() > 0 || pendingCommentPicture != null) ? View.GONE : View.VISIBLE);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
@ -347,7 +352,16 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
commentPictureView.setVisibility(View.GONE);
else {
commentPictureView.setVisibility(View.VISIBLE);
commentPictureView.setUrl(item.commentPicture);
if(imageCache.contains(item.commentPicture)) {
try {
commentPictureView.setDefaultImageBitmap(imageCache.get(item.commentPicture));
} catch (IOException e) {
e.printStackTrace();
}
}
else {
commentPictureView.setUrl(item.commentPicture);
}
}
}
}
@ -398,7 +412,15 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
}
private String getPictureHashForUpdate(Update u) {
String s = u.getValue(Update.TASK) + "" + u.getValue(Update.CREATION_DATE);
return s;
}
private void addComment(String message, String actionCode, boolean usePicture) {
// Allow for users to just add picture
if (TextUtils.isEmpty(message) && usePicture) {
message = " "; //$NON-NLS-1$
}
Update update = new Update();
update.setValue(Update.MESSAGE, message);
update.setValue(Update.ACTION_CODE, actionCode);
@ -408,6 +430,14 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
if (usePicture && pendingCommentPicture != null) {
update.setValue(Update.PICTURE, Update.PICTURE_LOADING);
try {
String updateString = getPictureHashForUpdate(update);
imageCache.put(updateString, pendingCommentPicture);
update.setValue(Update.PICTURE, updateString);
}
catch (Exception e) {
Log.e("EditNoteActivity", "Failed to put image to disk...");
}
}
Flags.set(Flags.ACTFM_SUPPRESS_SYNC);
updateDao.createNew(update);
@ -508,8 +538,8 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
addComment(String.format("%s %s", //$NON-NLS-1$
getContext().getString(R.string.TEA_timer_comment_started),
DateUtilities.getTimeString(getContext(), new Date())),
"task_started", //$NON-NLS-1$
false);
"task_started", //$NON-NLS-1$
false);
}
@Override
@ -536,7 +566,7 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
};
return (ActFmCameraModule.activityResult((Activity)getContext(), requestCode, resultCode, data, callback));
//Handled
//Handled
}
}

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