FileImageCache.java
/*
* Copyright 2016 the Cook-E development team
*
* This file is part of Cook-E.
*
* Cook-E is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Cook-E is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Cook-E. If not, see <http://www.gnu.org/licenses/>.
*/
package org.cook_e.data;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import org.cook_e.cook_e.App;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Locale;
import java.util.zip.CRC32;
/**
* An image cache that uses files
*/
public class FileImageCache implements ImageCache {
/**
* An OutputStream that calculates the CRC32 checksum of the bytes written to it
*/
private static class CRCOutputStream extends FilterOutputStream {
/**
* The CRC calculator
*/
private CRC32 mCrc;
/**
* Creates a CRCOutputStream that wraps another stream
* @param inner the stream to wrap. Must not be null.
*/
public CRCOutputStream(OutputStream inner) {
super(inner);
mCrc = new CRC32();
}
/**
* Writes a byte to the underlying stream and updates the CRC32 checksum to include
* the written byte.
*
* If the underlying stream throws an exception, the checksum is not updated.
*
* @param oneByte the byte to write
* @throws IOException if an error occurred writing the byte
*/
@Override
public void write(int oneByte) throws IOException {
super.write(oneByte);
mCrc.update(oneByte);
}
/**
* Returns the CRC32 checksum of the bytes written so far
* @return the checksum
*/
public long getCrc() {
return mCrc.getValue();
}
}
/**
* The tag used for logging
*/
private static final String TAG = FileImageCache.class.getSimpleName();
/**
* The name of the directory in which images are stored
*/
private static final String DIRECTORY_NAME = "image_cache";
/**
* The format used for compression
*/
private static final Bitmap.CompressFormat FORMAT = Bitmap.CompressFormat.PNG;
/**
* The compression quality level, 0-100 (ignored for PNG)
*/
private static final int QUALITY = 100;
/**
* The instance
*/
private static ImageCache instance;
/**
* The context used to access storage
*/
private final Context mContext;
/**
* The directory in which files are stored
*/
private final File mCacheDir;
@Override
public long put(Bitmap image) throws CacheException {
File imageFile = null;
try {
// Compress image into a byte array to calculate the CRC
final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
final CRCOutputStream crc = new CRCOutputStream(bytes);
final boolean result = image.compress(FORMAT, QUALITY, crc);
if (!result) {
throw new CacheException("Failed to compress image");
}
final long crcResult = crc.getCrc();
// Write image
imageFile = pathForCrc(crcResult);
final FileOutputStream fileStream = new FileOutputStream(imageFile);
fileStream.write(bytes.toByteArray());
return crcResult;
}
catch (IOException e) {
// Try to delete file, ignore errors
//noinspection ResultOfMethodCallIgnored
imageFile.delete();
throw new CacheException("Failed to write image", e);
}
}
@Override
public Bitmap get(long id) throws CacheException {
final File imageFile = pathForCrc(id);
if (!imageFile.exists()) {
return null;
}
return BitmapFactory.decodeFile(imageFile.getPath());
}
@Override
public Bitmap remove(long id) throws CacheException {
final File imageFile = pathForCrc(id);
if (!imageFile.exists()) {
return null;
}
final Bitmap loadedImage = BitmapFactory.decodeFile(imageFile.getPath());
final boolean success = imageFile.delete();
if (!success) {
throw new CacheException("Failed to delete file " + imageFile.getPath());
}
return loadedImage;
}
@Override
public void clear() throws CacheException {
for (File file : mCacheDir.listFiles()) {
final boolean success = file.delete();
if (!success) {
throw new CacheException("Failed to delete file " + file.getPath());
}
}
}
/**
* Returns a File for an image with the specified CRC checksum
* @param crc the checksum
* @return a File
*/
private File pathForCrc(long crc) {
return new File(String.format(Locale.US, "%s%s%016x.png", mCacheDir.getPath(), File.separator, crc));
}
/**
* Ensures that the storage directory exists, and returns a File that represents it
* @return the storage directory as a File
*/
private File getStorageDirectory() throws IOException {
final File dir = new File(
mContext.getCacheDir().getPath() + File.separator + DIRECTORY_NAME);
// Ensure directory exists and is a directory
if (dir.exists()) {
if (!dir.isDirectory()) {
final boolean success = dir.delete();
if (!success) {
throw new IOException("Could not delete non-directory file " + dir.getPath());
}
}
}
else {
// Does not exist
// Create as directory
final boolean success = dir.mkdirs();
if (!success) {
throw new IOException("Could not create directory " + dir.getPath());
}
}
return dir;
}
/**
* Creates a new cache
* @param context the context to use to access storage. Must not be null.
*/
private FileImageCache(Context context) throws IOException {
Objects.requireNonNull(context, "context must not be null");
mContext = context;
mCacheDir = getStorageDirectory();
}
/**
* Returns the ImageCache used by the application
* @return an ImageCache
*/
public static ImageCache getInstance() {
if (instance == null) {
try {
instance = new FileImageCache(App.getAppContext());
} catch (IOException e) {
// No ideal way to handle this
Log.e(TAG, "Failed to set up image cache directory", e);
return null;
}
}
return instance;
}
}