diff options
author | daniel oeh <daniel.oeh@gmail.com> | 2013-09-05 15:24:50 +0200 |
---|---|---|
committer | daniel oeh <daniel.oeh@gmail.com> | 2013-09-05 15:24:50 +0200 |
commit | 02926a6e5ffa968d08efeae5012a0ecf41a6f33a (patch) | |
tree | f9cfef6a7569b82301ea1d1aa7066cefc7cd1146 /src/de/danoeh/antennapod/asynctask | |
parent | 862b8db20b8003691b2a40693275c2390dd9a4e7 (diff) | |
parent | eb7addaaf07e5ede3c1bc730b33aee6541c78290 (diff) | |
download | AntennaPod-02926a6e5ffa968d08efeae5012a0ecf41a6f33a.zip |
Merge branch 'gpoddernet' into develop
Conflicts:
AndroidManifest.xml
res/values/arrays.xml
res/values/strings.xml
res/xml/preferences.xml
src/de/danoeh/antennapod/activity/PreferenceActivity.java
Diffstat (limited to 'src/de/danoeh/antennapod/asynctask')
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; } /** |