/*
* Copyright 2014 Bevbot LLC <info@bevbot.com>
*
* This file is part of the Kegtab package from the Kegbot project. For
* more information on Kegtab or Kegbot, see <http://kegbot.org/>.
*
* Kegtab 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, version 2.
*
* Kegtab 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 Kegtab. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kegbot.app.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import com.google.common.collect.Sets;
import com.hoho.android.usbserial.util.HexDump;
import org.kegbot.app.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* This helper class download images from the Internet and binds those with the provided ImageView.
* <p/>
* <p/>
* A local cache of downloaded images is maintained internally to improve performance.
*/
public class ImageDownloader {
private static final String TAG = ImageDownloader.class.getSimpleName();
private static final int HANDLER_KEY_DOWNLOAD_COMPLETE = 1;
private static final boolean DEBUG = false;
/**
* All ImageViews and the URL they have requested.
*/
private final WeakHashMap<ImageView, String> mDownloadRequests =
new WeakHashMap<ImageView, String>();
private static class DownloadResult {
String url;
Bitmap bitmap;
DownloadResult(String url, Bitmap bitmap) {
this.url = url;
this.bitmap = bitmap;
}
}
private final ExecutorService mExecutor = Executors.newFixedThreadPool(5);
private final Context mContext;
private URL mBaseUrl;
private File mCacheDir;
@SuppressLint("HandlerLeak")
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == HANDLER_KEY_DOWNLOAD_COMPLETE) {
final DownloadResult result = (DownloadResult) msg.obj;
handleDownloadComplete(result);
}
super.handleMessage(msg);
}
};
public ImageDownloader(final Context context, final String baseUrl) {
mContext = context;
mCacheDir = context.getCacheDir();
setBaseUrl(baseUrl);
}
public void setBaseUrl(String url) {
if (url != null) {
try {
mBaseUrl = new URL(url);
} catch (MalformedURLException e) {
Log.w(TAG, "Bad base url: " + url);
mBaseUrl = null;
}
}
}
/**
* Download the specified image from the Internet and binds it to the provided ImageView. The
* binding is immediate if the image is found in the cache and will be done asynchronously
* otherwise. A null bitmap will be associated to the ImageView if an error occurs.
*
* @param url The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void download(String url, final ImageView imageView) {
if (mBaseUrl != null) {
URL fullUrl;
try {
fullUrl = new URL(mBaseUrl, url);
} catch (MalformedURLException e) {
Log.w(TAG, "Malformed URL: " + url);
return;
}
if (DEBUG) Log.d(TAG, "original url=" + url);
url = fullUrl.toString();
}
if (DEBUG) Log.d(TAG, "download url=" + url + " imageView=" + imageView);
imageView.setTag(url);
// resetPurgeTimer();
final Bitmap bitmap = getBitmapFromCache(url.toString());
if (bitmap != null) {
if (DEBUG) Log.d(TAG, "download: cache hit");
// Bitmap in cache: no download necessary.
applyBitmapToImageView(bitmap, imageView);
} else {
if (DEBUG) Log.d(TAG, "download: cache miss");
// TODO(mikey): this should be done only in an adapter when convertView !=
// null
// imageView.setBackgroundDrawable(null);
// imageView.setImageBitmap(null);
// No bitmap in cache: enqueue the download.
synchronized (mDownloadRequests) {
if (DEBUG) Log.d(TAG, "download: adding to request queue");
if (!mDownloadRequests.containsValue(url)) {
enqueueDownload(url);
}
mDownloadRequests.put(imageView, url);
}
}
}
private void enqueueDownload(final String url) {
if (DEBUG) Log.d(TAG, "Enqueuing download: url=" + url);
mExecutor.submit(new Runnable() {
@Override
public void run() {
if (DEBUG) Log.d(TAG, "Download running for url=" + url);
Bitmap bitmap = getBitmapFromFileCache(url);
if (bitmap != null) {
if (DEBUG) Log.d(TAG, "Found bitmap in file cache.");
} else {
if (DEBUG) Log.d(TAG, "Download running for url=" + url);
bitmap = Downloader.downloadBitmap(url);
Log.d(TAG, "Downloaded: " + url);
addBitmapToFileCache(url, bitmap);
}
addBitmapToCache(url, bitmap);
postDownloadCompletedToHandler(url, bitmap);
}
});
}
private void postDownloadCompletedToHandler(String url, Bitmap bitmap) {
final DownloadResult result = new DownloadResult(url, bitmap);
final Message msg = mHandler.obtainMessage(HANDLER_KEY_DOWNLOAD_COMPLETE, result);
mHandler.sendMessage(msg);
}
private void applyBitmapToImageView(Bitmap bitmap, ImageView imageView) {
if (DEBUG) Log.d(TAG, "Assigning bitmap=" + bitmap + " imageView=" + imageView);
Utils.setBackground(imageView, null);
imageView.setImageBitmap(bitmap);
imageView.setAlpha(1.0f);
}
private void handleDownloadComplete(DownloadResult downloadResult) {
final String url = downloadResult.url;
final Bitmap bitmap = downloadResult.bitmap;
if (DEBUG) Log.d(TAG, "handleDownloadComplete: url=" + url + " bitmap=" + bitmap);
final Set<ImageView> toRemove = Sets.newLinkedHashSet();
synchronized (mDownloadRequests) {
for (final Map.Entry<ImageView, String> entry : mDownloadRequests.entrySet()) {
if (url.equals(entry.getValue())) {
final ImageView imageView = entry.getKey();
toRemove.add(imageView);
final String imageViewTag = (String) imageView.getTag();
if (url.equals(imageViewTag)) {
applyBitmapToImageView(bitmap, imageView);
Animation myFadeInAnimation = AnimationUtils.loadAnimation(mContext,
R.anim.image_fade_in);
imageView.startAnimation(myFadeInAnimation);
}
}
}
for (final ImageView view : toRemove) {
mDownloadRequests.remove(view);
}
}
}
public void cancelDownloadForView(ImageView view) {
view.setTag(null);
synchronized (mDownloadRequests) {
mDownloadRequests.remove(view);
}
}
/*
* An InputStream that skips the exact number of bytes provided, unless it
* reaches EOF.
*/
static class FlushedInputStream extends FilterInputStream {
public FlushedInputStream(InputStream inputStream) {
super(inputStream);
}
@Override
public long skip(long n) throws IOException {
long totalBytesSkipped = 0L;
while (totalBytesSkipped < n) {
long bytesSkipped = in.skip(n - totalBytesSkipped);
if (bytesSkipped == 0L) {
int b = read();
if (b < 0) {
break; // we reached EOF
} else {
bytesSkipped = 1; // we read one byte
}
}
totalBytesSkipped += bytesSkipped;
}
return totalBytesSkipped;
}
}
/*
* Cache-related fields and methods.
*
* We use a hard and a soft cache. A soft reference cache is too aggressively
* cleared by the Garbage Collector.
*/
private static final int HARD_CACHE_CAPACITY = 10;
// Hard cache, with a fixed maximum capacity and a life duration
private final HashMap<String, Bitmap> sHardBitmapCache = new LinkedHashMap<String, Bitmap>(
HARD_CACHE_CAPACITY / 2, 0.75f, true) {
@Override
protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) {
if (size() > HARD_CACHE_CAPACITY) {
// Entries push-out of hard reference cache are transferred to soft
// reference cache
sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
return true;
} else {
return false;
}
}
};
// Soft cache for bitmaps kicked out of hard cache
private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache =
new ConcurrentHashMap<String, SoftReference<Bitmap>>(HARD_CACHE_CAPACITY / 2);
/**
* Adds this bitmap to the cache.
*
* @param bitmap The newly downloaded bitmap.
*/
private void addBitmapToCache(String url, Bitmap bitmap) {
if (bitmap != null) {
synchronized (sHardBitmapCache) {
sHardBitmapCache.put(url, bitmap);
}
}
}
private static String getFingerprint(String uri) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
md.update(uri.getBytes());
byte[] digest = md.digest();
return HexDump.toHexString(digest);
}
private File getCacheFilename(String url) {
return new File(mCacheDir, "ImageDownloader-" + getFingerprint(url));
}
private void addBitmapToFileCache(String url, Bitmap bitmap) {
if (bitmap == null) {
return;
}
final File cacheFile = getCacheFilename(url);
if (cacheFile.exists()) {
Log.d(TAG, "Updating cached file url=" + url + " filename=" + cacheFile);
} else {
Log.d(TAG, "Creating cached file url=" + url + " filename=" + cacheFile);
}
try {
cacheFile.createNewFile();
FileOutputStream fos = new FileOutputStream(cacheFile);
bitmap.compress(Bitmap.CompressFormat.PNG, 85, fos);
fos.flush();
fos.close();
} catch (IOException e) {
Log.w(TAG, "Error adding cache file.", e);
cacheFile.delete();
}
}
private Bitmap getBitmapFromFileCache(String url) {
final File cacheFile = getCacheFilename(url);
if (cacheFile.exists()) {
if (DEBUG) Log.d(TAG, "getFromFileCache hit: url=" + url + " filename=" + cacheFile);
} else {
if (DEBUG) Log.d(TAG, "getFromFileCache MISS: url=" + url + " filename=" + cacheFile);
return null;
}
return BitmapFactory.decodeFile(cacheFile.getAbsolutePath());
}
/**
* @param url The URL of the image that will be retrieved from the cache.
* @return The cached bitmap or null if it was not found.
*/
private Bitmap getBitmapFromCache(String url) {
// First try the hard reference cache
synchronized (sHardBitmapCache) {
final Bitmap bitmap = sHardBitmapCache.get(url);
if (bitmap != null) {
// Bitmap found in hard cache
// Move element to first position, so that it is removed last
sHardBitmapCache.remove(url);
sHardBitmapCache.put(url, bitmap);
return bitmap;
}
}
// Then try the soft reference cache
SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(url);
if (bitmapReference != null) {
final Bitmap bitmap = bitmapReference.get();
if (bitmap != null) {
// Bitmap found in soft cache
return bitmap;
}
// Soft reference has been Garbage Collected
sSoftBitmapCache.remove(url);
}
return null;
}
/**
* Clears the image cache used internally to improve performance. Note that for memory efficiency
* reasons, the cache will automatically be cleared after a certain inactivity delay.
*/
public void clearCache() {
sHardBitmapCache.clear();
sSoftBitmapCache.clear();
}
}