/*
* 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;
}
}
}