package com.afollestad.silk.images; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.util.LruCache; import com.afollestad.silk.Silk; import com.afollestad.silk.http.SilkHttpClient; import com.afollestad.silk.http.SilkHttpResponse; import com.afollestad.silk.utilities.DiskCache; import java.io.*; import java.net.URLEncoder; import java.util.concurrent.*; /** * <p>The most important class in the AImage library; downloads images, and handles caching them on the disk and in memory * so they can quickly be retrieved. Also allows you to download images to fit a certain width and height.</p> * <p/> * <p>If you're using AImage for displaying images in your UI, see {@link com.afollestad.silk.views.image.SilkImageView} and * the other variations of it for easy-to-use options.</p> */ public class SilkImageManager { public static final String SOURCE_FALLBACK = "aimage://fallback_image"; protected static final int MEM_CACHE_SIZE_KB = (int) (Runtime.getRuntime().maxMemory() / 2 / 1024); protected static final int ASYNC_THREAD_COUNT = (Runtime.getRuntime().availableProcessors() * 4); private final Context context; private final DiskCache mDiskCache; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final ExecutorService mNetworkExecutorService = newConfiguredThreadPool(); private final ExecutorService mDiskExecutorService = Executors.newCachedThreadPool(new LowPriorityThreadFactory()); private int fallbackImageId; private boolean DEBUG = false; private LruCache<String, Bitmap> mLruCache = newConfiguredLruCache(); public SilkImageManager(Context context) { this.context = context; mLruCache = new LruCache<String, Bitmap>(MEM_CACHE_SIZE_KB * 1024) { @Override public int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; mDiskCache = new DiskCache(context); } private static ExecutorService newConfiguredThreadPool() { int corePoolSize = 0; int maximumPoolSize = ASYNC_THREAD_COUNT; long keepAliveTime = 60L; TimeUnit unit = TimeUnit.SECONDS; BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(); RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); } private static LruCache<String, Bitmap> newConfiguredLruCache() { return new LruCache<String, Bitmap>(MEM_CACHE_SIZE_KB * 1024) { @Override public int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; } protected void log(String message) { if (!DEBUG) return; Log.i("SilkImageManager", message); } public boolean isDebugEnabled() { return DEBUG; } public SilkImageManager setDebugEnabled(boolean enabled) { this.DEBUG = enabled; return this; } /** * Sets the directory that will be used to cache images. */ public SilkImageManager setCacheDirectory(File cacheDir) { mDiskCache.setCacheDirectory(cacheDir); return this; } /** * Sets the resource ID of fallback image that is used when an image can't be loaded, or when you call * {@link com.afollestad.silk.views.image.SilkImageView#showFallback(SilkImageManager)} from the SilkImageView. */ public SilkImageManager setFallbackImage(int resourceId) { this.fallbackImageId = resourceId; return this; } /** * Gets an image from a URI on the thread (Android doesn't allow this on the main UI thread) and returns the result. * * @param source The URI to get the image from. */ public Bitmap get(String source, Dimension dimension) { if (source == null) { return null; } String key = Utils.getKey(source, dimension); Bitmap bitmap = mLruCache.get(key); if (bitmap == null) { bitmap = getBitmapFromDisk(key); } else { log("Got " + source + " from the memory cache."); } if (bitmap == null) { bitmap = getBitmapFromExternal(key, source, dimension, null); log("Got " + source + " from the external source."); } else { log("Got " + source + " from the disk cache."); } return bitmap; } /** * Gets an image from a URI on a separate thread and posts the results to a callback. * * @param source The URI to get the image from. * @param callback The callback that the result will be posted to. * @param cache Whether or not to load from the cache and save to the cache. */ public void get(final String source, final ImageListener callback, final Dimension dimension, boolean cache) { if (cache) { get(source, callback, dimension); return; } // Caching disabled, load from external immediately final String key = Utils.getKey(source, dimension); mNetworkExecutorService.execute(new Runnable() { @Override public void run() { final Bitmap bitmap = getBitmapFromExternal(key, source, dimension, new ProcessCallback() { @Override public Bitmap onProcess(Bitmap image) { if (callback != null && callback instanceof AdvancedImageListener) image = ((AdvancedImageListener) callback).onPostProcess(image); return image; } }); log("Got " + source + " from external source."); postCallback(callback, source, bitmap); } }); } /** * Gets an image from a URI on a separate thread and posts the results to a callback. * * @param source The URI to get the image from. * @param callback The callback that the result will be posted to. */ public void get(final String source, final ImageListener callback, final Dimension dimension) { if (!Looper.getMainLooper().equals(Looper.myLooper())) { throw new RuntimeException("This must only be executed on the main UI Thread!"); } else if (source == null) { return; } final String key = Utils.getKey(source, dimension); Bitmap bitmap = mLruCache.get(key); if (bitmap != null) { log("Got " + source + " from the memory cache."); postCallback(callback, source, bitmap); return; } mDiskExecutorService.execute(new Runnable() { @Override public void run() { final Bitmap bitmap = getBitmapFromDisk(key); if (bitmap != null) { log("Got " + source + " from the disk cache."); postCallback(callback, source, bitmap); return; } if (!Silk.isOnline(context) && source.startsWith("http")) { log("Device is offline, image is not cached; getting fallback image..."); Bitmap fallback = get(SilkImageManager.SOURCE_FALLBACK, dimension); if (callback != null && callback instanceof AdvancedImageListener) fallback = ((AdvancedImageListener) callback).onPostProcess(bitmap); postCallback(callback, source, fallback); return; } mNetworkExecutorService.execute(new Runnable() { @Override public void run() { Bitmap bitmap = getBitmapFromExternal(key, source, dimension, new ProcessCallback() { @Override public Bitmap onProcess(Bitmap image) { if (callback != null && callback instanceof AdvancedImageListener) image = ((AdvancedImageListener) callback).onPostProcess(image); return image; } }); log("Got " + source + " from external source."); postCallback(callback, source, bitmap); } }); } }); } public File getCacheFile(String originalSource, Dimension dimen) { return mDiskCache.getFile(Utils.getKey(originalSource, dimen)); } private void postCallback(final ImageListener callback, final String source, final Bitmap bitmap) { mHandler.post(new Runnable() { public void run() { if (callback != null) callback.onImageReceived(source, bitmap); } }); } private Bitmap getBitmapFromDisk(String key) { Bitmap bitmap = null; try { bitmap = mDiskCache.get(key); if (bitmap != null) { mLruCache.put(key, bitmap); } } catch (Exception e) { e.printStackTrace(); } return bitmap; } private Bitmap getBitmapFromExternal(String key, String source, Dimension dimension, ProcessCallback callback) { byte[] byteArray = sourceToBytes(source); if (byteArray == null) { source = SilkImageManager.SOURCE_FALLBACK; byteArray = sourceToBytes(source); } Bitmap bitmap = Utils.decodeByteArray(byteArray, dimension); if (source.equals(SilkImageManager.SOURCE_FALLBACK)) { if (callback != null && bitmap != null) bitmap = callback.onProcess(bitmap); return bitmap; } if (bitmap != null) { if (callback != null) bitmap = callback.onProcess(bitmap); if (!source.startsWith("content") && !source.startsWith("file")) { // If the source is already from the local disk, don't cache it locally again. try { mDiskCache.put(key, bitmap); } catch (Exception e) { e.printStackTrace(); } } mLruCache.put(key, bitmap); return bitmap; } return null; } private byte[] inputStreamToBytes(InputStream stream) { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); try { IOUtils.copy(stream, byteArrayOutputStream); } catch (IOException e) { IOUtils.closeQuietly(byteArrayOutputStream); return null; } return byteArrayOutputStream.toByteArray(); } private byte[] sourceToBytes(String source) { InputStream inputStream = null; byte[] toreturn = null; try { if (source.equals(SilkImageManager.SOURCE_FALLBACK)) { log("Loading fallback image..."); if (fallbackImageId > 0) inputStream = context.getResources().openRawResource(fallbackImageId); else return null; } else if (source.startsWith("content")) { inputStream = context.getContentResolver().openInputStream(Uri.parse(source)); } else if (source.startsWith("file")) { Uri uri = Uri.parse(source); inputStream = new FileInputStream(new File(uri.getPath())); } else { SilkHttpClient client = new SilkHttpClient(context); SilkHttpResponse response = client.get(source); inputStream = response.getContent().getContent(); } toreturn = inputStreamToBytes(inputStream); } catch (Exception e) { log("Error: " + e.getMessage()); e.printStackTrace(); return null; } finally { IOUtils.closeQuietly(inputStream); } return toreturn; } private interface ProcessCallback { public Bitmap onProcess(Bitmap image); } public interface ImageListener { public abstract void onImageReceived(String source, Bitmap bitmap); } public interface AdvancedImageListener extends ImageListener { public abstract Bitmap onPostProcess(Bitmap image); } public static class Utils { public static int calculateInSampleSize(BitmapFactory.Options options, Dimension dimension) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > dimension.getHeight() || width > dimension.getWidth()) { // Calculate ratios of height and width to requested height and width final int heightRatio = Math.round((float) height / (float) dimension.getHeight()); final int widthRatio = Math.round((float) width / (float) dimension.getWidth()); // Choose the smallest ratio as inSampleSize value, this will guarantee // a final image with both dimensions larger than or equal to the // requested height and width. inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; } public static BitmapFactory.Options getBitmapFactoryOptions(Dimension dimension) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inPurgeable = true; options.inInputShareable = true; options.inPreferredConfig = Bitmap.Config.ARGB_8888; if (dimension != null) options.inSampleSize = calculateInSampleSize(options, dimension); return options; } public static Bitmap decodeByteArray(byte[] byteArray, Dimension dimension) { try { BitmapFactory.Options bitmapFactoryOptions = getBitmapFactoryOptions(dimension); return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length, bitmapFactoryOptions); } catch (Throwable t) { t.printStackTrace(); } return null; } public static String getKey(String source, Dimension dimension) { if (source == null) return null; source = source.replace("http://", "").replace("https://", ""); String ext = null; if (source.endsWith(".jpg") || source.endsWith(".jpeg")) ext = ".jpeg"; else if (source.endsWith(".png")) ext = ".png"; if (dimension != null) source += "_" + dimension.toString(); try { return URLEncoder.encode(source, "UTF-8") + ext; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return null; } } public static class IOUtils { public static final int DEFAULT_BUFFER_SIZE = 1024 * 4; public static void closeQuietly(InputStream input) { closeQuietly((Closeable) input); } public static void closeQuietly(OutputStream output) { closeQuietly((Closeable) output); } public static void closeQuietly(Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (IOException ioe) { // ignore } } public static int copy(InputStream input, OutputStream output) throws IOException { long count = copyLarge(input, output); if (count > Integer.MAX_VALUE) { return -1; } return (int) count; } private static long copyLarge(InputStream input, OutputStream output) throws IOException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; long count = 0; int n; while (-1 != (n = input.read(buffer))) { output.write(buffer, 0, n); count += n; } return count; } } }