/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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.aincc.lib.util;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.Environment;
import com.aincc.lib.io.DiskLruCache;
/**
* A simple disk LRU bitmap cache to illustrate how a disk cache would be used for bitmap caching. A
* much more robust and efficient disk LRU cache solution can be found in the ICS source code
* (libcore/luni/src/main/java/libcore/io/DiskLruCache.java) and is preferable to this simple
* implementation.
*/
public class BitmapDiskLruCache
{
private static final String TAG = "DiskLruCache";
private static final String CACHE_FILENAME_PREFIX = "cache_";
private static final int MAX_REMOVALS = 4;
private static final int INITIAL_CAPACITY = 32;
private static final float LOAD_FACTOR = 0.75f;
private final File mCacheDir;
private int cacheSize = 0;
private int cacheByteSize = 0;
private final int maxCacheItemSize = 64; // 64 item default
private long maxCacheByteSize = 1024 * 1024 * 5; // 5MB default
private CompressFormat mCompressFormat = CompressFormat.JPEG;
private int mCompressQuality = 70;
private final Map<String, String> mLinkedHashMap = Collections.synchronizedMap(new LinkedHashMap<String, String>(INITIAL_CAPACITY, LOAD_FACTOR, true));
// 2012.08.20 aincc : add for using DiskLruCache
private static final int APP_VERSION = 1;
private static final int VALUE_COUNT = 1;
private DiskLruCache cache;
/**
* A filename filter to use to identify the cache filenames which have CACHE_FILENAME_PREFIX
* prepended.
*/
@SuppressWarnings("unused")
private static final FilenameFilter cacheFileFilter = new FilenameFilter()
{
@Override
public boolean accept(File dir, String filename)
{
return filename.startsWith(CACHE_FILENAME_PREFIX);
}
};
/**
* Used to fetch an instance of DiskLruCache.
*
* @param context
* @param cacheDir
* @param maxByteSize
* @return
*/
public static BitmapDiskLruCache openCache(Context context, File cacheDir, long maxByteSize)
{
if (!cacheDir.exists())
{
cacheDir.mkdir();
}
if (cacheDir.isDirectory() && cacheDir.canWrite() && Utils.getUsableSpace(cacheDir) > maxByteSize)
{
return new BitmapDiskLruCache(cacheDir, maxByteSize);
}
return null;
}
/**
* Constructor that should not be called directly, instead use {@link BitmapDiskLruCache#openCache(Context, File, long)} which runs some extra checks before
* creating a DiskLruCache instance.
*
* @param cacheDir
* @param maxByteSize
* @throws IOException
*/
private BitmapDiskLruCache(File cacheDir, long maxByteSize)
{
mCacheDir = cacheDir;
maxCacheByteSize = maxByteSize;
// 2012.08.20 aincc
try
{
cache = DiskLruCache.open(cacheDir, APP_VERSION, VALUE_COUNT, maxByteSize);
}
catch (IOException e)
{
e.printStackTrace();
}
}
/**
* Add a bitmap to the disk cache.
*
* @param key
* A unique identifier for the bitmap.
* @param data
* The bitmap to store.
*/
public void put(String key, Bitmap data)
{
// 2012.08.20 aincc
DiskLruCache.Editor editor = null;
Logger.d1(TAG, "try image put on disk cache " + key);
String keypath = getKeyPath(key);
try
{
// 기존의 파일로 저장하기 위한 로직을 키값으로 사용한다.
editor = cache.edit(keypath);
if (null == editor)
{
return;
}
if (writeBitmapToFile(data, editor))
{
cache.flush();
editor.commit();
Logger.d1(TAG, "image put on disk cache " + keypath);
}
else
{
editor.abort();
Logger.d1(TAG, "ERROR on : image put on disk cache " + keypath);
}
}
catch (IOException e)
{
e.printStackTrace();
Logger.e1(TAG, "ERROR on : image put on disk cache " + keypath);
try
{
if (null != editor)
{
editor.abort();
}
}
catch (IOException ignored)
{
}
}
// SIMPLE VERSION
// synchronized (mLinkedHashMap)
// {
// if (mLinkedHashMap.get(key) == null)
// {
// try
// {
// final String file = createFilePath(mCacheDir, key);
// if (writeBitmapToFile(data, file))
// {
// put(key, file);
// flushCache();
// }
// }
// catch (final FileNotFoundException e)
// {
// Log.e(TAG, "Error in put: " + e.getMessage());
// }
// catch (final IOException e)
// {
// Log.e(TAG, "Error in put: " + e.getMessage());
// }
// }
// }
}
@SuppressWarnings("unused")
private void put(String key, String file)
{
mLinkedHashMap.put(key, file);
cacheSize = mLinkedHashMap.size();
cacheByteSize += new File(file).length();
}
/**
* Flush the cache, removing oldest entries if the total size is over the specified cache size.
* Note that this isn't keeping track of stale files in the cache directory that aren't in the
* HashMap. If the images and keys in the disk cache change often then they probably won't ever
* be removed.
*/
@SuppressWarnings("unused")
private void flushCache()
{
Entry<String, String> eldestEntry;
File eldestFile;
long eldestFileSize;
int count = 0;
while (count < MAX_REMOVALS && (cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize))
{
eldestEntry = mLinkedHashMap.entrySet().iterator().next();
eldestFile = new File(eldestEntry.getValue());
eldestFileSize = eldestFile.length();
mLinkedHashMap.remove(eldestEntry.getKey());
eldestFile.delete();
cacheSize = mLinkedHashMap.size();
cacheByteSize -= eldestFileSize;
count++;
Logger.d1(TAG, "flushCache - Removed cache file, " + eldestFile + ", " + eldestFileSize);
}
}
/**
* Get an image from the disk cache.
*
* @param key
* The unique identifier for the bitmap
* @return The bitmap or null if not found
*/
public Bitmap get(String key)
{
// 2012.08.20 aincc
Bitmap bitmap = null;
DiskLruCache.Snapshot snapshot = null;
String keypath = getKeyPath(key);
try
{
snapshot = cache.get(keypath);
if (null == snapshot)
{
return null;
}
final InputStream in = snapshot.getInputStream(0);
if (null != in)
{
final BufferedInputStream buffIn = new BufferedInputStream(in, Utils.IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(buffIn);
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
if (null != snapshot)
{
snapshot.close();
}
}
Logger.d1(TAG, null == bitmap ? "" : "image read from disk " + keypath);
return bitmap;
// SIMPLE VERSION
// synchronized (mLinkedHashMap)
// {
// final String file = mLinkedHashMap.get(key);
// if (file != null)
// {
// Logger.d1(TAG, "Disk cache hit");
// return BitmapFactory.decodeFile(file);
// }
// else
// {
// final String existingFile = createFilePath(mCacheDir, key);
// if (new File(existingFile).exists())
// {
// put(key, existingFile);
// Logger.d1(TAG, "Disk cache hit (existing file)");
// return BitmapFactory.decodeFile(existingFile);
// }
// }
// return null;
// }
}
/**
* Checks if a specific key exist in the cache.
*
* @param key
* The unique identifier for the bitmap
* @return true if found, false otherwise
*/
public boolean containsKey(String key)
{
// 2012.08.20 aincc
boolean contained = false;
DiskLruCache.Snapshot snapshot = null;
String keypath = getKeyPath(key);
try
{
snapshot = cache.get(keypath);
contained = null != snapshot;
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
if (null != snapshot)
{
snapshot.close();
}
}
return contained;
// SIMPLE VERSION
// // See if the key is in our HashMap
// if (mLinkedHashMap.containsKey(key))
// {
// return true;
// }
//
// // Now check if there's an actual file that exists based on the key
// final String existingFile = createFilePath(mCacheDir, key);
// if (new File(existingFile).exists())
// {
// // File found, add it to the HashMap for future use
// put(key, existingFile);
// return true;
// }
// return false;
}
/**
* Removes all disk cache entries from this instance cache dir
*/
public void clearCache()
{
// 2012.08.20 aincc
Logger.d1(TAG, "disk cache CLEAR");
try
{
cache.delete();
}
catch (IOException e)
{
e.printStackTrace();
}
// SIMPLE VERSION
// BitmapDiskLruCache.clearCache(mCacheDir);
}
/**
* Removes all disk cache entries from the application cache directory in the uniqueName
* sub-directory.
*
* @param context
* The context to use
* @param uniqueName
* A unique cache directory name to append to the app cache directory
*/
public static void clearCache(Context context, String uniqueName) throws IOException
{
Logger.d1(TAG, "disk cache CLEAR " + uniqueName);
File cacheDir = getDiskCacheDir(context, uniqueName);
clearCache(cacheDir);
}
/**
* Removes all disk cache entries from the given directory. This should not be called directly,
* call {@link BitmapDiskLruCache#clearCache(Context, String)} or {@link BitmapDiskLruCache#clearCache()} instead.
*
* @param cacheDir
* The directory to remove the cache files from
*/
private static void clearCache(File cacheDir) throws IOException
{
// 2012.08.20 aincc
File[] files = cacheDir.listFiles();
if (files == null)
{
throw new IllegalArgumentException("not a directory: " + cacheDir);
}
for (File file : files)
{
if (file.isDirectory())
{
clearCache(file);
}
if (!file.delete())
{
// 2012.08.30 aincc : 실패해도 다른 파일을 삭제하기 위해 예외 발생 주석처리.
// throw new IOException("failed to delete file: " + file);
}
}
// SIMPLE VERSION
// final File[] files = cacheDir.listFiles(cacheFileFilter);
// for (int i = 0; i < files.length; i++)
// {
// files[i].delete();
// }
}
/**
* Get a usable cache directory (external if available, internal otherwise).
*
* @param context
* The context to use
* @param uniqueName
* A unique directory name to append to the cache dir
* @return The cache dir
*/
public static File getDiskCacheDir(Context context, String uniqueName)
{
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED || !Utils.isExternalStorageRemovable() ? Utils.getExternalCacheDir(context).getPath() : context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
/**
* Creates a constant cache file path given a target cache directory and an image key.
*
* @param cacheDir
* @param key
* @return
*/
public static String createFilePath(File cacheDir, String key)
{
try
{
// Use URLEncoder to ensure we have a valid filename, a tad hacky but it will do for
// this example
return cacheDir.getAbsolutePath() + File.separator + CACHE_FILENAME_PREFIX + URLEncoder.encode(key.replace("*", ""), "UTF-8");
}
catch (final UnsupportedEncodingException e)
{
Logger.e1(TAG, "createFilePath - " + e);
}
return null;
}
/**
* Create a constant cache file path using the current cache directory and an image key.
*
* @param key
* @return
*/
public String createFilePath(String key)
{
return createFilePath(mCacheDir, key);
}
/**
*
* @since 1.0.0
* @param key
* @return
*/
public String getKeyPath(String key)
{
try
{
// Use URLEncoder to ensure we have a valid filename, a tad hacky but it will do for
// this example
return URLEncoder.encode(key.replace("*", ""), "UTF-8");
}
catch (final UnsupportedEncodingException e)
{
Logger.e1(TAG, "createFilePath - " + e);
}
return null;
}
/**
* Sets the target compression format and quality for images written to the disk cache.
*
* @param compressFormat
* @param quality
*/
public void setCompressParams(CompressFormat compressFormat, int quality)
{
mCompressFormat = compressFormat;
mCompressQuality = quality;
}
/**
* Writes a bitmap to a file. Call {@link BitmapDiskLruCache#setCompressParams(CompressFormat, int)} first to set the target bitmap compression and format.
*
* @param bitmap
* @param file
* @return
*/
@SuppressWarnings("unused")
private boolean writeBitmapToFile(Bitmap bitmap, String file) throws IOException, FileNotFoundException
{
OutputStream out = null;
try
{
out = new BufferedOutputStream(new FileOutputStream(file), Utils.IO_BUFFER_SIZE);
return bitmap.compress(mCompressFormat, mCompressQuality, out);
}
finally
{
if (out != null)
{
out.close();
}
}
}
/**
* Writes a bitmap to a file. Call {@link BitmapDiskLruCache#setCompressParams(CompressFormat, int)} first to set the target bitmap compression and format.
*
* @param bitmap
* @param file
* @return
*/
private boolean writeBitmapToFile(Bitmap bitmap, DiskLruCache.Editor editor) throws IOException, FileNotFoundException
{
OutputStream out = null;
try
{
out = new BufferedOutputStream(editor.newOutputStream(0), Utils.IO_BUFFER_SIZE);
return bitmap.compress(mCompressFormat, mCompressQuality, out);
}
finally
{
if (out != null)
{
out.close();
}
}
}
}