/* * Copyright (c) 2012 Daniel Huckaby * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.handlerexploit.prime.utils; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.util.Log; import com.handlerexploit.common.utils.DiskLruCache; import com.handlerexploit.common.utils.DiskLruCache.Editor; import com.handlerexploit.common.utils.DiskLruCache.Snapshot; import com.handlerexploit.common.utils.LruCache; import com.handlerexploit.prime.Configuration; import com.handlerexploit.prime.utils.ApacheUtils.DigestUtils; import com.handlerexploit.prime.utils.ApacheUtils.IOUtils; import com.handlerexploit.prime.widgets.RemoteImageView; /** * This class is responsible for retrieving and caching all Bitmap images. Using * a two tier caching mechanism the images are saved both in memory and on disk * for both an efficient and clean user experience.</br> * * <div class="special reference"> <b>Development Notes:</b></br> The most * fool-proof method of integration is to use * {@link RemoteImageView#setImageURL(String) * RemoteImageView.setImageURL(String)}. </div> * * If you want to receive images asynchronously you can use * {@link ImageManager#get(String, OnImageReceivedListener)} or * {@link ImageManager#get(Request)}.</br></br> * * <pre> * final String imageURL = "http://example.com/image.png"; * ImageManager imageManager = ImageManager.getInstance(context); * imageManager.get(imageURL, new OnImageReceivedListener() { * * @Override * public void onImageReceived(String source, Bitmap bitmap) { * // Do something with the retrieved Bitmap * } * }); * * imageManager.get(new Request() { * * @Override * public String getSource() { * return imageURL; * } * * @Override * public void onImageReceived(String source, Bitmap bitmap) { * // Do something with the retrieved Bitmap * } * });</pre> * * If you want to retrieve images synchronously you can use * {@link ImageManager#get(String)}.</br></br> * * <pre> * ImageManager imageManager = ImageManager.getInstance(context); * String imageURL = "http://example.com/image.png"; * Bitmap bitmap = imageManager.get(imageURL);</pre> */ public final class ImageManager { private static final String TAG = "ImageManager"; private static final Object[] LOCK = new Object[0]; private static ImageManager sInstance; private final String mCacheDirectory; private DiskLruCache mDiskLruCache; private Bitmap.Config mPreferredConfig = Bitmap.Config.ARGB_8888; private Handler mHandler = new Handler(Looper.getMainLooper()); private LruCache<String, Bitmap> mLruCache = newConfiguredLruCache(); private ExecutorService mNetworkExecutorService = newConfiguredThreadPool(); private ExecutorService mDiskExecutorService = Executors.newCachedThreadPool(new LowPriorityThreadFactory()); private ImageManager(Context context) { mCacheDirectory = getCacheDirectory(context).getAbsolutePath(); mDiskLruCache = open(mCacheDirectory); } public static synchronized ImageManager getInstance(Context context) { if (sInstance == null) { sInstance = new ImageManager(context); } return sInstance; } private static DiskLruCache open(String cacheDirectory) { try { return DiskLruCache.open(getImageCacheDirectory(cacheDirectory), 1, 1, Configuration.DISK_CACHE_SIZE_KB * 1024); } catch (IOException e) { Log.e(TAG, e.getMessage()); throw new RuntimeException(e); } } private static File getImageCacheDirectory(String cacheDirectory) { return new File(cacheDirectory, "/images/"); } public void setPreferredConfig(Bitmap.Config preferredConfig) { mPreferredConfig = preferredConfig; } /** * Return the appropriate {@link Bitmap} associated with the provided * {@link String}. This is a synchronous call, if you need to asynchronously * retrieve an image use * {@link ImageManager#get(String, OnImageReceivedListener)} or * {@link ImageManager#get(Request)}. * * @param source * The URL of a remote image */ public Bitmap get(String source) { String key = getKey(source); Bitmap bitmap = getBitmapFromMemory(key); if (bitmap == null) { bitmap = getBitmapFromDisk(key); } if (bitmap == null) { bitmap = getBitmapFromNetwork(key, source, 0, 0, null); } return bitmap; } /** * Return the appropriate {@link Bitmap} associated with the provided * {@link OnImageReceivedListener} synchronously or asynchronously depending * on the state of the internal cache state. <br> * <br> * This must only be executed on the main UI Thread. * * @param source * The URL of a remote image * @param listener * Listener for being notified when image is retrieved, can be * null */ public void get(final String source, final OnImageReceivedListener listener) { get(new Request() { @Override public String getSource() { return source; } @Override public void onImageReceived(String source, Bitmap bitmap) { if (listener != null) { listener.onImageReceived(source, bitmap); } } }); } /** * Return the appropriate {@link Bitmap} associated with the provided * {@link Request} synchronously or asynchronously depending on the state of * the internal cache state. <br> * <br> * This must only be executed on the main UI Thread. */ public void get(Request request) { if (request instanceof ExtendedRequest) { get((ExtendedRequest) request); } else { get(new SimpleRequest(request)); } } private void get(final ExtendedRequest request) { final String source = request != null ? request.getSource() : null; if (source == null) { return; } if (!Looper.getMainLooper().equals(Looper.myLooper())) { throw new RuntimeException("This must only be executed on the main UI Thread!"); } final int requestHeight = request.getHeight(); final int requestWidth = request.getWidth(); final String key = requestHeight > 0 && requestWidth > 0 ? getKey(source + requestHeight + "x" + requestWidth) : getKey(source); Bitmap bitmap = getBitmapFromMemory(key); if (bitmap != null) { request.onImageReceived(source, bitmap); } else { mDiskExecutorService.execute(new Runnable() { @Override public void run() { if (verifySourceOverTime(source, request)) { final Bitmap bitmap = getBitmapFromDisk(key); if (bitmap != null) { mHandler.post(new Runnable() { @Override public void run() { request.onImageReceived(source, bitmap); } }); } else { mNetworkExecutorService.execute(new Runnable() { @Override public void run() { final Bitmap bitmap = getBitmapFromNetwork(key, source, requestHeight, requestWidth, request); mHandler.post(new Runnable() { @Override public void run() { request.onImageReceived(source, bitmap); } }); } }); } } } }); } } private Bitmap getBitmapFromMemory(String key) { return mLruCache.get(key); } private Bitmap getBitmapFromDisk(String key) { Bitmap bitmap = null; Snapshot snapshot = null; try { snapshot = mDiskLruCache.get(key); } catch (IOException e) { Log.w(TAG, e); } finally { if (snapshot != null) { bitmap = decodeFromSnapshot(snapshot); if (bitmap != null) { mLruCache.put(key, bitmap); } } } return bitmap; } private Bitmap getBitmapFromNetwork(String key, String source, int height, int width, ExtendedRequest request) { byte[] byteArray = copyURLToByteArray(source); if (byteArray != null) { Bitmap bitmap = decodeByteArray(byteArray, height, width, request); if (bitmap != null) { copyBitmapToDiskLruCache(key, bitmap); mLruCache.put(key, bitmap); return bitmap; } } return null; } private static Bitmap decodeByteArray(byte[] byteArray, int height, int width, ExtendedRequest request) { try { Bitmap bitmap; BitmapFactory.Options bitmapFactoryOptions = getBitmapFactoryOptions(); synchronized (LOCK) { if (height > 0 && width > 0) { bitmapFactoryOptions.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length, bitmapFactoryOptions); int heightRatio = (int) Math.ceil(bitmapFactoryOptions.outHeight / (float) height); int widthRatio = (int) Math.ceil(bitmapFactoryOptions.outWidth / (float) width); if (heightRatio > 1 || widthRatio > 1) { if (heightRatio > widthRatio) { bitmapFactoryOptions.inSampleSize = heightRatio; } else { bitmapFactoryOptions.inSampleSize = widthRatio; } } bitmapFactoryOptions.inJustDecodeBounds = false; } bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length, bitmapFactoryOptions); } if (request != null) { bitmap = request.onPreProcess(bitmap); } return bitmap; } catch (Throwable t) { if (Configuration.DEBUGGING) { Log.w(TAG, t); } } return null; } private static byte[] copyURLToByteArray(String source) { InputStream inputStream = null; ByteArrayOutputStream byteArrayOutputStream = null; try { inputStream = new URL(source).openConnection().getInputStream(); byteArrayOutputStream = new ByteArrayOutputStream(); IOUtils.copy(inputStream, byteArrayOutputStream); return byteArrayOutputStream.toByteArray(); } catch (MalformedURLException e) { Log.w(TAG, e); } catch (IOException e) { if (Configuration.DEBUGGING) { Log.w(TAG, e); } } catch (OutOfMemoryError e) { Log.w(TAG, e); } finally { IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(byteArrayOutputStream); } return null; } private static void copyBitmapToDiskLruCache(String key, Bitmap bitmap) { Editor editor = null; OutputStream outputStream = null; try { synchronized (sInstance) { if (!getImageCacheDirectory(sInstance.mCacheDirectory).exists()) { /* * We are in an unexpected state, our cache directory was * destroyed without our static instance being destroyed * also. The best thing we can do here is start over. */ sInstance.mDiskLruCache = open(sInstance.mCacheDirectory); } } /* * We block here because Editor.edit will return null if another * edit is in progress */ while (editor == null) { editor = sInstance.mDiskLruCache.edit(key); Thread.sleep(50); } outputStream = editor.newOutputStream(0); bitmap.compress(CompressFormat.PNG, 0, outputStream); } catch (IOException e) { if (Configuration.DEBUGGING) { Log.w(TAG, e); } } catch (InterruptedException e) { if (Configuration.DEBUGGING) { Log.d(TAG, "Thread was interrupted"); } } finally { IOUtils.closeQuietly(outputStream); if (editor != null) { try { editor.commit(); } catch (IOException e) { if (Configuration.DEBUGGING) { Log.w(TAG, e); } } } } } private static Bitmap decodeFromSnapshot(Snapshot snapshot) { InputStream inputStream = null; try { inputStream = snapshot.getInputStream(0); synchronized (LOCK) { return BitmapFactory.decodeStream(inputStream, null, getBitmapFactoryOptions()); } } catch (Throwable t) { Log.w(TAG, t); } finally { IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(snapshot); } return null; } private static boolean verifySourceOverTime(String source, Request request) { if (source != null && request != null) { try { Thread.sleep(300); } catch (InterruptedException e) { if (Configuration.DEBUGGING) { Log.d(TAG, "Thread was interrupted"); } } finally { if (source.equals(request.getSource())) { return true; } } } return false; } private static String getKey(String source) { if (source == null) { return null; } else { return DigestUtils.sha256Hex(source); } } private static Options getBitmapFactoryOptions() { Options options = new Options(); options.inPurgeable = true; options.inInputShareable = true; options.inPreferredConfig = sInstance.mPreferredConfig; return options; } private static File getCacheDirectory(Context context) { File directory; switch (Configuration.DOWNLOAD_LOCATION) { case EXTERNAL: if (Environment.getExternalStorageDirectory() != null && Environment.getExternalStorageDirectory().canWrite()) { directory = new File(Environment.getExternalStorageDirectory().getPath() + "/Android/data/" + context.getApplicationContext().getPackageName() + "/cache"); directory.mkdirs(); } else { directory = context.getCacheDir(); } break; case INTERNAL: default: directory = new File(Environment.getDataDirectory().getAbsolutePath() + "/data/" + context.getPackageName() + "/cache"); break; } return directory; } /** * @hide */ public static ExecutorService newConfiguredThreadPool() { int corePoolSize = 0; int maximumPoolSize = Configuration.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>(Configuration.MEM_CACHE_SIZE_KB * 1024) { @Override public int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; } /** * Listener for being notified when image is retrieved. */ public static interface OnImageReceivedListener { /** * Notification that an image was retrieved, this is guaranteed to be * called on the UI thread. */ public void onImageReceived(String source, Bitmap bitmap); } /** * Interface used to retrieve images remotely, used primarily with * {@link RemoteImageView} for optimization purposes. */ public static interface Request extends OnImageReceivedListener { /** * Returns remote image URL, can be null. */ public String getSource(); } /** * Advanced interface for retrieving images in a non-standard way, this is * still under heavy development and will most likely change in the future. */ public static interface ExtendedRequest extends Request { /** * Used in the processing of images after they are retrieved from the * remote source but before they are cached. */ public Bitmap onPreProcess(Bitmap raw); /** * Used in the resizing of images intelligently. */ public int getHeight(); /** * Used in the resizing of images intelligently. */ public int getWidth(); } private static class SimpleRequest implements ExtendedRequest { private Request mRequest; public SimpleRequest(Request request) { mRequest = request; } @Override public void onImageReceived(String source, Bitmap bitmap) { mRequest.onImageReceived(source, bitmap); } @Override public String getSource() { return mRequest.getSource(); } @Override public Bitmap onPreProcess(Bitmap raw) { return raw; } @Override public int getHeight() { return 0; } @Override public int getWidth() { return 0; } } /** * Create thread with low priority for use {@link java.util.concurrent.Executor}. * * @author Tomáš Procházka <<a href="mailto:tomas.prochazka@inmite.eu">tomas.prochazka@inmite.eu</a>> * @version $Revision: 0$ ($Date: 22.6.2012 15:07:18$) * * @hide */ public static class LowPriorityThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; private final int priority; public LowPriorityThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "lp-pool-" + poolNumber.getAndIncrement() + "-thread-"; priority = Thread.MIN_PRIORITY + 1; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != priority) t.setPriority(priority); return t; } } }