package com.tomclaw.mandarin.core;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.LruCache;
import com.tomclaw.mandarin.main.views.LazyImageView;
import com.tomclaw.mandarin.util.BitmapHelper;
import com.tomclaw.mandarin.util.Logger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Set;
/**
* Created with IntelliJ IDEA.
* User: solkin
* Date: 12/5/13
* Time: 1:32 AM
*/
public class BitmapCache {
private static class Holder {
static BitmapCache instance = new BitmapCache();
}
public static BitmapCache getInstance() {
return Holder.instance;
}
private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.PNG;
public static final int BITMAP_SIZE_UNKNOWN = -1;
public static final int BITMAP_SIZE_ORIGINAL = 0;
private static final String BITMAP_CACHE_FOLDER = "bitmaps";
private File path;
private LruCache<String, Bitmap> bitmapLruCache;
private int densityDpi;
public BitmapCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory());
// Use 1/12th of the available memory for this memory cache.
int cacheSize = maxMemory / 12;
bitmapLruCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
public void init(Context context) {
path = context.getDir(BITMAP_CACHE_FOLDER, Context.MODE_PRIVATE);
densityDpi = context.getResources().getDisplayMetrics().densityDpi;
}
private static String getCacheKey(String hash, int width, int height) {
return hash + "_" + width + "_" + height;
}
private static boolean isCacheKeyFromHash(String cacheKey, String hash) {
return !TextUtils.isEmpty(cacheKey) && !TextUtils.isEmpty(hash) && cacheKey.startsWith(hash + "_");
}
/**
* Setup required image by hash on specified image view in background thread.
* While image loading from disk cache and scaling, on image view will be placed default resource.
*
* @param imageView - image view to show image
* @param hash - required image hash
* @param defaultResource - default resource to show while original image being loaded and scaled
* @param original - if false, image will be cached with specified imageView size,
* if true original size image will be used
*/
public void getBitmapAsync(LazyImageView imageView, final String hash, int defaultResource, boolean original) {
int width, height;
if (original) {
width = height = BITMAP_SIZE_ORIGINAL;
} else {
width = height = BITMAP_SIZE_UNKNOWN;
}
getBitmapAsync(imageView, hash, defaultResource, width, height);
}
/**
* Setup required image by hash on specified image view in background thread.
* While image loading from disk cache and scaling, on image view will be placed default resource.
*
* @param imageView - image view to show image
* @param hash - required image hash
* @param defaultResource - default resource to show while original image being loaded and scaled
* @param width - desired image width
* @param height - desired image height
*/
public void getBitmapAsync(LazyImageView imageView, final String hash, int defaultResource, int width, int height) {
if (width == BITMAP_SIZE_UNKNOWN && height == BITMAP_SIZE_UNKNOWN) {
width = imageView.getWidth();
height = imageView.getHeight();
}
Bitmap bitmap = getBitmapSyncFromCache(hash, width, height);
// Checking for there is no cached bitmap and reset is really required.
if (TextUtils.isEmpty(hash) || (bitmap == null && BitmapTask.isResetRequired(imageView, hash))) {
imageView.setPlaceholder(defaultResource);
}
imageView.setHash(hash);
if (!TextUtils.isEmpty(hash)) {
// Checking for bitmap cached or not.
if (bitmap == null) {
TaskExecutor.getInstance().execute(new BitmapTask(imageView, hash, width, height));
} else {
imageView.setBitmap(bitmap);
}
}
}
public void getThumbnailAsync(LazyImageView imageView, String hash, long imageId, int placeholder) {
int width = imageView.getWidth();
int height = imageView.getHeight();
Bitmap bitmap = getBitmapSyncFromCache(hash, width, height);
// Checking for there is no cached bitmap and reset is really required.
if (bitmap == null && ThumbnailTask.isResetRequired(imageView, hash)) {
imageView.setPlaceholder(placeholder);
}
imageView.setHash(hash);
if (!TextUtils.isEmpty(hash)) {
// Checking for bitmap cached or not.
if (bitmap == null) {
TaskExecutor.getInstance().execute(new ThumbnailTask(imageView, hash, imageId, width, height));
} else {
imageView.setBitmap(bitmap);
}
}
}
public Bitmap getBitmapSyncFromCache(String hash, int width, int height) {
String cacheKey = getCacheKey(hash, width, height);
return bitmapLruCache.get(cacheKey);
}
/**
* Returns bitmap from memory LRU cache or load image
* from disk first if there is no image in memory cache.
* Image of every size will be loaded from disk, scaled and cached!
*
* @param hash - required image hash
* @param width - required width
* @param height - required height
* @param isProportional - proportional scale flag
* @param isAccurate - bitmap may be sampled size or exact width and height
* @return Bitmap or null if such image not found.
*/
public Bitmap getBitmapSync(String hash, int width, int height, boolean isProportional, boolean isAccurate) {
String cacheKey = getCacheKey(hash, width, height);
Bitmap bitmap = bitmapLruCache.get(cacheKey);
if (bitmap == null) {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(getBitmapFilePath(hash));
if (width != BITMAP_SIZE_ORIGINAL && height != BITMAP_SIZE_ORIGINAL) {
bitmap = BitmapHelper.decodeSampledBitmapFromStream(inputStream, width, height);
} else {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
bitmap = BitmapFactory.decodeStream(inputStream, null, options);
// Check and set original size.
if (width == BITMAP_SIZE_ORIGINAL) {
width = bitmap.getWidth();
}
if (height == BITMAP_SIZE_ORIGINAL) {
height = bitmap.getHeight();
}
}
// Checking for exact size is needed.
if (isAccurate) {
// Resize bitmap for the largest size.
if (isProportional) {
if (bitmap.getWidth() > bitmap.getHeight()) {
height = width * bitmap.getHeight() / bitmap.getWidth();
} else if (bitmap.getHeight() > bitmap.getWidth()) {
width = height * bitmap.getWidth() / bitmap.getHeight();
}
}
// Check for bitmap needs to be resized.
if (bitmap.getWidth() != width || bitmap.getHeight() != height) {
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
bitmap.recycle();
bitmap = scaledBitmap;
}
}
cacheBitmap(cacheKey, bitmap);
} catch (FileNotFoundException ignored) {
Logger.log("Bitmap '" + hash + "' not found!");
} catch (Throwable ex) {
Logger.log("Couldn't cache '" + hash + "' bitmap!", ex);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ignored) {
}
}
}
}
return bitmap;
}
public boolean saveBitmapSync(String hash, Bitmap bitmap) {
return saveBitmapSync(hash, bitmap, COMPRESS_FORMAT);
}
public boolean saveBitmapSync(String hash, Bitmap bitmap, Bitmap.CompressFormat compressFormat) {
try {
OutputStream os = new FileOutputStream(getBitmapFilePath(hash));
bitmap.compress(compressFormat, 85, os);
os.flush();
os.close();
return true;
} catch (IOException e) {
// Unable to create file, likely because external storage is
// not currently mounted.
Logger.log("Error writing bitmap: " + hash, e);
}
return false;
}
public void saveBitmapAsync(String hash, Bitmap bitmap, Bitmap.CompressFormat compressFormat) {
TaskExecutor.getInstance().execute(new SaveBitmapTask(hash, bitmap, compressFormat));
}
public void cacheBitmapOriginal(String hash, Bitmap bitmap) {
String cacheKey = getCacheKey(hash, BITMAP_SIZE_ORIGINAL, BITMAP_SIZE_ORIGINAL);
cacheBitmap(cacheKey, bitmap);
}
public void cacheBitmap(String cacheKey, Bitmap bitmap) {
bitmapLruCache.put(cacheKey, bitmap);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public void invalidateHash(String hash) {
// Remove file first.
new File(getBitmapFilePath(hash)).delete();
// Create Lru cache snapshot.
Set<String> cacheKeySet = bitmapLruCache.snapshot().keySet();
// Find and remove cache keys, assigned with specified hash.
for (String cacheKey : cacheKeySet) {
if (isCacheKeyFromHash(cacheKey, hash)) {
bitmapLruCache.remove(cacheKey);
}
}
}
private String getBitmapFilePath(String hash) {
return path.getPath().concat("/").concat(hash).concat(".").concat(COMPRESS_FORMAT.name());
}
/**
* This method converts dp unit to equivalent pixels, depending on device density.
*
* @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels
* @param context Context to get resources and device specific display metrics
* @return A float value to represent px equivalent to dp depending on device density
*/
public static int convertDpToPixel(float dp, Context context) {
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return (int) (dp * metrics.density);
}
public boolean isLowDensity() {
return densityDpi == DisplayMetrics.DENSITY_LOW ||
densityDpi == DisplayMetrics.DENSITY_MEDIUM;
}
public class SaveBitmapTask extends Task {
private String hash;
private Bitmap bitmap;
private Bitmap.CompressFormat compressFormat;
public SaveBitmapTask(String hash, Bitmap bitmap, Bitmap.CompressFormat compressFormat) {
this.hash = hash;
this.bitmap = bitmap;
this.compressFormat = compressFormat;
}
@Override
public void executeBackground() throws Throwable {
saveBitmapSync(hash, bitmap, compressFormat);
}
}
}