Remove AsyncImageView

pull/46/head
Alex Baker 12 years ago
parent fb78537cce
commit 103d1d1118

@ -46,13 +46,10 @@ 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.AsyncImageView;
import org.json.JSONObject; import org.json.JSONObject;
import org.tasks.R; import org.tasks.R;
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,15 +69,12 @@ 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 ImageCache imageCache;
protected Resources resources; protected Resources resources;
@Autowired UserActivityDao userActivityDao; @Autowired UserActivityDao userActivityDao;
public CommentsFragment() { public CommentsFragment() {
DependencyInjectionService.getInstance().inject(this); DependencyInjectionService.getInstance().inject(this);
imageCache = AsyncImageView.getImageCache();
} }
@Override @Override

@ -39,7 +39,6 @@ import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.TagData; import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.TagMetadata; import com.todoroo.astrid.data.TagMetadata;
import com.todoroo.astrid.data.User; import com.todoroo.astrid.data.User;
import com.todoroo.astrid.helper.AsyncImageView;
import com.todoroo.astrid.helper.UUIDHelper; import com.todoroo.astrid.helper.UUIDHelper;
import com.todoroo.astrid.service.TagDataService; import com.todoroo.astrid.service.TagDataService;
import com.todoroo.astrid.service.ThemeService; import com.todoroo.astrid.service.ThemeService;
@ -53,8 +52,6 @@ import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.tasks.R; import org.tasks.R;
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$
@ -82,14 +79,12 @@ public class TagSettingsActivity extends SherlockFragmentActivity {
private EditText tagName; private EditText tagName;
private Bitmap setBitmap; private Bitmap setBitmap;
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 = AsyncImageView.getImageCache();
} }
@Override @Override
@ -269,7 +264,6 @@ public class TagSettingsActivity extends SherlockFragmentActivity {
} }
try { try {
String tagPicture = RemoteModel.PictureHelper.getPictureHash(tagData); String tagPicture = RemoteModel.PictureHelper.getPictureHash(tagData);
imageCache.put(tagPicture, bitmap);
tagData.setValue(TagData.PICTURE, tagPicture); tagData.setValue(TagData.PICTURE, tagPicture);
} }
catch (Exception e) { catch (Exception e) {

@ -23,6 +23,7 @@ import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.LayoutParams;
import android.widget.CursorAdapter; import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property;
@ -39,14 +40,9 @@ import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.Task; 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 org.tasks.R; import org.tasks.R;
import java.io.IOException;
import edu.mit.mobile.android.imagecache.ImageCache;
/** /**
* Adapter for displaying a user's activity * Adapter for displaying a user's activity
* *
@ -60,7 +56,6 @@ 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 ImageCache imageCache;
private final String linkColor; private final String linkColor;
private final String fromView; private final String fromView;
@ -138,7 +133,6 @@ public class UpdateAdapter extends CursorAdapter {
inflater = (LayoutInflater) fragment.getActivity().getSystemService( inflater = (LayoutInflater) fragment.getActivity().getSystemService(
Context.LAYOUT_INFLATER_SERVICE); Context.LAYOUT_INFLATER_SERVICE);
imageCache = AsyncImageView.getImageCache();
this.fromView = fromView; this.fromView = fromView;
this.resource = resource; this.resource = resource;
@ -269,7 +263,7 @@ public class UpdateAdapter extends CursorAdapter {
} }
private void setupUserActivityRow(View view, UserActivity activity, User user) { private void setupUserActivityRow(View view, UserActivity activity, User user) {
final AsyncImageView commentPictureView = (AsyncImageView)view.findViewById(R.id.comment_picture); { final ImageView commentPictureView = (ImageView)view.findViewById(R.id.comment_picture); {
String pictureThumb = activity.getPictureUrl(UserActivity.PICTURE, RemoteModel.PICTURE_MEDIUM); String pictureThumb = activity.getPictureUrl(UserActivity.PICTURE, RemoteModel.PICTURE_MEDIUM);
String pictureFull = activity.getPictureUrl(UserActivity.PICTURE, RemoteModel.PICTURE_LARGE); String pictureFull = activity.getPictureUrl(UserActivity.PICTURE, RemoteModel.PICTURE_LARGE);
Bitmap updateBitmap = null; Bitmap updateBitmap = null;
@ -277,7 +271,7 @@ public class UpdateAdapter extends CursorAdapter {
updateBitmap = activity.getPictureBitmap(UserActivity.PICTURE); updateBitmap = activity.getPictureBitmap(UserActivity.PICTURE);
} }
setupImagePopupForCommentView(view, commentPictureView, pictureThumb, pictureFull, updateBitmap, setupImagePopupForCommentView(view, commentPictureView, pictureThumb, pictureFull, updateBitmap,
activity.getValue(UserActivity.MESSAGE), fragment, imageCache); activity.getValue(UserActivity.MESSAGE), fragment);
} }
// name // name
@ -302,37 +296,23 @@ public class UpdateAdapter extends CursorAdapter {
return false; return false;
} }
public static void setupImagePopupForCommentView(View view, AsyncImageView commentPictureView, final String pictureThumb, final String pictureFull, final Bitmap updateBitmap, public static void setupImagePopupForCommentView(View view, ImageView commentPictureView, final String pictureThumb, final String pictureFull, final Bitmap updateBitmap,
final String message, final Fragment fragment, ImageCache imageCache) { final String message, final Fragment fragment) {
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) {
commentPictureView.setImageBitmap(updateBitmap); commentPictureView.setImageBitmap(updateBitmap);
} else {
commentPictureView.setUrl(pictureThumb);
}
if (pictureThumb != null && imageCache.contains(pictureThumb) && updateBitmap == null) {
try {
commentPictureView.setImageBitmap(imageCache.get(pictureThumb));
} catch (IOException e) {
e.printStackTrace();
}
} else if (updateBitmap == null) {
commentPictureView.setUrl(pictureThumb);
} }
view.setOnClickListener(new OnClickListener() { view.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
AlertDialog image = new AlertDialog.Builder(fragment.getActivity()).create(); AlertDialog image = new AlertDialog.Builder(fragment.getActivity()).create();
AsyncImageView imageView = new AsyncImageView(fragment.getActivity()); ImageView imageView = new ImageView(fragment.getActivity());
imageView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT)); imageView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
imageView.setImageResource(android.R.drawable.ic_menu_gallery); imageView.setImageResource(android.R.drawable.ic_menu_gallery);
if (updateBitmap != null) { if (updateBitmap != null) {
imageView.setImageBitmap(updateBitmap); imageView.setImageBitmap(updateBitmap);
} else {
imageView.setUrl(pictureFull);
} }
image.setView(imageView); image.setView(imageView);

@ -1,75 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.helper;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.os.Looper;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.widget.ImageView;
import com.todoroo.andlib.service.ContextManager;
import java.io.IOException;
import edu.mit.mobile.android.imagecache.ImageCache;
public class AsyncImageView extends ImageView {
private final ImageCache imageDiskCache;
private Bitmap cacheImage;
private String cacheURL = ""; //$NON-NLS-1$
public AsyncImageView(Context context) {
super(context);
imageDiskCache = getImageCache();
}
public AsyncImageView(Context context, AttributeSet set) {
super(context, set);
imageDiskCache = getImageCache();
}
public AsyncImageView(Context context, AttributeSet set, int defStyle) {
super(context, set, defStyle);
imageDiskCache = getImageCache();
}
public void setUrl(String url) {
if (cacheImage != null && cacheURL.equals(url) && !TextUtils.isEmpty(url)) {
setImageBitmap(cacheImage);
return;
} else if (url != null && imageDiskCache != null && imageDiskCache.contains(url)) {
try {
cacheImage = imageDiskCache.get(url);
setImageBitmap(cacheImage);
cacheURL = url;
return;
} catch (IOException e) {
//
}
}
}
private static volatile ImageCache imageCacheInstance = null;
public static ImageCache getImageCache() {
if (imageCacheInstance == null) {
synchronized(AsyncImageView.class) {
if (imageCacheInstance == null) {
try {
if (Looper.myLooper() == null) {
Looper.prepare();
}
} catch (Exception e) {
// Ignore
}
imageCacheInstance = new ImageCache(ContextManager.getContext(), CompressFormat.JPEG, 85);
}
}
}
return imageCacheInstance;
}
}

@ -27,6 +27,7 @@ import android.view.inputmethod.EditorInfo;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
@ -54,7 +55,6 @@ import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.Task; 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.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.TaskService; import com.todoroo.astrid.service.TaskService;
@ -70,8 +70,6 @@ import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
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$
@ -97,7 +95,6 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
private final Resources resources; private final Resources resources;
private final ImageCache imageCache;
private final int cameraButton; private final int cameraButton;
private final String linkColor; private final String linkColor;
private int historyCount = 0; private int historyCount = 0;
@ -118,7 +115,6 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
super(fragment.getActivity()); super(fragment.getActivity());
DependencyInjectionService.getInstance().inject(this); DependencyInjectionService.getInstance().inject(this);
imageCache = AsyncImageView.getImageCache();
this.fragment = fragment; this.fragment = fragment;
this.activity = (AstridActivity) fragment.getActivity(); this.activity = (AstridActivity) fragment.getActivity();
@ -395,9 +391,8 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
} }
// picture // picture
final AsyncImageView commentPictureView = (AsyncImageView)view.findViewById(R.id.comment_picture); { final ImageView commentPictureView = (ImageView)view.findViewById(R.id.comment_picture);
UpdateAdapter.setupImagePopupForCommentView(view, commentPictureView, item.pictureThumb, item.pictureFull, item.commentBitmap, item.title.toString(), fragment, imageCache); UpdateAdapter.setupImagePopupForCommentView(view, commentPictureView, item.pictureThumb, item.pictureFull, item.commentBitmap, item.title.toString(), fragment);
}
} }
public void refreshData() { public void refreshData() {

@ -1,604 +0,0 @@
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 android.os.Build;
import android.os.StatFs;
import android.util.Log;
import com.todoroo.astrid.utility.Constants;
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;
/**
* <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
*/
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.
*/
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)}
* @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.
*/
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()}.
*/
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
* @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)}.
*
* @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.
*
* @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.
*
* @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.
*
* @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 (Constants.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 (Constants.DEBUG) {
Log.d(TAG, "trimmed " + cacheFile.getName() + " from cache.");
}
} else {
Log.e(TAG, "error deleting " + cacheFile);
}
}
if (Constants.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.
*/
protected abstract void toDisk(K key, V in, OutputStream out);
/**
* Implement this to do the actual disk reading.
* @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.
*
* @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;
}
}

@ -1,44 +0,0 @@
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> {
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;
}
}

@ -1,810 +0,0 @@
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 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;
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 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;
/**
* <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
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.
*
* @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.
*/
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.
*/
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
*/
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>
*/
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>
*/
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)}
*/
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)}
*
* @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)}.
*
* @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)}.
*/
@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)}.
*/
@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
* @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);
}
}

@ -1,30 +0,0 @@
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);
}
}

@ -1,72 +0,0 @@
package edu.mit.mobile.android.imagecache;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
/**
* 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;
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();
}
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);
}
}

@ -49,12 +49,12 @@
</RelativeLayout> </RelativeLayout>
<!-- picture --> <!-- picture -->
<com.todoroo.astrid.helper.AsyncImageView <ImageView
android:id="@+id/comment_picture" android:id="@+id/comment_picture"
android:layout_width="50dip" android:layout_width="50dip"
android:layout_height="50dip" android:layout_height="50dip"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_marginLeft="5dip" android:layout_marginLeft="50dip"
android:layout_marginBottom="4dip" android:layout_marginBottom="4dip"
android:visibility="gone" android:visibility="gone"
android:scaleType="fitCenter"/> android:scaleType="fitCenter"/>

Loading…
Cancel
Save