package com.mixpanel.android.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v4.util.LruCache;
import android.util.Base64;
import com.mixpanel.android.mpmetrics.MPConfig;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.SSLSocketFactory;
/**
* ABSOLUTELY NOT thread, or even process safe.
* Writes and reads files and directories at known paths, and uses a shared instance of MessageDigest.
*/
public class ImageStore {
public static class CantGetImageException extends Exception {
public CantGetImageException(String message) {
super(message);
}
public CantGetImageException(String message, Throwable cause) {
super(message, cause);
}
}
public ImageStore(Context context, String moduleName) {
this(context, DEFAULT_DIRECTORY_PREFIX + moduleName, new HttpService());
}
public ImageStore(Context context, String directoryName, RemoteService poster) {
mDirectory = context.getDir(directoryName, Context.MODE_PRIVATE);
mPoster = poster;
mConfig = MPConfig.getInstance(context);
MessageDigest useDigest;
try {
useDigest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
MPLog.w(LOGTAG, "Images won't be stored because this platform doesn't supply a SHA1 hash function");
useDigest = null;
}
mDigest = useDigest;
if (sMemoryCache == null) {
synchronized (ImageStore.class) {
if (sMemoryCache == null) {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / mConfig.getImageCacheMaxMemoryFactor();
sMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
}
}
}
public File getImageFile(String url) throws CantGetImageException {
final File file = storedFile(url);
byte[] bytes = null;
if (file == null || !file.exists()) {
try {
final SSLSocketFactory factory = mConfig.getSSLSocketFactory();
bytes = mPoster.performRequest(url, null, factory);
} catch (IOException e) {
throw new CantGetImageException("Can't download bitmap", e);
} catch (RemoteService.ServiceUnavailableException e) {
throw new CantGetImageException("Couldn't download image due to service availability", e);
}
if (null != bytes) {
if (null != file && bytes.length < MAX_BITMAP_SIZE) {
OutputStream out = null;
try {
out = new FileOutputStream(file);
out.write(bytes);
} catch (FileNotFoundException e) {
throw new CantGetImageException("It appears that ImageStore is misconfigured, or disk storage is unavailable- can't write to bitmap directory", e);
} catch (IOException e) {
throw new CantGetImageException("Can't store bitmap", e);
} finally {
if (null != out) {
try {
out.close();
} catch (IOException e) {
MPLog.w(LOGTAG, "Problem closing output file", e);
}
}
}
}
}
}
return file;
}
public Bitmap getImage(String url) throws CantGetImageException {
Bitmap cachedBitmap = getBitmapFromMemCache(url);
if (cachedBitmap == null) {
final File imageFile = getImageFile(url);
cachedBitmap = decodeImage(imageFile);
addBitmapToMemoryCache(url, cachedBitmap);
}
return cachedBitmap;
}
private static Bitmap decodeImage(File file) throws CantGetImageException {
BitmapFactory.Options option = new BitmapFactory.Options();
option.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), option);
float imageSize = (float) option.outHeight * option.outWidth;
if (imageSize > getAvailableMemory()) {
throw new CantGetImageException("Do not have enough memory for the image");
}
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
if (null == bitmap) {
final boolean ignored = file.delete();
throw new CantGetImageException("Bitmap on disk can't be opened or was corrupt");
}
return bitmap;
}
private static float getAvailableMemory() {
Runtime runtime = Runtime.getRuntime();
float used = runtime.totalMemory() - runtime.freeMemory(); // used = heap - free
return runtime.maxMemory() - used; // available = max - used
}
public void clearStorage() {
File[] files = mDirectory.listFiles();
int length = files.length;
for (int i = 0; i < length; i++) {
final File file = files[i];
final String filename = file.getName();
if (filename.startsWith(FILE_PREFIX)) {
final boolean ignored = file.delete();
}
}
clearMemCache();
}
public void deleteStorage(String url) {
final File file = storedFile(url);
if (null != file) {
final boolean ignored = file.delete();
removeBitmapFromMemCache(url);
}
}
private File storedFile(String url) {
if (null == mDigest) {
return null;
}
final byte[] hashed = mDigest.digest(url.getBytes());
final String safeName = FILE_PREFIX + Base64.encodeToString(hashed, Base64.URL_SAFE | Base64.NO_WRAP);
return new File(mDirectory, safeName);
}
public static void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
synchronized (sMemoryCache) {
sMemoryCache.put(key, bitmap);
}
}
}
public static Bitmap getBitmapFromMemCache(String key) {
synchronized (sMemoryCache) {
return sMemoryCache.get(key);
}
}
public static void removeBitmapFromMemCache(String key) {
synchronized (sMemoryCache) {
sMemoryCache.remove(key);
}
}
public static void clearMemCache() {
synchronized (sMemoryCache) {
sMemoryCache.evictAll();
}
}
private final File mDirectory;
private final RemoteService mPoster;
private final MessageDigest mDigest;
private final MPConfig mConfig;
private static LruCache<String, Bitmap> sMemoryCache;
private static final String DEFAULT_DIRECTORY_PREFIX = "MixpanelAPI.Images.";
private static final int MAX_BITMAP_SIZE = 10000000; // 10 MB
private static final String FILE_PREFIX = "MP_IMG_";
@SuppressWarnings("unused")
private static final String LOGTAG = "MixpanelAPI.ImageStore";
}