package external.GifImageViewEx.net.frakbot.imageviewex;
import java.io.File;
import java.io.IOException;
import android.content.Context;
import android.content.IntentFilter;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.util.LruCache;
import android.util.AttributeSet;
import com.aiyou.utils.logcat.Logcat;
import com.jakewharton.disklrucache.DiskLruCache;
import external.GifImageViewEx.com.foxykeep.datadroid.requestmanager.Request;
import external.GifImageViewEx.com.foxykeep.datadroid.requestmanager.RequestManager.RequestListener;
import external.GifImageViewEx.net.frakbot.cache.CacheHelper;
import external.GifImageViewEx.net.frakbot.imageviewex.broadcastreceiver.ConnectivityChangeBroadcastReceiver;
import external.GifImageViewEx.net.frakbot.imageviewex.listener.ImageViewExRequestListener;
import external.GifImageViewEx.net.frakbot.imageviewex.requestmanager.ImageViewExRequestFactory;
import external.GifImageViewEx.net.frakbot.imageviewex.requestmanager.ImageViewExRequestManager;
/**
* Extension of the ImageViewEx that handles the download and caching of images
* and animated GIFs.
*
* @author Francesco Pontillo, Sebastiano Poggi
*/
public class ImageViewNext extends ImageViewEx {
private static final String TAG = ImageViewNext.class.getSimpleName();
private static final int DISK_CACHE_VALUE_COUNT = 1;
private Drawable mLoadingD;
private static int mClassLoadingResId;
private Drawable mErrorD;
private static int mClassErrorResId;
private boolean mAutoRetryFromNetwork;
private static boolean mClassAutoRetryFromNetwork;
private boolean hasFailedDownload;
private String mUrl;
private ImageLoadCompletionListener mLoadCallbacks;
protected ImageViewExRequestManager mRequestManager;
protected Request mCurrentRequest;
protected RequestListener mCurrentRequestListener;
private Context mContext;
private static int mMemCacheSize = 10 * 1024 * 1024; // 10MiB
private static LruCache<String, byte[]> mMemCache;
private static int mAppVersion = 1;
private static int mDiskCacheSize = 50 * 1024 * 1024; // 50MiB
private static DiskLruCache mDiskCache;
private static boolean mCacheInit = false;
private static int mConcurrentThreads = 10;
private ConnectivityChangeBroadcastReceiver mReceiver;
private static final String RECEIVER_ACTION = android.net.ConnectivityManager.CONNECTIVITY_ACTION;
/** Represents a cache level. */
public enum CacheLevel {
/** The first level of cache: the memory cache */
MEMORY,
/** The second level of cache: the disk cache */
DISK,
/** No caching, direct fetching from the network */
NETWORK
}
/** {@inheritDoc} */
public ImageViewNext(Context context) {
super(context);
init(context);
}
/**
* Creates an instance for the class. Initializes the auto retry from
* network to true.
*
* @param context The context to initialize the instance into.
* @param attrs The parameters to initialize the instance with.
*/
public ImageViewNext(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
/**
* Initializes a few instance level variables.
*
* @param context The Context used for initialization.
*/
private void init(Context context) {
mContext = context;
mRequestManager = ImageViewExRequestManager.from(context);
mClassAutoRetryFromNetwork = true;
mAutoRetryFromNetwork = true;
hasFailedDownload = false;
}
/** {@inheritDoc} */
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
registerReceiver();
}
/** {@inheritDoc} */
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
unregisterReceiver();
}
/**
* Register the {@link ConnectivityChangeBroadcastReceiver} for this
* instance.
*/
private void registerReceiver() {
// If the receiver does not exist
if (mReceiver == null) {
mReceiver = new ConnectivityChangeBroadcastReceiver(this);
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(RECEIVER_ACTION);
mContext.registerReceiver(mReceiver, intentFilter);
}
}
/**
* Unregister the {@link ConnectivityChangeBroadcastReceiver} for this
* instance.
*/
private void unregisterReceiver() {
// If the receiver does exists
if (mReceiver != null) {
mContext.unregisterReceiver(mReceiver);
mReceiver = null;
}
}
/** Gets the current image loading callback, if any */
public ImageLoadCompletionListener getLoadCallbacks() {
return mLoadCallbacks;
}
/**
* Sets the image loading callback.
*
* @param loadCallbacks The listener instance, or null to clear it.
*/
public void setLoadCallbacks(ImageLoadCompletionListener loadCallbacks) {
mLoadCallbacks = loadCallbacks;
}
/** @return The in-memory cache. */
public static LruCache<String, byte[]> getMemCache() {
return mMemCache;
}
/** @return The disk cache. */
public static DiskLruCache getDiskCache() {
return mDiskCache;
}
/** @return The in-memory cache size, in bits. */
public static int getMemCacheSize() {
return mMemCacheSize;
}
/**
* @param memCacheSize The in-memory cache size to set, in bits.
*/
public static void setMemCacheSize(int memCacheSize) {
mMemCacheSize = memCacheSize;
}
/** @return The version of the app. */
public static int getAppVersion() {
return mAppVersion;
}
/**
* @param appVersion The app version to set.
*/
public static void setAppVersion(int appVersion) {
ImageViewNext.mAppVersion = appVersion;
}
/**
* Sets the image loading callbacks listener.
*
* @param l The listener, or null to clear it.
*/
public void setImageLoadCallbacks(ImageLoadCompletionListener l) {
mLoadCallbacks = l;
}
/**
* Gets the current image loading callbacks listener, if any.
*
* @return Returns the callbacks listener.
*/
public ImageLoadCompletionListener getImageLoadCallbacks() {
return mLoadCallbacks;
}
/** @return The disk cache max size, in bits. */
public static int getDiskCacheSize() {
return mDiskCacheSize;
}
/**
* @param diskCacheSize The disk cache max size to set, in bits.
*/
public static void setDiskCacheSize(int diskCacheSize) {
ImageViewNext.mDiskCacheSize = diskCacheSize;
}
/**
* Initializes both the in-memory and the disk-cache at class-level, if it
* hasn't been done already. This method is idempotent.
*/
public static void initCaches(Context context) {
if (!mCacheInit) {
mMemCache = new LruCache<String, byte[]>(mMemCacheSize) {
protected int sizeOf(String key, byte[] value) {
return value.length;
}
};
File diskCacheDir = CacheHelper.getDiskCacheDir(context,
"imagecache");
try {
mDiskCache = DiskLruCache.open(diskCacheDir, mAppVersion,
DISK_CACHE_VALUE_COUNT, mDiskCacheSize);
} catch (IOException ignored) {
}
mCacheInit = true;
}
}
/**
* Sets the loading {@link Drawable} to be used for every
* {@link ImageViewNext}.
*
* @param classLoadingDrawableResId the {@link int} resource ID of the
* Drawable while loading an image.
*/
public static void setClassLoadingDrawable(int classLoadingDrawableResId) {
mClassLoadingResId = classLoadingDrawableResId;
}
/**
* Sets the loading {@link Drawable} to be used for this
* {@link ImageViewNext}.
*
* @param loadingDrawable the {@link Drawable} to display while loading an
* image.
*/
public void setLoadingDrawable(Drawable loadingDrawable) {
mLoadingD = loadingDrawable;
}
/**
* Gets the {@link Drawable} to display while loading an image.
*
* @return {@link Drawable} to display while loading.
*/
public Drawable getLoadingDrawable() {
if (mLoadingD != null) {
return mLoadingD;
} else {
return mClassLoadingResId > 0 ? getResources().getDrawable(
mClassLoadingResId) : null;
}
}
/**
* Sets the error {@link Drawable} to be used for every
* {@link ImageViewNext}.
*
* @param classErrorDrawableResId the {@link int} resource ID of the
* Drawable to display after an error getting an image.
*/
public static void setClassErrorDrawable(int classErrorDrawableResId) {
mClassErrorResId = classErrorDrawableResId;
}
/**
* Sets the error {@link Drawable} to be used for this {@link ImageViewNext}
* .
*
* @param errorDrawable the {@link Drawable} to display after an error
* getting an image.
*/
public void setErrorDrawable(Drawable errorDrawable) {
mErrorD = errorDrawable;
}
/**
* Gets the {@link Drawable} to display after an error loading an image.
*
* @return {@link Drawable} to display after an error loading an image.
*/
public Drawable getErrorDrawable() {
return mErrorD != null ? mErrorD : getResources().getDrawable(
mClassErrorResId);
}
/**
* Checks if a request is already in progress.
*
* @return true if there is a pending request, false otherwise.
*/
private boolean isRequestInProgress() {
return mCurrentRequest != null
&& mRequestManager.isRequestInProgress(mCurrentRequest);
}
/** Aborts the current request, if any, and stops everything else. */
private void abortEverything() {
// Abort the current request before starting another one
if (isRequestInProgress()) {
mRequestManager.removeRequestListener(mCurrentRequestListener);
}
stop();
stopLoading();
}
/**
* Sets the content of the {@link ImageViewNext} with the data to be
* downloaded from the provided URL.
*
* @param url The URL to download the image from. It can be an animated GIF.
*/
public void setUrl(String url) {
mUrl = url;
// Abort the pending request (if any) and stop animating/loading
abortEverything();
// Start the whole retrieval chain
getFromMemCache(url);
}
/**
* Returns the current URL set to the {@link ImageViewNext}. The URL will be
* returned regardless of the existence of the image or of the
* caching/downloading progress.
*
* @return The URL set for this {@link ImageViewNext}.
*/
public String getUrl() {
return mUrl;
}
/**
* Returns true if this instance will automatically retry the download from
* the network when it becomes available once again. The instance level
* settings has priority over the class level's.
*
* @return true if the instance retries to download the image when the
* network is once again available, false otherwise.
*/
public boolean isAutoRetryFromNetwork() {
return mAutoRetryFromNetwork;
}
/**
* Sets the value of auto retry from network for this instance, set it to
* true if this instance has to automatically retry the download from the
* network when it becomes available once again, false otherwise. The
* instance level settings has priority over the class level's.
* <p/>
* If the instance was previously forbidden to auto-retry, it will be
* allowed as soon as this method is called with a true argument.
* <p/>
* If the instance was previously allowed to auto-retry, it will be
* forbidden as soon as this method is called with a false argument.
*
* @param autoRetryFromNetwork The instance value for the auto retry.
*/
public void setAutoRetryFromNetwork(boolean autoRetryFromNetwork) {
boolean registerAfter;
boolean unregisterAfter;
// If nothing changes, do nothing
if (mAutoRetryFromNetwork == autoRetryFromNetwork)
return;
// Set the "after" booleans
registerAfter = !mAutoRetryFromNetwork;
unregisterAfter = !autoRetryFromNetwork;
// Set the state value
mAutoRetryFromNetwork = autoRetryFromNetwork;
// Register or unregister the receiver according to the new value
if (registerAfter) {
registerReceiver();
} else if (unregisterAfter) {
unregisterReceiver();
}
}
/**
* Returns true if every ImageViewNext will automatically retry the download
* from the network when it becomes available once again.
*
* @return true if ImageViewNext retries to download the image when the
* network is once again available, false otherwise.
*/
public static boolean isClassAutoRetryFromNetwork() {
return ImageViewNext.mClassAutoRetryFromNetwork;
}
/**
* Sets the value of auto retry from network for ImageViewNext, set it to
* true if ImageViewNext has to automatically retry the download from the
* network when it becomes available once again, false otherwise.
* <p/>
* All of the existing constructed instances won't be affected by this.
*
* @param classAutoRetryFromNetwork The instance value for the auto retry.
*/
public static void setClassAutoRetryFromNetwork(
boolean classAutoRetryFromNetwork) {
ImageViewNext.mClassAutoRetryFromNetwork = classAutoRetryFromNetwork;
}
/**
* Checks if the auto retry can be applied for the current instance.
*
* @return true if this instance is allowed to auto retry network ops, false
* otherwise.
*/
private boolean isAutoRetryTrueSomewhere() {
return isAutoRetryFromNetwork() || isClassAutoRetryFromNetwork();
}
/**
* Tries to retrieve the image from network, if and only if:
* <ul>
* <li>No requests are pending for this instance.</li>
* <li>A previous download failed.</li>
* <li>The instance-level or class-level auto retry is set to true, with
* this priority.</li>
* </ul>
*/
public void retryFromNetworkIfPossible() {
// Only retry to get the image from the network:
// - if no requests are in progress
// - if the download previously failed
// - auto retry is set to true for the instance or the class (in order)
if (!isRequestInProgress() && hasFailedDownload
&& isAutoRetryTrueSomewhere()) {
Logcat.i(TAG, "Autoretry: true somewhere, retrying...");
// Abort the pending request (if any) and stop animating/loading
abortEverything();
// Initalize caches
ImageViewNext.initCaches(mContext);
// Starts the retrieval from the network once again
getFromNetwork(getUrl());
// Cross ye fingers
} else {
Logcat.i(TAG, "Autoretry: false, sorry.");
}
}
/**
* Tries to get the image from the memory cache.
*
* @param url The URL to download the image from. It can be an animated GIF.
*/
private void getFromMemCache(String url) {
Logcat.i(TAG, "Memcache: getting for URL " + url + " @" + hashCode());
if (mLoadCallbacks != null) {
mLoadCallbacks.onLoadStarted(this, CacheLevel.MEMORY);
}
// Get the URL from the input Bundle
if (url == null || "".equals(url))
return;
// Initializes the caches, if they're not initialized already
ImageViewNext.initCaches(mContext);
LruCache<String, byte[]> cache = ImageViewNext.getMemCache();
byte[] image = cache.get(url);
if (image == null) {
handleMemCacheMiss();
} else {
onMemCacheHit(image, url);
}
}
/** Generic function to handle the mem cache miss. */
private void handleMemCacheMiss() {
// Calls the class callback
onMemCacheMiss();
// Starts searching in the disk cache
getFromDiskCache(getUrl());
}
/**
* Tries to get the image from the disk cache.
*
* @param url The URL to download the image from. It can be an animated GIF.
*/
private void getFromDiskCache(String url) {
Logcat.i(TAG, "Diskcache: getting for URL " + url + " @" + hashCode());
Request mRequest = ImageViewExRequestFactory
.getImageDiskCacheRequest(url);
mCurrentRequestListener = new ImageDiskCacheListener(this);
mRequestManager.execute(mRequest, mCurrentRequestListener);
if (mLoadCallbacks != null) {
mLoadCallbacks.onLoadStarted(this, CacheLevel.DISK);
}
}
/**
* Tries to get the image from the network.
*
* @param url The URL to download the image from. It can be an animated GIF.
*/
private void getFromNetwork(String url) {
Logcat.i(TAG, "Network: getting for URL " + url + " @" + hashCode());
Request mRequest = ImageViewExRequestFactory
.getImageDownloaderRequest(url);
mCurrentRequestListener = new ImageDownloadListener(this);
mRequestManager.execute(mRequest, mCurrentRequestListener);
if (mLoadCallbacks != null) {
mLoadCallbacks.onLoadStarted(this, CacheLevel.NETWORK);
}
}
/**
* Called when the image is got from whatever the source. Override this to
* get the appropriate callback.
*
* @param image The image as a byte array.
*/
protected void onSuccess(byte[] image) {
setByteArray(image);
}
/**
* Called when the image is got from whatever the source. Checks if the
* original URL matches the current one set in the instance of
* ImageViewNext.
*
* @param image The image as a byte array.
* @param url The URL of the retrieved image.
*/
private void onPreSuccess(byte[] image, String url) {
// Only set the image if the current url equals to the retrieved image's
// url
if (url != null && url.equals(getUrl())) {
onSuccess(image);
}
}
/**
* Called when the image is got from the memory cache. Override this to get
* the appropriate callback.
*
* @param image The image as a byte array.
* @param url The URL of the retrieved image.
*/
protected void onMemCacheHit(byte[] image, String url) {
Logcat.i(TAG, "Memory cache HIT @" + hashCode());
onPreSuccess(image, url);
if (mLoadCallbacks != null) {
mLoadCallbacks.onLoadCompleted(this, CacheLevel.MEMORY);
}
}
/**
* Called when there is a memory cache miss for the image. Override this to
* get the appropriate callback.
*/
protected void onMemCacheMiss() {
Drawable loadingDrawable = getLoadingDrawable();
if (loadingDrawable != null) {
ScaleType scaleType = getScaleType();
if (scaleType != null) {
setScaleType(scaleType);
} else {
setScaleType(ScaleType.CENTER_INSIDE);
}
setImageDrawable(loadingDrawable);
if (loadingDrawable instanceof AnimationDrawable) {
((AnimationDrawable) loadingDrawable).start();
}
} else {
setImageDrawable(mEmptyDrawable); // This also stops any ongoing
// loading process
}
if (mLoadCallbacks != null) {
mLoadCallbacks.onLoadError(this, CacheLevel.MEMORY);
}
}
/**
* Called when the image is got from the disk cache. Override this to get
* the appropriate callback.
*
* @param image The image as a byte array.
* @param url The URL of the retrieved image.
*/
protected void onDiskCacheHit(byte[] image, String url) {
Logcat.i(TAG, "Disk cache HIT @" + hashCode());
onPreSuccess(image, url);
if (mLoadCallbacks != null) {
mLoadCallbacks.onLoadCompleted(this, CacheLevel.DISK);
}
}
/**
* Called when there is a disk cache miss for the image. Override this to
* get the appropriate callback.
*/
protected void onDiskCacheMiss() {
if (mLoadCallbacks != null) {
mLoadCallbacks.onLoadError(this, CacheLevel.DISK);
}
}
/**
* Called when the image is got from the network. Override this to get the
* appropriate callback.
*
* @param image The image as a byte array.
* @param url The URL of the retrieved image.
*/
protected void onNetworkHit(byte[] image, String url) {
Logcat.i(TAG, "Network HIT @" + hashCode());
onPreSuccess(image, url);
hasFailedDownload = false;
if (mLoadCallbacks != null) {
mLoadCallbacks.onLoadCompleted(this, CacheLevel.NETWORK);
}
}
/**
* Called when there is a network miss for the image, usually a 404.
* Override this to get the appropriate callback.
*/
protected void onNetworkMiss() {
if (mLoadCallbacks != null) {
mLoadCallbacks.onLoadError(this, CacheLevel.NETWORK);
}
hasFailedDownload = true;
}
/**
* Called when the image could not be found anywhere. Override this to get
* the appropriate callback.
*/
protected void onMiss() {
Drawable errorDrawable = getErrorDrawable();
if (getErrorDrawable() != null) {
ScaleType scaleType = getScaleType();
if (scaleType != null) {
setScaleType(scaleType);
} else {
setScaleType(ScaleType.CENTER_INSIDE);
}
setImageDrawable(errorDrawable);
if (errorDrawable instanceof AnimationDrawable) {
((AnimationDrawable) errorDrawable).start();
}
}
}
/**
* Sets the image from a byte array.
*
* @param image The image to set.
*/
private void setByteArray(final byte[] image) {
if (image != null) {
ScaleType scaleType = getScaleType();
if (scaleType != null) {
setScaleType(scaleType);
}
setSource(image);
}
}
/**
* Returns the maximum number of concurrent worker threads used to get
* images from cache/network.
*
* @return Maximum number of concurrent threads.
*/
public static int getMaximumNumberOfThreads() {
return mConcurrentThreads;
}
/**
* Define the maximum number of concurrent worker threads used to get images
* from cache/network. By default only 10 concurrent worker threads are used
* at the same time. The value will be set once and for all when the first
* ImageViewNext is instantiated. Calling this function again after an
* ImageViewNext is instantiated will have no effect.
*
* @param concurrentThreads The number of concurrent threads.
*/
public static void setMaximumNumberOfThreads(int concurrentThreads) {
mConcurrentThreads = concurrentThreads;
}
/**
* Operation listener for the disk cache retrieval operation.
*
* @author Francesco Pontillo
*/
private class ImageDiskCacheListener extends ImageViewExRequestListener {
public ImageDiskCacheListener(ImageViewNext imageViewNext) {
super(imageViewNext);
}
@Override
public void onRequestFinished(Request request, Bundle resultData) {
byte[] image = resultData
.getByteArray(ImageViewExRequestFactory.BUNDLE_EXTRA_OBJECT);
String url = resultData
.getString(ImageViewExRequestFactory.BUNDLE_EXTRA_IMAGE_URL);
if (image == null) {
handleMiss();
} else {
mImageViewNext.onDiskCacheHit(image, url);
}
}
@Override
public void onRequestConnectionError(Request request, int statusCode) {
handleMiss();
}
@Override
public void onRequestDataError(Request request) {
handleMiss();
}
@Override
public void onRequestCustomError(Request request, Bundle resultData) {
handleMiss();
}
/** Generic function to handle the cache miss. */
private void handleMiss() {
// Calls the class callback
mImageViewNext.onDiskCacheMiss();
// Starts searching in the network
getFromNetwork(mImageViewNext.getUrl());
}
}
/**
* Operation listener for the network retrieval operation.
*
* @author Francesco Pontillo
*/
private class ImageDownloadListener extends ImageViewExRequestListener {
public ImageDownloadListener(ImageViewNext imageViewNext) {
super(imageViewNext);
}
@Override
public void onRequestFinished(Request request, Bundle resultData) {
byte[] image = resultData
.getByteArray(ImageViewExRequestFactory.BUNDLE_EXTRA_OBJECT);
String url = resultData
.getString(ImageViewExRequestFactory.BUNDLE_EXTRA_IMAGE_URL);
if (image == null || image.length == 0) {
handleMiss();
} else {
mImageViewNext.onNetworkHit(image, url);
}
}
@Override
public void onRequestConnectionError(Request request, int statusCode) {
handleMiss();
}
@Override
public void onRequestDataError(Request request) {
handleMiss();
}
@Override
public void onRequestCustomError(Request request, Bundle resultData) {
handleMiss();
}
/** Generic function to handle the network miss. */
private void handleMiss() {
// Calls the class callback
mImageViewNext.onNetworkMiss();
// Calss the final miss class callback
mImageViewNext.onMiss();
}
}
/** A simple interface for image loading callbacks. */
public interface ImageLoadCompletionListener {
/**
* Loading of a resource has been started by invoking
* {@link #setUrl(String)}.
*
* @param v The ImageViewNext on which the loading has begun
* @param level The cache level involved. You will receive a pair of
* calls, one to onLoadStarted and one to onLoadCompleted or
* to onLoadError, for each cache level, in this order:
* memory->disk->network (for MISS on both memory and disk
* caches)
*/
public void onLoadStarted(ImageViewNext v, CacheLevel level);
/**
* Loading of a resource has been completed. This corresponds to a cache
* HIT for the memory and disk cache levels, or a successful download
* from the net.
*
* @param v The ImageViewNext on which the loading has completed
* @param level The cache level involved. You will receive a pair of
* calls, one to onLoadStarted and one to onLoadCompleted or
* to onLoadError, for each cache level, in this order:
* memory->disk->network (for MISS on both memory and disk
* caches).
*/
public void onLoadCompleted(ImageViewNext v, CacheLevel level);
/**
* Loading of a resource has failed. This corresponds to a cache MISS
* for the memory and disk cache levels, or a successful download from
* the net.
*
* @param v The ImageViewNext on which the loading has begun
* @param level The cache level involved. You will receive a pair of
* calls, one to onLoadStarted and one to onLoadCompleted or
* to onLoadError, for each cache level, in this order:
* memory->disk->network (for MISS on both memory and disk
* caches)
*/
public void onLoadError(ImageViewNext v, CacheLevel level);
}
}