package de.danoeh.antennapodsp.asynctask; import android.os.Handler; import android.util.Log; import android.util.Pair; import android.widget.ImageView; import de.danoeh.antennapodsp.AppConfig; import de.danoeh.antennapodsp.PodcastApp; import de.danoeh.antennapodsp.R; import de.danoeh.antennapodsp.service.download.DownloadRequest; import de.danoeh.antennapodsp.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; } }; } } }