summaryrefslogtreecommitdiff
path: root/src/de/danoeh/antennapod/asynctask
diff options
context:
space:
mode:
Diffstat (limited to 'src/de/danoeh/antennapod/asynctask')
-rw-r--r--src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java190
-rw-r--r--src/de/danoeh/antennapod/asynctask/ImageDiskCache.java391
-rw-r--r--src/de/danoeh/antennapod/asynctask/ImageLoader.java14
3 files changed, 497 insertions, 98 deletions
diff --git a/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java b/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java
index 7ba68ae22..cb8e4d292 100644
--- a/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java
+++ b/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java
@@ -2,105 +2,115 @@ package de.danoeh.antennapod.asynctask;
import android.content.res.TypedArray;
import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
import android.os.Handler;
import android.util.Log;
import android.widget.ImageView;
import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.asynctask.ImageLoader.ImageWorkerTaskResource;
import de.danoeh.antennapod.util.BitmapDecoder;
public class BitmapDecodeWorkerTask extends Thread {
- protected int PREFERRED_LENGTH;
-
- /** Can be thumbnail or cover */
- protected int imageType;
-
- private static final String TAG = "BitmapDecodeWorkerTask";
- private ImageView target;
- protected CachedBitmap cBitmap;
-
- protected ImageLoader.ImageWorkerTaskResource imageResource;
-
- private Handler handler;
-
- private final int defaultCoverResource;
-
- public BitmapDecodeWorkerTask(Handler handler, ImageView target,
- ImageWorkerTaskResource imageResource, int length, int imageType) {
- super();
- this.handler = handler;
- this.target = target;
- this.imageResource = imageResource;
- this.PREFERRED_LENGTH = length;
- this.imageType = imageType;
- TypedArray res = target.getContext().obtainStyledAttributes(
- new int[] { R.attr.default_cover });
- this.defaultCoverResource = res.getResourceId(0, 0);
- res.recycle();
- }
-
- /**
- * Should return true if tag of the imageview is still the same it was
- * before the bitmap was decoded
- */
- protected boolean tagsMatching(ImageView target) {
- return target.getTag() == null
- || target.getTag().equals(imageResource.getImageLoaderCacheKey());
- }
-
- protected void onPostExecute() {
- // check if imageview is still supposed to display this image
- if (tagsMatching(target) && cBitmap.getBitmap() != null) {
- target.setImageBitmap(cBitmap.getBitmap());
- } else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "Not displaying image");
- }
- }
-
- @Override
- public void run() {
- cBitmap = new CachedBitmap(BitmapDecoder.decodeBitmapFromWorkerTaskResource(
- PREFERRED_LENGTH, imageResource), PREFERRED_LENGTH);
- if (cBitmap.getBitmap() != null) {
- storeBitmapInCache(cBitmap);
- } else {
- Log.w(TAG, "Could not load bitmap. Using default image.");
- cBitmap = new CachedBitmap(BitmapFactory.decodeResource(
- target.getResources(), defaultCoverResource),
- PREFERRED_LENGTH);
- }
- if (AppConfig.DEBUG)
- Log.d(TAG, "Finished loading bitmaps");
-
- endBackgroundTask();
- }
-
- protected final void endBackgroundTask() {
- handler.post(new Runnable() {
-
- @Override
- public void run() {
- onPostExecute();
- }
-
- });
- }
-
- protected void onInvalidStream() {
- cBitmap = new CachedBitmap(BitmapFactory.decodeResource(
- target.getResources(), defaultCoverResource), PREFERRED_LENGTH);
- }
-
- protected void storeBitmapInCache(CachedBitmap cb) {
- ImageLoader loader = ImageLoader.getInstance();
- if (imageType == ImageLoader.IMAGE_TYPE_COVER) {
- loader.addBitmapToCoverCache(imageResource.getImageLoaderCacheKey(), cb);
- } else if (imageType == ImageLoader.IMAGE_TYPE_THUMBNAIL) {
- loader.addBitmapToThumbnailCache(imageResource.getImageLoaderCacheKey(), cb);
- }
- }
+ protected int PREFERRED_LENGTH;
+ public static final int FADE_DURATION = 500;
+
+ /**
+ * Can be thumbnail or cover
+ */
+ protected int imageType;
+
+ private static final String TAG = "BitmapDecodeWorkerTask";
+ private ImageView target;
+ protected CachedBitmap cBitmap;
+
+ protected ImageLoader.ImageWorkerTaskResource imageResource;
+
+ private Handler handler;
+
+ private final int defaultCoverResource;
+
+ public BitmapDecodeWorkerTask(Handler handler, ImageView target,
+ ImageWorkerTaskResource imageResource, int length, int imageType) {
+ super();
+ this.handler = handler;
+ this.target = target;
+ this.imageResource = imageResource;
+ this.PREFERRED_LENGTH = length;
+ this.imageType = imageType;
+ this.defaultCoverResource = android.R.color.transparent;
+ }
+
+ /**
+ * Should return true if tag of the imageview is still the same it was
+ * before the bitmap was decoded
+ */
+ protected boolean tagsMatching(ImageView target) {
+ return target.getTag(R.id.imageloader_key) == null
+ || target.getTag(R.id.imageloader_key).equals(imageResource.getImageLoaderCacheKey());
+ }
+
+ protected void onPostExecute() {
+ // check if imageview is still supposed to display this image
+ if (tagsMatching(target) && cBitmap.getBitmap() != null) {
+ Drawable[] drawables = new Drawable[]{
+ PodcastApp.getInstance().getResources().getDrawable(android.R.color.transparent),
+ new BitmapDrawable(PodcastApp.getInstance().getResources(), cBitmap.getBitmap())
+ };
+ TransitionDrawable transitionDrawable = new TransitionDrawable(drawables);
+ target.setImageDrawable(transitionDrawable);
+ transitionDrawable.startTransition(FADE_DURATION);
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Not displaying image");
+ }
+ }
+
+ @Override
+ public void run() {
+ cBitmap = new CachedBitmap(BitmapDecoder.decodeBitmapFromWorkerTaskResource(
+ PREFERRED_LENGTH, imageResource), PREFERRED_LENGTH);
+ if (cBitmap.getBitmap() != null) {
+ storeBitmapInCache(cBitmap);
+ } else {
+ Log.w(TAG, "Could not load bitmap. Using default image.");
+ cBitmap = new CachedBitmap(BitmapFactory.decodeResource(
+ target.getResources(), defaultCoverResource),
+ PREFERRED_LENGTH);
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Finished loading bitmaps");
+
+ endBackgroundTask();
+ }
+
+ protected final void endBackgroundTask() {
+ handler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ onPostExecute();
+ }
+
+ });
+ }
+
+ protected void onInvalidStream() {
+ cBitmap = new CachedBitmap(BitmapFactory.decodeResource(
+ target.getResources(), defaultCoverResource), PREFERRED_LENGTH);
+ }
+
+ protected void storeBitmapInCache(CachedBitmap cb) {
+ ImageLoader loader = ImageLoader.getInstance();
+ if (imageType == ImageLoader.IMAGE_TYPE_COVER) {
+ loader.addBitmapToCoverCache(imageResource.getImageLoaderCacheKey(), cb);
+ } else if (imageType == ImageLoader.IMAGE_TYPE_THUMBNAIL) {
+ loader.addBitmapToThumbnailCache(imageResource.getImageLoaderCacheKey(), cb);
+ }
+ }
}
diff --git a/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java b/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java
new file mode 100644
index 000000000..f7f6b576f
--- /dev/null
+++ b/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java
@@ -0,0 +1,391 @@
+package de.danoeh.antennapod.asynctask;
+
+import android.os.Handler;
+import android.util.Log;
+import android.util.Pair;
+import android.widget.ImageView;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.service.download.DownloadRequest;
+import de.danoeh.antennapod.service.download.HttpDownloader;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Provides local cache for storing downloaded image. An image disk cache downloads images and stores them as long
+ * as the cache is not full. Once the cache is full, the image disk cache will delete older images.
+ */
+public class ImageDiskCache {
+ private static final String TAG = "ImageDiskCache";
+
+ private static HashMap<String, ImageDiskCache> cacheSingletons = new HashMap<String, ImageDiskCache>();
+
+ /**
+ * Return a default instance of an ImageDiskCache. This cache will store data in the external cache folder.
+ */
+ public static synchronized ImageDiskCache getDefaultInstance() {
+ final String DEFAULT_PATH = "imagecache";
+ final long DEFAULT_MAX_CACHE_SIZE = 10 * 1024 * 1024;
+
+ File cacheDir = PodcastApp.getInstance().getExternalCacheDir();
+ if (cacheDir == null) {
+ return null;
+ }
+ return getInstance(new File(cacheDir, DEFAULT_PATH).getAbsolutePath(), DEFAULT_MAX_CACHE_SIZE);
+ }
+
+ /**
+ * Return an instance of an ImageDiskCache that stores images in the specified folder.
+ */
+ public static synchronized ImageDiskCache getInstance(String path, long maxCacheSize) {
+ if (path == null) {
+ throw new NullPointerException();
+ }
+ if (cacheSingletons.containsKey(path)) {
+ return cacheSingletons.get(path);
+ }
+
+ ImageDiskCache cache = cacheSingletons.get(path);
+ if (cache == null) {
+ cache = new ImageDiskCache(path, maxCacheSize);
+ cacheSingletons.put(new File(path).getAbsolutePath(), cache);
+ }
+ cacheSingletons.put(path, cache);
+ return cache;
+ }
+
+ /**
+ * Filename - cache object mapping
+ */
+ private static final String CACHE_FILE_NAME = "cachefile";
+ private ExecutorService executor;
+ private ConcurrentHashMap<String, DiskCacheObject> diskCache;
+ private final long maxCacheSize;
+ private int cacheSize;
+ private final File cacheFolder;
+ private Handler handler;
+
+ private ImageDiskCache(String path, long maxCacheSize) {
+ this.maxCacheSize = maxCacheSize;
+ this.cacheFolder = new File(path);
+ if (!cacheFolder.exists() && !cacheFolder.mkdir()) {
+ throw new IllegalArgumentException("Image disk cache could not create cache folder in: " + path);
+ }
+
+ executor = Executors.newFixedThreadPool(Runtime.getRuntime()
+ .availableProcessors());
+ handler = new Handler();
+ }
+
+ private synchronized void initCacheFolder() {
+ if (diskCache == null) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Initializing cache folder");
+ File cacheFile = new File(cacheFolder, CACHE_FILE_NAME);
+ if (cacheFile.exists()) {
+ try {
+ InputStream in = new FileInputStream(cacheFile);
+ BufferedInputStream buffer = new BufferedInputStream(in);
+ ObjectInputStream objectInput = new ObjectInputStream(buffer);
+ diskCache = (ConcurrentHashMap<String, DiskCacheObject>) objectInput.readObject();
+ // calculate cache size
+ for (DiskCacheObject dco : diskCache.values()) {
+ cacheSize += dco.size;
+ }
+ deleteInvalidFiles();
+ } catch (IOException e) {
+ e.printStackTrace();
+ diskCache = new ConcurrentHashMap<String, DiskCacheObject>();
+ } catch (ClassCastException e) {
+ e.printStackTrace();
+ diskCache = new ConcurrentHashMap<String, DiskCacheObject>();
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ diskCache = new ConcurrentHashMap<String, DiskCacheObject>();
+ }
+ } else {
+ diskCache = new ConcurrentHashMap<String, DiskCacheObject>();
+ }
+ }
+ }
+
+ private List<File> getCacheFileList() {
+ Collection<DiskCacheObject> values = diskCache.values();
+ List<File> files = new ArrayList<File>();
+ for (DiskCacheObject dco : values) {
+ files.add(dco.getFile());
+ }
+ files.add(new File(cacheFolder, CACHE_FILE_NAME));
+ return files;
+ }
+
+ private Pair<String, DiskCacheObject> getOldestCacheObject() {
+ Collection<String> keys = diskCache.keySet();
+ DiskCacheObject oldest = null;
+ String oldestKey = null;
+
+ for (String key : keys) {
+
+ if (oldestKey == null) {
+ oldestKey = key;
+ oldest = diskCache.get(key);
+ } else {
+ DiskCacheObject dco = diskCache.get(key);
+ if (oldest.timestamp > dco.timestamp) {
+ oldestKey = key;
+ oldest = diskCache.get(key);
+ }
+ }
+ }
+ return new Pair<String, DiskCacheObject>(oldestKey, oldest);
+ }
+
+ private synchronized void deleteCacheObject(String key, DiskCacheObject value) {
+ Log.i(TAG, "Deleting cached object: " + key);
+ diskCache.remove(key);
+ boolean result = value.getFile().delete();
+ if (!result) {
+ Log.w(TAG, "Could not delete file " + value.fileUrl);
+ }
+ cacheSize -= value.size;
+ }
+
+ private synchronized void deleteInvalidFiles() {
+ // delete files that are not stored inside the cache
+ File[] files = cacheFolder.listFiles();
+ List<File> cacheFiles = getCacheFileList();
+ for (File file : files) {
+ if (!cacheFiles.contains(file)) {
+ Log.i(TAG, "Deleting unused file: " + file.getAbsolutePath());
+ boolean result = file.delete();
+ if (!result) {
+ Log.w(TAG, "Could not delete file: " + file.getAbsolutePath());
+ }
+ }
+ }
+ }
+
+ private synchronized void cleanup() {
+ if (cacheSize > maxCacheSize) {
+ while (cacheSize > maxCacheSize) {
+ Pair<String, DiskCacheObject> oldest = getOldestCacheObject();
+ deleteCacheObject(oldest.first, oldest.second);
+ }
+ }
+ }
+
+ /**
+ * Loads a new image from the disk cache. If the image that the url points to has already been downloaded, the image will
+ * be loaded from the disk. Otherwise, the image will be downloaded first.
+ * The image will be stored in the thumbnail cache.
+ */
+ public void loadThumbnailBitmap(final String url, final ImageView target, final int length) {
+ final ImageLoader il = ImageLoader.getInstance();
+ target.setTag(R.id.image_disk_cache_key, url);
+ if (diskCache != null) {
+ DiskCacheObject dco = getFromCacheIfAvailable(url);
+ if (dco != null) {
+ il.loadThumbnailBitmap(dco.loadImage(), target, length);
+ return;
+ }
+ }
+ target.setImageResource(android.R.color.transparent);
+ executor.submit(new ImageDownloader(url) {
+ @Override
+ protected void onImageLoaded(DiskCacheObject diskCacheObject) {
+ final Object tag = target.getTag(R.id.image_disk_cache_key);
+ if (tag != null || StringUtils.equals((String) tag, url)) {
+ il.loadThumbnailBitmap(diskCacheObject.loadImage(), target, length);
+ }
+ }
+ });
+
+ }
+
+ /**
+ * Loads a new image from the disk cache. If the image that the url points to has already been downloaded, the image will
+ * be loaded from the disk. Otherwise, the image will be downloaded first.
+ * The image will be stored in the cover cache.
+ */
+ public void loadCoverBitmap(final String url, final ImageView target, final int length) {
+ final ImageLoader il = ImageLoader.getInstance();
+ target.setTag(R.id.image_disk_cache_key, url);
+ if (diskCache != null) {
+ DiskCacheObject dco = getFromCacheIfAvailable(url);
+ if (dco != null) {
+ il.loadThumbnailBitmap(dco.loadImage(), target, length);
+ return;
+ }
+ }
+ target.setImageResource(android.R.color.transparent);
+ executor.submit(new ImageDownloader(url) {
+ @Override
+ protected void onImageLoaded(DiskCacheObject diskCacheObject) {
+ final Object tag = target.getTag(R.id.image_disk_cache_key);
+ if (tag != null || StringUtils.equals((String) tag, url)) {
+ il.loadCoverBitmap(diskCacheObject.loadImage(), target, length);
+ }
+ }
+ });
+ }
+
+ private synchronized void addToDiskCache(String url, DiskCacheObject obj) {
+ if (diskCache == null) {
+ initCacheFolder();
+ }
+ if (AppConfig.DEBUG) Log.d(TAG, "Adding new image to disk cache: " + url);
+ diskCache.put(url, obj);
+ cacheSize += obj.size;
+ if (cacheSize > maxCacheSize) {
+ cleanup();
+ }
+ saveCacheInfoFile();
+ }
+
+ private synchronized void saveCacheInfoFile() {
+ OutputStream out = null;
+ try {
+ out = new BufferedOutputStream(new FileOutputStream(new File(cacheFolder, CACHE_FILE_NAME)));
+ ObjectOutputStream objOut = new ObjectOutputStream(out);
+ objOut.writeObject(diskCache);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ private synchronized DiskCacheObject getFromCacheIfAvailable(String key) {
+ if (diskCache == null) {
+ initCacheFolder();
+ }
+ DiskCacheObject dco = diskCache.get(key);
+ if (dco != null) {
+ dco.timestamp = System.currentTimeMillis();
+ }
+ return dco;
+ }
+
+ ConcurrentHashMap<String, File> runningDownloads = new ConcurrentHashMap<String, File>();
+
+ private abstract class ImageDownloader implements Runnable {
+ private String downloadUrl;
+
+ public ImageDownloader(String downloadUrl) {
+ this.downloadUrl = downloadUrl;
+ }
+
+ protected abstract void onImageLoaded(DiskCacheObject diskCacheObject);
+
+ public void run() {
+ DiskCacheObject tmp = getFromCacheIfAvailable(downloadUrl);
+ if (tmp != null) {
+ onImageLoaded(tmp);
+ return;
+ }
+
+ DiskCacheObject dco = null;
+ File newFile = new File(cacheFolder, Integer.toString(downloadUrl.hashCode()));
+ synchronized (ImageDiskCache.this) {
+ if (runningDownloads.containsKey(newFile.getAbsolutePath())) {
+ Log.d(TAG, "Download is already running: " + newFile.getAbsolutePath());
+ return;
+ } else {
+ runningDownloads.put(newFile.getAbsolutePath(), newFile);
+ }
+ }
+ if (newFile.exists()) {
+ newFile.delete();
+ }
+
+ HttpDownloader result = downloadFile(newFile.getAbsolutePath(), downloadUrl);
+ if (result.getResult().isSuccessful()) {
+ long size = result.getDownloadRequest().getSoFar();
+
+ dco = new DiskCacheObject(newFile.getAbsolutePath(), size);
+ addToDiskCache(downloadUrl, dco);
+ if (AppConfig.DEBUG) Log.d(TAG, "Image was downloaded");
+ } else {
+ Log.w(TAG, "Download of url " + downloadUrl + " failed. Reason: " + result.getResult().getReasonDetailed() + "(" + result.getResult().getReason() + ")");
+ }
+
+ if (dco != null) {
+ final DiskCacheObject dcoRef = dco;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ onImageLoaded(dcoRef);
+ }
+ });
+
+ }
+ runningDownloads.remove(newFile.getAbsolutePath());
+
+ }
+
+ private HttpDownloader downloadFile(String destination, String source) {
+ DownloadRequest request = new DownloadRequest(destination, source, "", 0, 0);
+ HttpDownloader downloader = new HttpDownloader(request);
+ downloader.call();
+ return downloader;
+ }
+ }
+
+ private static class DiskCacheObject implements Serializable {
+ private final String fileUrl;
+
+ /**
+ * Last usage of this image cache object.
+ */
+ private long timestamp;
+ private final long size;
+
+ public DiskCacheObject(String fileUrl, long size) {
+ if (fileUrl == null) {
+ throw new NullPointerException();
+ }
+ this.fileUrl = fileUrl;
+ this.timestamp = System.currentTimeMillis();
+ this.size = size;
+ }
+
+ public File getFile() {
+ return new File(fileUrl);
+ }
+
+ public ImageLoader.ImageWorkerTaskResource loadImage() {
+ return new ImageLoader.ImageWorkerTaskResource() {
+
+ @Override
+ public InputStream openImageInputStream() {
+ try {
+ return new FileInputStream(getFile());
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @Override
+ public InputStream reopenImageInputStream(InputStream input) {
+ IOUtils.closeQuietly(input);
+ return openImageInputStream();
+ }
+
+ @Override
+ public String getImageLoaderCacheKey() {
+ return fileUrl;
+ }
+ };
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/asynctask/ImageLoader.java b/src/de/danoeh/antennapod/asynctask/ImageLoader.java
index 45a99e704..a4a9bc823 100644
--- a/src/de/danoeh/antennapod/asynctask/ImageLoader.java
+++ b/src/de/danoeh/antennapod/asynctask/ImageLoader.java
@@ -66,7 +66,7 @@ public class ImageLoader {
private ExecutorService createExecutor() {
return Executors.newFixedThreadPool(Runtime.getRuntime()
- .availableProcessors() + 1, new ThreadFactory() {
+ .availableProcessors(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
@@ -106,7 +106,8 @@ public class ImageLoader {
.getContext());
if (source != null && source.getImageLoaderCacheKey() != null) {
- CachedBitmap cBitmap = getBitmapFromCoverCache(source.getImageLoaderCacheKey());
+ target.setTag(R.id.imageloader_key, source.getImageLoaderCacheKey());
+ CachedBitmap cBitmap = getBitmapFromCoverCache(source.getImageLoaderCacheKey());
if (cBitmap != null && cBitmap.getLength() >= length) {
target.setImageBitmap(cBitmap.getBitmap());
} else {
@@ -143,7 +144,8 @@ public class ImageLoader {
.getContext());
if (source != null && source.getImageLoaderCacheKey() != null) {
- CachedBitmap cBitmap = getBitmapFromThumbnailCache(source.getImageLoaderCacheKey());
+ target.setTag(R.id.imageloader_key, source.getImageLoaderCacheKey());
+ CachedBitmap cBitmap = getBitmapFromThumbnailCache(source.getImageLoaderCacheKey());
if (cBitmap != null && cBitmap.getLength() >= length) {
target.setImageBitmap(cBitmap.getBitmap());
} else {
@@ -195,11 +197,7 @@ public class ImageLoader {
}
private int getDefaultCoverResource(Context context) {
- TypedArray res = context
- .obtainStyledAttributes(new int[] { R.attr.default_cover });
- final int defaultCoverResource = res.getResourceId(0, 0);
- res.recycle();
- return defaultCoverResource;
+ return android.R.color.transparent;
}
/**