/*******************************************************************************
* Copyright 2012 Crazywater
*
* 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 de.knufficast.logic;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import de.knufficast.R;
import de.knufficast.events.EventBus;
import de.knufficast.events.NewImageEvent;
import de.knufficast.util.BooleanCallback;
import de.knufficast.util.NetUtil;
import de.knufficast.util.file.FileUtil;
import de.knufficast.util.file.InternalFileUtil;
/**
* Caches downloaded images locally.
*
* @author crazywater
*/
public class ImageCache {
private static final String IMAGECACHE_FILENAME = "imageCache-index";
private static final int MAX_DOWNLOAD_THREADS = 5;
private static final int IMAGE_SIZE = 200;
private final Map<String, String> urlToFile = new HashMap<String, String>();
private final Map<String, BitmapDrawable> imageMap = new HashMap<String, BitmapDrawable>();
private final Set<String> processingUrls = new HashSet<String>();
private final Set<String> blockedUrls = new HashSet<String>();
private final BlockingQueue<Runnable> downloadTaskQueue = new LinkedBlockingQueue<Runnable>();
private final ThreadPoolExecutor downloadExecutor = new ThreadPoolExecutor(1,
MAX_DOWNLOAD_THREADS, 5, TimeUnit.SECONDS, downloadTaskQueue);
private final Context context;
private final EventBus eventBus;
private final NetUtil netUtil;
private final FileUtil fileUtil;
private final FileUtil configFileUtil;
private BitmapDrawable defaultIcon;
public ImageCache(Context context, EventBus eventBus, FileUtil fileUtil) {
this.context = context;
this.eventBus = eventBus;
this.netUtil = new NetUtil(context);
this.fileUtil = fileUtil;
this.configFileUtil = new InternalFileUtil(context);
}
/**
* Get a {@link Drawable} from a URL. Might return the default icon first and
* fire a {@link NewImageEvent} later.
*/
public BitmapDrawable getResource(final String url) {
if (url == null || "".equals(url) || blockedUrls.contains(url)
|| processingUrls.contains(url)) {
return getDefaultIcon();
}
if (urlToFile.containsKey(url)) {
if (imageMap.get(url) == null) {
return insertDrawable(url);
} else {
BitmapDrawable drawable = imageMap.get(url);
// extra-check because bitmaps get recycled accidentally
if (drawable.getBitmap() != null && drawable.getBitmap().isRecycled()) {
imageMap.remove(url);
return insertDrawable(url);
}
return imageMap.get(url);
}
}
if (netUtil.isOnWifi()) {
processingUrls.add(url);
final String filename = "imageCache-file-" + url.hashCode();
FileUtil util = fileUtil;
DownloadTask task = new DownloadTask(util, null,
new BooleanCallback<Void, String>() {
@Override
public void success(Void unused) {
urlToFile.put(url, filename);
save();
insertDrawable(url);
}
@Override
public void fail(String error) {
if (error != DownloadTask.ERROR_CONNECTION
&& error != DownloadTask.ERROR_DATA_RANGE) {
blockedUrls.add(url);
} else {
eventBus.fireEvent(new NewImageEvent(url));
}
processingUrls.remove(url);
}
});
task.executeOnExecutor(downloadExecutor, url, filename);
}
return getDefaultIcon();
}
private BitmapDrawable insertDrawable(final String url) {
processingUrls.add(url);
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
try {
// get the header information only
FileInputStream fStream = fileUtil.read(urlToFile.get(url));
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeStream(fStream, null, o);
if (o.outWidth <= 0 || o.outHeight <= 0) {
throw new FileNotFoundException();
}
// find the scale at which we still get minimum IMAGE_SIZE pixels in
// one dimension (must be power of 2)
int dim = o.outWidth > o.outHeight ? o.outWidth : o.outHeight;
int scale = 1;
while ((dim >>= 1) > IMAGE_SIZE) {
scale++;
}
// decode and put into imagemap
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize = scale;
Bitmap bitmap = BitmapFactory.decodeStream(
fileUtil.read(urlToFile.get(url)), null, o2);
BitmapDrawable drawable = new BitmapDrawable(context.getResources(),
bitmap);
imageMap.put(url, drawable);
return true;
} catch (FileNotFoundException e) {
urlToFile.remove(url);
return false;
} catch (OutOfMemoryError e) {
urlToFile.remove(url);
return false;
}
}
@Override
public void onPostExecute(Boolean success) {
processingUrls.remove(url);
if (success) {
eventBus.fireEvent(new NewImageEvent(url));
}
}
}.execute(null, null);
return getDefaultIcon();
}
/**
* Deferred initialization so we know that the call to getResources works
*/
public void init() {
Bitmap icon = BitmapFactory.decodeResource(context.getResources(),
R.drawable.logo);
defaultIcon = new BitmapDrawable(context.getResources(), icon);
load();
}
public void save() {
FileOutputStream fos;
try {
fos = configFileUtil.write(IMAGECACHE_FILENAME, false);
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(urlToFile);
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@SuppressWarnings("unchecked")
private void load() {
try {
FileInputStream fis = configFileUtil
.read(IMAGECACHE_FILENAME);
ObjectInputStream is = new ObjectInputStream(fis);
setImages((Map<String, String>) is.readObject());
is.close();
} catch (IOException e) {
// on failure: assume no cache
setImages(new HashMap<String, String>());
File f = configFileUtil.resolveFile(IMAGECACHE_FILENAME);
if (f.exists()) {
f.delete();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
private void setImages(Map<String, String> newImages) {
urlToFile.clear();
urlToFile.putAll(newImages);
}
public BitmapDrawable getDefaultIcon() {
return defaultIcon;
}
}