package com.android.volley.cache;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.util.Log;
import com.android.volley.BuildConfig;
import com.android.volley.Cache;
import com.android.volley.VolleyLog;
import com.android.volley.cache.DiskBasedCache.CacheHeader;
import com.android.volley.misc.CountingInputStream;
import com.android.volley.misc.DiskLruCache;
import com.android.volley.misc.IOUtils;
import com.android.volley.misc.ImageUtils;
import com.android.volley.misc.Utils;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Cache implementation that caches files directly onto the hard disk in the specified directory
* using DiskLruCache
*/
public class DiskLruBasedCache implements Cache {
private static final String TAG = "DiskLruImageCache";
// Default memory cache size in kilobytes
private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5MB
// Default disk cache size in bytes
private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
// Constants to easily toggle various caches
private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
// Compression settings when writing images to disk cache
private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
private static final int DEFAULT_COMPRESS_QUALITY = 70;
private static final int DISK_CACHE_INDEX = 0;
private static final int APP_VERSION = 1;
private static final int VALUE_COUNT = 1;
private DiskLruCache mDiskLruCache;
@SuppressWarnings("unused")
private CompressFormat mCompressFormat = DEFAULT_COMPRESS_FORMAT;
@SuppressWarnings("unused")
private static int IO_BUFFER_SIZE = 8 * 1024;
@SuppressWarnings("unused")
private int mCompressQuality = DEFAULT_COMPRESS_QUALITY;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private ImageCacheParams mCacheParams;
public DiskLruBasedCache(File root) {
mCacheParams = new ImageCacheParams(root);
}
public DiskLruBasedCache(ImageCacheParams cacheParams) {
mCacheParams = cacheParams;
}
public void putBitmap(String data, Bitmap value) {
if (data == null || value == null) {
return;
}
synchronized (mDiskCacheLock) {
// Add to disk cache
if (mDiskLruCache != null) {
final String key = hashKeyForDisk(data);
OutputStream out = null;
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot == null) {
final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
value.compress(
mCacheParams.compressFormat, mCacheParams.compressQuality, out);
editor.commit();
out.close();
}
} else {
snapshot.getInputStream(DISK_CACHE_INDEX).close();
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
} catch (Exception e) {
Log.e(TAG, "addBitmapToCache - " + e);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {}
}
}
}
}
public Bitmap getBitmap(String data) {
final String key = hashKeyForDisk(data);
Bitmap bitmap = null;
synchronized (mDiskCacheLock) {
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
InputStream inputStream = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit");
}
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (inputStream != null) {
FileDescriptor fd = ((FileInputStream) inputStream).getFD();
// Decode bitmap, but we don't want to sample so give
// MAX_VALUE as the target dimensions
bitmap = ImageUtils.decodeSampledBitmapFromDescriptor(fd, Integer.MAX_VALUE, Integer.MAX_VALUE);
}
}
} catch (final IOException e) {
Log.e(TAG, "getBitmapFromDiskCache - " + e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {}
}
}
return bitmap;
}
}
public boolean containsKey(String key) {
boolean contained = false;
DiskLruCache.Snapshot snapshot = null;
try {
snapshot = mDiskLruCache.get(key);
contained = snapshot != null;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (snapshot != null) {
snapshot.close();
}
}
return contained;
}
public void clearCache() {
if (BuildConfig.DEBUG) {
Log.d("cache_test_DISK_", "disk cache CLEARED");
}
try {
mDiskLruCache.delete();
} catch (IOException e) {
e.printStackTrace();
}
}
public File getCacheFolder() {
return mDiskLruCache.getDirectory();
}
/**
* Initializes the disk cache. Note that this includes disk access so this should not be
* executed on the main/UI thread. By default an ImageCache does not initialize the disk
* cache when it is created, instead you should call initDiskCache() to initialize it on a
* background thread.
*/
public void initDiskCache() {
// Set up disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
File diskCacheDir = mCacheParams.diskCacheDir;
if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (Utils.getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, APP_VERSION, VALUE_COUNT, mCacheParams.diskCacheSize);
if (BuildConfig.DEBUG) {
VolleyLog.d("Disk cache initialized");
}
} catch (final IOException e) {
mCacheParams.diskCacheDir = null;
VolleyLog.e("initDiskCache - " + e);
}
}
}
}
mDiskCacheStarting = false;
mDiskCacheLock.notifyAll();
}
}
/**
* A holder class that contains cache parameters.
*/
public static class ImageCacheParams {
public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
public File diskCacheDir;
public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
public int compressQuality = DEFAULT_COMPRESS_QUALITY;
public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
/**
* Create a set of image cache parameters that can be provided to
* @param maxCacheSizeInBytes cache size in bytes.
*/
public ImageCacheParams(File rootDirectory, int maxCacheSizeInBytes) {
diskCacheDir = rootDirectory;
memCacheSize = maxCacheSizeInBytes;
}
/**
* Create a set of image cache parameters
* @param context A context to use.
* @param rootDirectory A unique subdirectory name that will be appended to the
* application cache directory. Usually "cache" or "images"
* is sufficient.
* @param maxCacheSizeInBytes cache size in bytes.
*/
public ImageCacheParams(Context context, String rootDirectory, int maxCacheSizeInBytes) {
diskCacheDir = Utils.getDiskCacheDir(context, rootDirectory);
memCacheSize = maxCacheSizeInBytes;
}
/**
* Create a set of image cache parameters
* @param context A context to use.
* @param rootDirectory A unique subdirectory name that will be appended to the
* application cache directory. Usually "cache" or "images"
* is sufficient.
*/
public ImageCacheParams(Context context, String rootDirectory) {
diskCacheDir = Utils.getDiskCacheDir(context, rootDirectory);
}
/**
* Create a set of image cache parameters
* @param rootDirectory A unique subdirectory name that will be appended to the
* application cache directory. Usually "cache" or "images"
* is sufficient.
*/
public ImageCacheParams(File rootDirectory) {
diskCacheDir = rootDirectory;
}
/**
* Sets the memory cache size based on a percentage of the max available VM memory.
* Eg. setting percent to 0.2 would set the memory cache to one fifth of the available
* memory. Throws {@link IllegalArgumentException} if percent is < 0.01 or > .8.
* memCacheSize is stored in kilobytes instead of bytes as this will eventually be passed
* to construct a LruCache which takes an int in its constructor.
*
* This value should be chosen carefully based on a number of factors
* Refer to the corresponding Android Training class for more discussion:
* http://developer.android.com/training/displaying-bitmaps/
*
* @param percent Percent of available app memory to use to size memory cache
*/
public void setMemCacheSizePercent(float percent) {
if (percent < 0.01f || percent > 0.8f) {
throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
+ "between 0.01 and 0.8 (inclusive)");
}
memCacheSize = Math.round(percent * Runtime.getRuntime().maxMemory() / 1024);
}
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable for using as a
* disk filename.
*/
public static String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private static String bytesToHexString(byte[] bytes) {
// http://stackoverflow.com/questions/332079
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* Creates a pseudo-unique filename for the specified cache key.
* @param key The key to generate a file name for.
* @return A pseudo-unique filename.
*/
@SuppressWarnings("unused")
private String getFilenameForKey(String key) {
int firstHalfLength = key.length() / 2;
String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
return localFilename;
}
/**
* Returns a file object for the given cache key.
*/
public File getFileForKey(String key) {
return new File(mCacheParams.diskCacheDir, key+".0");
}
@Override
public Entry get(String data) {
final String key = hashKeyForDisk(data);
// if the entry does not exist, return.
if (data == null) {
return null;
}
synchronized (mDiskCacheLock) {
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
InputStream inputStream = null;
File file = getFileForKey(key);
try {
final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit");
}
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (inputStream != null) {
CountingInputStream cis = new CountingInputStream(inputStream);
CacheHeader entry = CacheHeader.readHeader(cis); // eat header
byte[] dataBytes = IOUtils.streamToBytes(cis, (int) (file.length() - cis.getBytesRead()));
return entry.toCacheEntry(dataBytes);
}
}
} catch (final IOException e) {
remove(key);
Log.e(TAG, "getDiskLruBasedCache - " + e);
return null;
} catch (OutOfMemoryError e) {
VolleyLog.e("Caught OOM for %d byte image, path=%s: %s", file.length(), file.getAbsolutePath(), e.toString());
return null;
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {}
}
}
}
return null;
}
@Override
public void put(String data, Entry value) {
if (data == null || value == null) {
return;
}
synchronized (mDiskCacheLock) {
// Add to disk cache
if (mDiskLruCache != null) {
final String key = hashKeyForDisk(data);
OutputStream out = null;
try {
//DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
//if (snapshot == null) {
final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
CacheHeader e = new CacheHeader(key, value);
e.writeHeader(out);
out.write(value.data);
editor.commit();
out.close();
}
/* } else {
snapshot.getInputStream(DISK_CACHE_INDEX).close();
}*/
} catch (final IOException e) {
Log.e(TAG, "putDiskLruBasedCache - " + e);
} catch (Exception e) {
Log.e(TAG, "putDiskLruBasedCache - " + e);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {}
}
}
}
}
@Override
public void initialize() {
initDiskCache();
}
@Override
public void invalidate(String key, boolean fullExpire) {
Entry entry = get(key);
if (entry != null) {
entry.softTtl = -1;
if (fullExpire) {
entry.ttl = -1;
}
put(key, entry);
}
}
@Override
public void remove(String data) {
if (data == null) {
return;
}
synchronized (mDiskCacheLock) {
// remove to disk cache
if (mDiskLruCache != null) {
final String key = hashKeyForDisk(data);
try {
mDiskLruCache.remove(key);
} catch (final IOException e) {
Log.e(TAG, "removeDiskLruBasedCache - " + e);
} catch (Exception e) {
Log.e(TAG, "removeDiskLruBasedCache - " + e);
}
}
}
}
@Override
public void clear() {
synchronized (mDiskCacheLock) {
mDiskCacheStarting = true;
if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
try {
mDiskLruCache.delete();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache cleared");
}
} catch (IOException e) {
Log.e(TAG, "clearCache - " + e);
}
mDiskLruCache = null;
initDiskCache();
}
}
}
/**
* Flushes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void flush() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
mDiskLruCache.flush();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache flushed");
}
} catch (IOException e) {
Log.e(TAG, "flush - " + e);
}
}
}
}
/**
* Closes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void close() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
if (!mDiskLruCache.isClosed()) {
mDiskLruCache.close();
mDiskLruCache = null;
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache closed");
}
}
} catch (IOException e) {
Log.e(TAG, "close - " + e);
}
}
}
}
}