public interface Decoder {

    /**
     * Load image from disk file on the current thread and set it in the image request object.
     */
    void decode(MemoryPool memoryPool, ImageRequest imageRequest, File file, ImageLoadSpec spec);
} public interface DiskCache {

    /**
     * Gets the representation of the online uri on the local disk.<br>
     * Must be unique mapping from URI to file path.
     *
     * @param uri The online image uri
     * @return The path of the file on the disk
     */
    File getCacheFile(String uri, ImageLoadSpec spec);

    /**
     * Get disk cached image for the given request.<br>
     * If the image is NOT in the cache the callback will be executed immediately.<br>
     * If the image is in cache an async operation will load the image from disk and then execute the callback.
     *
     * @param imageRequest the request to load the image from disk for.
     * @param decoder Used to decode images from the disk to bitmap.
     * @param memoryPool Used to provide reusable bitmaps for image decoding into.
     * @param callback The callback to execute on async requests to the cache
     */
    void getAsync(ImageRequest imageRequest, ImageLoadSpec altSpec, Decoder decoder, MemoryPool memoryPool, Callback callback);

    /**
     * Image added to disk cache, update the disk cache.<br>
     * Called when an image was downloaded and now is part of the disk cache, disk cache will update
     * its knowledge of the disk cache size, may trigger cleanup of the cache if limit is reached.
     */
    void imageAdded(long size);

    /**
     * Clear all the cached files async.
     */
    void clear();

    //region: Inner class: Callbacks

    /**
     * Callback for getting cached image.
     */
    public static interface Callback {

        /**
         * Callback for getting cached image, if not cached will have null.
         *
         * @param canceled if the request was canceled during execution therefor not loading the image
         */
        void loadImageDiskCacheCallback(ImageRequest imageRequest, boolean canceled);
    }
    //endregion
} public interface Downloader {

    /**
     * Download
     *
     * @param imageRequest the request to download the image for.
     * @param prefetch if the request is prefetch or actually required now.
     * @param callback The callback to execute on async requests to the downloader
     */
    void downloadAsync(ImageRequest imageRequest, boolean prefetch, Callback callback);

    //region: Inner class: Callback

    /**
     * Callback for downloading image.
     */
    public static interface Callback {

        /**
         * Callback for downloading image.<br>
         * If the image was downloaded the download flag will be true, it can be false is request
         * was canceled during execution or download has failed. download can be true even if
         * the request was canceled if more than 50% was download before cancellation.
         *
         * @param downloaded if the image was downloaded, maybe false if canceled or failed
         * @param canceled if the request was canceled during execution therefor not loading the image
         */
        public void loadImageDownloaderCallback(ImageRequest imageRequest, boolean downloaded, boolean canceled);
    }
    //endregion
} public final class ImageLoadSpec {

    //region: Fields and Consts

    /**
     * the unique key of the spec used for identification and debug
     */
    private final String mKey;

    /**
     * the width of the image in pixels
     */
    private final int mWidth;

    /**
     * the height of the image in pixels
     */
    private final int mHeight;

    /**
     * The format of the image.
     */
    private final Format mFormat;

    /**
     * the pixel configuration to load the image in (4 bytes per image pixel, 2 bytes, etc.)
     */
    private final Bitmap.Config mPixelConfig;

    /**
     * The URI enhancer to use for this spec image loading
     */
    private final ImageServiceAdapter mImageServiceAdapter;
    //endregion

    /**
     * Init image loading spec.
     *
     * @param key the unique key of the spec used for identification and debug
     * @param width the width of the image in pixels
     * @param height the height of the image in pixels
     * @param format The format of the image.
     * @param pixelConfig the pixel configuration to load the image in (4 bytes per image pixel, 2 bytes, etc.)
     * @param imageServiceAdapter The URI enhancer to use for this spec image loading
     */
    ImageLoadSpec(String key, int width, int height, Format format, Bitmap.Config pixelConfig, ImageServiceAdapter imageServiceAdapter) {
        mKey = key;
        mWidth = width;
        mHeight = height;
        mFormat = format;
        mPixelConfig = pixelConfig;
        mImageServiceAdapter = imageServiceAdapter;
    }

    /**
     * the unique key of the spec used for identification and debug
     */
    public String getKey() {
        return mKey;
    }

    /**
     * the width of the image in pixels
     */
    public int getWidth() {
        return mWidth;
    }

    /**
     * the height of the image in pixels
     */
    public int getHeight() {
        return mHeight;
    }

    /**
     * The format of the image.
     */
    public Format getFormat() {
        return mFormat;
    }

    /**
     * the pixel configuration to load the image in (4 bytes per image pixel, 2 bytes, etc.)
     */
    public Bitmap.Config getPixelConfig() {
        return mPixelConfig;
    }

    /**
     * The URI enhancer to use for this spec image loading
     */
    public ImageServiceAdapter getImageServiceAdapter() {
        return mImageServiceAdapter;
    }

    /**
     * Is the spec define specific width and height for the image.
     */
    public boolean isSizeBounded() {
        return mWidth > 0 && mHeight > 0;
    }

    @Override
    public String toString() {
        return "ImageLoadSpec{" +
                "mKey='" + mKey + '\'' +
                ", mWidth=" + mWidth +
                ", mHeight=" + mHeight +
                ", mFormat=" + mFormat +
                ", mPixelConfig=" + mPixelConfig +
                ", mImageServiceAdapter=" + mImageServiceAdapter +
                '}';
    }

    //region: Inner class: Format

    /**
     * The format of the image.
     */
    public static enum Format {
        UNCHANGE,
        JPEG,
        PNG,
        WEBP,
    }
    //endregion
} public enum LoadedFrom {
    MEMORY,
    DISK,
    NETWORK
} public interface MemoryPool {

    /**
     * Retrieve an image for the specified {@code url} and {@code spec}.<br>
     * If not found for primary spec, use the alternative.
     */
    ReusableBitmap get(String url, ImageLoadSpec spec, ImageLoadSpec altSpec);

    /**
     * Store an image in the cache for the specified {@code key}.
     */
    void set(ReusableBitmap bitmap);

    /**
     * TODO:a. doc
     */
    ReusableBitmap getUnused(ImageLoadSpec spec);

    /**
     * TODO:a. doc
     */
    void returnUnused(ReusableBitmap bitmap);

    /**
     * Clears the cache/pool.
     */
    void clear();

    /**
     * Handle trim memory event to release image caches on memory pressure.
     */
    void onTrimMemory(int level);
} public class ReusableBitmap {

    //region: Fields and Consts

    /**
     * The actual bitmap
     */
    protected Bitmap mBitmap;

    /**
     * the spec to load the image by
     */
    protected final ImageLoadSpec mSpec;

    /**
     * The URL of the image loaded in the bitmap, used to know that loaded image changed
     */
    protected String mBitmapUrl;

    /**
     * Is the bitmap is currently in use
     */
    protected AtomicInteger mInUse = new AtomicInteger();

    /**
     * Is the bitmap is currently in use by loading, not real use
     */
    protected boolean mInLoadUse;

    /**
     * Is the bitmap is has been released.
     */
    protected boolean mClosed;

    /**
     * The number of times the bitmap has been recycled
     */
    protected int mRecycleCount;
    //endregion

    /**
     * @param bitmap The actual bitmap
     * @param spec the spec to load the image by
     */
    public ReusableBitmap(Bitmap bitmap, ImageLoadSpec spec) {
        FILUtils.notNull(bitmap, "bitmap");
        FILUtils.notNull(spec, "spec");
        mBitmap = bitmap;
        mSpec = spec;
    }

    /**
     * The actual bitmap instance.
     */
    public Bitmap getBitmap() {
        return mBitmap;
    }

    /**
     * the URI of the loaded image in the bitmap.<br>
     * Used to know if the target requested image has been changed.<br>
     */
    public String getUri() {
        return mBitmapUrl;
    }

    /**
     * the URL of the loaded image in the bitmap.
     */
    public void setUrl(String url) {
        mRecycleCount++;
        mBitmapUrl = url;
    }

    /**
     * the spec the loaded image was loaded by.
     */
    public ImageLoadSpec getSpec() {
        return mSpec;
    }

    /**
     * Is the bitmap is currently in use and cannot be reused.
     */
    public boolean isInUse() {
        return mInUse.get() > 0 || mInLoadUse;
    }

    /**
     * Increment the bitmap in use count by 1.<br>
     * Affects the {@link #isInUse()} to know if the bitmap can be reused.<br>
     * Critical to call this method correctly.
     */
    public void incrementInUse() {
        mInUse.incrementAndGet();
        mInLoadUse = false;
    }

    /**
     * Decrement the bitmap in use count by 1.<br>
     * Affects the {@link #isInUse()} to know if the bitmap can be reused.<br>
     * Critical to call this method correctly.
     */
    public void decrementInUse() {
        mInUse.decrementAndGet();
    }

    /**
     * Is the bitmap is currently in use by loading, not real use.
     */
    public void setInLoadUse(boolean inLoadUse) {
        mInLoadUse = inLoadUse;
    }

    /**
     * Release the inner bitmap.
     */
    public void close() {
        FILLogger.debug("Close recycle bitmap [{}]", this);
        mClosed = true;
        mBitmapUrl = null;
        mBitmap.recycle();
        mBitmap = null;
    }

    @Override
    public String toString() {
        return "RecycleBitmap{" +
                "hash=" + hashCode() +
                ", mSpec='" + mSpec + '\'' +
                ", mInUse=" + mInUse.get() +
                ", mInLoadUse=" + mInLoadUse +
                ", mRecycleCount=" + mRecycleCount +
                ", mClosed=" + mClosed +
                '}';
    }
} public interface Target {

    /**
     * The URI source of the image
     */
    String getUri();

    /**
     * the spec to load the image by
     */
    String getSpecKey();

    /**
     * Callback when image is been downloaded to show progress.
     *
     * @param downloaded the number of bytes already downloaded
     * @param contentLength the total number of bytes to download
     */
    void onBitmapDownloading(long downloaded, long contentLength);

    /**
     * Callback when an image has been successfully loaded.<br>
     * <strong>Note:</strong> You must not recycle the bitmap.
     */
    void onBitmapLoaded(ReusableBitmap bitmap, LoadedFrom from);

    /**
     * Callback indicating the image could not be successfully loaded.<br>
     */
    void onBitmapFailed();
} public final class FILLogger {

    /**
     * The tag to use for all logs
     */
    private static final String TAG = "FastImageLoader";

    /**
     * If to write logs to logcat.
     */
    public static boolean mLogcatEnabled = false;

    /**
     * Extensibility appender to write the logs to.
     */
    public static LogAppender mAppender;

    /**
     * The min log level to write logs at, logs below this level are ignored.
     */
    public static int mLogLevel = Log.INFO;

    /**
     * Image load operation complete.
     *
     * @param url the url of the image
     * @param specKey the spec of the image load request
     * @param from from where the image was loaded (MEMORY/DISK/NETWORK)
     * @param successful was the image load successful
     * @param time the time in milliseconds it took from request to finish
     */
    public static void operation(String url, String specKey, LoadedFrom from, boolean successful, long time) {
        if (mLogcatEnabled) {
            String msg = FILUtils.format("Operation: LoadImage [{}] [{}] [{}] [{}]", from, specKey, successful, time);
            Log.println(from == LoadedFrom.MEMORY ? Log.DEBUG : Log.INFO, TAG, msg);
        }
        if (mAppender != null)
            mAppender.imageLoadOperation(url, specKey, from, successful, time);
    }

    /**
     * Image download operation complete.
     *
     * @param url the url of the image
     * @param specKey the spec of the image load request
     * @param responseCode the response code of the download web request
     * @param time the time in milliseconds it took to download the image
     * @param bytes the number of bytes received if download was successful
     * @param error optional: if download failed will contain the error
     */
    public static void operation(String url, String specKey, int responseCode, long time, long bytes, Throwable error) {
        if (mLogcatEnabled) {
            String msg = FILUtils.format("Operation: DownloadImage [{}] [{}] [{}] [{}] [{}]", url, specKey, responseCode, bytes, time);
            if (error == null) {
                Log.i(TAG, msg);
            } else {
                Log.e(TAG, msg, error);
            }
        }
        if (mAppender != null)
            mAppender.imageDownloadOperation(url, specKey, responseCode, time, bytes, error);
    }

    public static void debug(String msg) {
        if (mLogLevel <= Log.DEBUG) {
            if (mLogcatEnabled)
                Log.d(TAG, msg);
            if (mAppender != null)
                mAppender.log(Log.DEBUG, TAG, msg, null);
        }
    }

    public static void debug(String msg, Object arg1) {
        if (mLogLevel <= Log.DEBUG) {
            if (mLogcatEnabled)
                Log.d(TAG, FILUtils.format(msg, arg1));
            if (mAppender != null)
                mAppender.log(Log.DEBUG, TAG, FILUtils.format(msg, arg1), null);
        }
    }

    public static void debug(String msg, Object arg1, Object arg2) {
        if (mLogLevel <= Log.DEBUG) {
            if (mLogcatEnabled)
                Log.d(TAG, FILUtils.format(msg, arg1, arg2));
            if (mAppender != null)
                mAppender.log(Log.DEBUG, TAG, FILUtils.format(msg, arg1, arg2), null);
        }
    }

    public static void debug(String msg, Object arg1, Object arg2, Object arg3) {
        if (mLogLevel <= Log.DEBUG) {
            if (mLogcatEnabled)
                Log.d(TAG, FILUtils.format(msg, arg1, arg2, arg3));
            if (mAppender != null)
                mAppender.log(Log.DEBUG, TAG, FILUtils.format(msg, arg1, arg2, arg3), null);
        }
    }

    public static void debug(String msg, Object arg1, Object arg2, Object arg3, Object arg4) {
        if (mLogLevel <= Log.DEBUG) {
            if (mLogcatEnabled)
                Log.d(TAG, FILUtils.format(msg, arg1, arg2, arg3, arg4));
            if (mAppender != null)
                mAppender.log(Log.DEBUG, TAG, FILUtils.format(msg, arg1, arg2, arg3, arg4), null);
        }
    }

    public static void info(String msg) {
        if (mLogLevel <= Log.INFO) {
            if (mLogcatEnabled)
                Log.i(TAG, msg);
            if (mAppender != null)
                mAppender.log(Log.INFO, TAG, msg, null);
        }
    }

    public static void info(String msg, Object arg1) {
        if (mLogLevel <= Log.INFO) {
            if (mLogcatEnabled)
                Log.i(TAG, FILUtils.format(msg, arg1));
            if (mAppender != null)
                mAppender.log(Log.INFO, TAG, FILUtils.format(msg, arg1), null);
        }
    }

    public static void info(String msg, Object arg1, Object arg2) {
        if (mLogLevel <= Log.INFO) {
            if (mLogcatEnabled)
                Log.i(TAG, FILUtils.format(msg, arg1, arg2));
            if (mAppender != null)
                mAppender.log(Log.INFO, TAG, FILUtils.format(msg, arg1, arg2), null);
        }
    }

    public static void info(String msg, Object arg1, Object arg2, Object arg3) {
        if (mLogLevel <= Log.INFO) {
            if (mLogcatEnabled)
                Log.i(TAG, FILUtils.format(msg, arg1, arg2, arg3));
            if (mAppender != null)
                mAppender.log(Log.INFO, TAG, FILUtils.format(msg, arg1, arg2, arg3), null);
        }
    }

    public static void info(String msg, Object... args) {
        if (mLogLevel <= Log.INFO) {
            if (mLogcatEnabled)
                Log.i(TAG, FILUtils.format(msg, args));
            if (mAppender != null)
                mAppender.log(Log.INFO, TAG, FILUtils.format(msg, args), null);
        }
    }

    public static void warn(String msg, Object... args) {
        if (mLogLevel <= Log.WARN) {
            if (mLogcatEnabled)
                Log.w(TAG, FILUtils.format(msg, args));
            if (mAppender != null)
                mAppender.log(Log.WARN, TAG, FILUtils.format(msg, args), null);
        }
    }

    public static void warn(String msg, Throwable e, Object... args) {
        if (mLogLevel <= Log.WARN) {
            if (mLogcatEnabled)
                Log.w(TAG, FILUtils.format(msg, args), e);
            if (mAppender != null)
                mAppender.log(Log.WARN, TAG, FILUtils.format(msg, args), e);
        }
    }

    public static void error(String msg) {
        if (mLogLevel <= Log.ERROR) {
            if (mLogcatEnabled)
                Log.e(TAG, msg);
            if (mAppender != null)
                mAppender.log(Log.ERROR, TAG, msg, null);
        }
    }

    public static void error(String msg, Object... args) {
        if (mLogLevel <= Log.ERROR) {
            msg = FILUtils.format(msg, args);
            if (mLogcatEnabled)
                Log.e(TAG, msg);
            if (mAppender != null)
                mAppender.log(Log.ERROR, TAG, msg, null);
        }
    }

    public static void error(String msg, Throwable e, Object... args) {
        if (mLogLevel <= Log.ERROR) {
            msg = FILUtils.format(msg, args);
            if (mLogcatEnabled)
                Log.e(TAG, msg, e);
            if (mAppender != null)
                mAppender.log(Log.ERROR, TAG, msg, e);
        }
    }

    public static void critical(String msg, Object... args) {
        if (mLogLevel <= Log.ASSERT) {
            msg = FILUtils.format(msg, args);
            Exception e = new Exception(msg);
            if (mLogcatEnabled)
                Log.e(TAG, msg, e);
            if (mAppender != null)
                mAppender.log(Log.ASSERT, TAG, msg, e);
        }
    }

    public static void critical(String msg, Throwable e, Object... args) {
        if (mLogLevel <= Log.ASSERT) {
            msg = FILUtils.format(msg, args);
            if (mLogcatEnabled)
                Log.e(TAG, msg, e);
            if (mAppender != null)
                mAppender.log(Log.ASSERT, TAG, msg, e);
        }
    }
} public final class FILUtils {

    /**
     * Reuse Rect object
     */
    public static final Rect rect = new Rect();

    /**
     * Reuse RectF object
     */
    public static final RectF rectF = new RectF();

    /**
     * The ID of the main thread of the application, used to know if currently execution on main thread.
     */
    public static long MainThreadId;

    /**
     * Validate that the current executing thread is the main Android thread for the app.
     *
     * @throws RuntimeException current thread is not main thread.
     */
    public static void verifyOnMainThread() {
        if (!isOnMainThread())
            throw new RuntimeException("Access to this method must be on main thread only");
    }

    /**
     * Returns true if the current executing thread is the main Android app thread, false otherwise.
     */
    public static boolean isOnMainThread() {
        return Thread.currentThread().getId() == MainThreadId;
    }

    /**
     * Validate given argument isn't null.
     *
     * @param arg argument to validate
     * @param argName name of the argument to show in error message
     * @throws IllegalArgumentException
     */
    public static void notNull(Object arg, String argName) {
        if (arg == null) {
            throw new IllegalArgumentException("argument is null: " + argName);
        }
    }

    /**
     * Validate given string argument isn't null or empty string.
     *
     * @param arg argument to validate
     * @param argName name of the argument to show in error message
     * @throws IllegalArgumentException
     */
    public static void notNullOrEmpty(String arg, String argName) {
        if (arg == null || arg.length() < 1) {
            throw new IllegalArgumentException("argument is null: " + argName);
        }
    }

    /**
     * Validate given array argument isn't null or empty string.
     *
     * @param arg argument to validate
     * @param argName name of the argument to show in error message
     * @throws IllegalArgumentException
     */
    public static <T> void notNullOrEmpty(T[] arg, String argName) {
        if (arg == null || arg.length < 1) {
            throw new IllegalArgumentException("argument is null: " + argName);
        }
    }

    /**
     * Validate given collection argument isn't null or empty string.
     *
     * @param arg argument to validate
     * @param argName name of the argument to show in error message
     * @throws IllegalArgumentException
     */
    public static <T> void notNullOrEmpty(Collection<T> arg, String argName) {
        if (arg == null || arg.size() < 1) {
            throw new IllegalArgumentException("argument is null: " + argName);
        }
    }

    /**
     * Safe parse the given string to long value.
     */
    public static long parseLong(String value, long defaultValue) {
        if (!TextUtils.isEmpty(value)) {
            try {
                return Integer.parseInt(value);
            } catch (Exception ignored) {
            }
        }
        return defaultValue;
    }

    /**
     * combine two path into a single path with File.separator.
     * Handle all cases where the separator already exists.
     */
    public static String pathCombine(String path1, String path2) {
        if (path2 == null)
            return path1;
        else if (path1 == null)
            return path2;

        path1 = path1.trim();
        path2 = path2.trim();
        if (path1.endsWith(File.separator)) {
            if (path2.startsWith(File.separator))
                return path1 + path2.substring(1);
            else
                return path1 + path2;
        } else {
            if (path2.startsWith(File.separator))
                return path1 + path2;
            else
                return path1 + File.separator + path2;
        }
    }

    /**
     * Close the given closeable object (Stream) in a safe way: check if it is null and catch-log
     * exception thrown.
     *
     * @param closeable the closable object to close
     */
    public static void closeSafe(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                FILLogger.warn("Failed to close closable object [{}]", e, closeable);
            }
        }
    }

    /**
     * Delete the given file in a safe way: check if it is null and catch-log exception thrown.
     *
     * @param file the file to delete
     */
    public static void deleteSafe(File file) {
        if (file != null) {
            try {
                //noinspection ResultOfMethodCallIgnored
                file.delete();
            } catch (Throwable e) {
                FILLogger.warn("Failed to delete file [{}]", e, file);
            }
        }
    }

    /**
     * Format the given <i>format</i> string by replacing {} placeholders with given arguments.
     */
    public static String format(String format, Object arg1) {
        return replacePlaceHolder(format, arg1);
    }

    /**
     * Format the given <i>format</i> string by replacing {} placeholders with given arguments.
     */
    public static String format(String format, Object arg1, Object arg2) {
        format = replacePlaceHolder(format, arg1);
        return replacePlaceHolder(format, arg2);
    }

    /**
     * Format the given <i>format</i> string by replacing {} placeholders with given arguments.
     */
    public static String format(String format, Object arg1, Object arg2, Object arg3) {
        format = replacePlaceHolder(format, arg1);
        format = replacePlaceHolder(format, arg2);
        return replacePlaceHolder(format, arg3);
    }

    /**
     * Format the given <i>format</i> string by replacing {} placeholders with given arguments.
     */
    public static String format(String format, Object arg1, Object arg2, Object arg3, Object arg4) {
        format = replacePlaceHolder(format, arg1);
        format = replacePlaceHolder(format, arg2);
        format = replacePlaceHolder(format, arg3);
        return replacePlaceHolder(format, arg4);
    }

    /**
     * Format the given <i>format</i> string by replacing {} placeholders with given arguments.
     */
    public static String format(String format, Object... args) {
        for (Object arg : args) {
            format = replacePlaceHolder(format, arg);
        }
        return format;
    }

    /**
     * Replace the first occurrence of {} placeholder in <i>format</i> string by <i>arg1</i> toString() value.
     */
    private static String replacePlaceHolder(String format, Object arg1) {
        int idx = format.indexOf("{}");
        if (idx > -1) {
            format = format.substring(0, idx) + arg1 + format.substring(idx + 2);
        }
        return format;
    }

    public static ThreadFactory threadFactory(final String name, final boolean daemon) {
        return new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                Thread result = new Thread(runnable, name);
                result.setDaemon(daemon);
                return result;
            }
        };
    }

}
import java.util.HashMap;
import java.util.Map;
import android.app.Application;
import android.content.ComponentCallbacks2;
import android.content.res.Configuration;
import android.os.Handler;
import android.text.TextUtils;
import com.theartofdev.fastimageloader.Decoder;
import com.theartofdev.fastimageloader.DiskCache;
import com.theartofdev.fastimageloader.Downloader;
import com.theartofdev.fastimageloader.ImageLoadSpec;
import com.theartofdev.fastimageloader.LoadedFrom;
import com.theartofdev.fastimageloader.MemoryPool;
import com.theartofdev.fastimageloader.ReusableBitmap;
import com.theartofdev.fastimageloader.Target;
import com.theartofdev.fastimageloader.impl.util.FILLogger;
import com.theartofdev.fastimageloader.impl.util.FILUtils;
import java.io.File;
// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"

package com.theartofdev.fastimageloader.impl;




/**
 * Handler for image loading using memory/disk cache and other features.
 */
public final class LoaderHandler implements DiskCacheImpl.Callback, DownloaderImpl.Callback, ComponentCallbacks2 {

    //region: Fields and Consts

    /**
     * map of url to image request running it to reuse if same image is requested again
     */
    private final Map<String, ImageRequest> mLoadingRequests = new HashMap<>();

    /**
     * Memory cache for images loaded
     */
    private final MemoryPool mMemoryPool;

    /**
     * Disk cache for images loaded
     */
    private final DiskCache mDiskCache;

    /**
     * Downloader to download images from the web
     */
    private final Downloader mDownloader;

    /**
     * Used to decode images from the disk to bitmap.
     */
    private final Decoder mDecoder;

    /**
     * Used to post execution to main thread.
     */
    private final Handler mHandler;

    /**
     * stats on the number of memory cache hits
     */
    private int mMemoryHits;

    /**
     * stats on the number of memory cache hits for alternative sepc
     */
    private int mMemoryAltHits;

    /**
     * stats on the number of disk cache hits
     */
    private int mDiskHits;

    /**
     * stats on the number of disk cache hits for alternative sepc
     */
    private int mDiskAltHits;

    /**
     * stats on the number of cache miss, network begin request
     */
    private int mNetworkRequests;

    /**
     * stats on the number of cache miss, network loaded
     */
    private int mNetworkLoads;
    //endregion

    /**
     * Init.
     *
     * @param decoder Used to decode images from the disk to bitmap.
     */
    public LoaderHandler(Application application,
                         MemoryPool memoryPool,
                         DiskCache diskCache,
                         Downloader downloader,
                         Decoder decoder) {
        FILUtils.notNull(application, "application");
        FILUtils.notNull(memoryPool, "memoryPool");
        FILUtils.notNull(diskCache, "diskCache");
        FILUtils.notNull(downloader, "downloader");
        FILUtils.notNull(decoder, "decoder");

        mMemoryPool = memoryPool;
        mDiskCache = diskCache;
        mDownloader = downloader;
        mDecoder = decoder;

        mHandler = new Handler(application.getMainLooper());

        application.registerComponentCallbacks(this);
    }

    /**
     * Create image report for analyses.
     */
    public String getReport() {
        StringBuilder sb = new StringBuilder();
        sb.append("Image handler report:");
        sb.append('\n');
        sb.append("Memory Hit: ").append(mMemoryHits).append('\n');
        sb.append("Memory alt Hit: ").append(mMemoryAltHits).append('\n');
        sb.append("Disk Hit: ").append(mDiskHits).append('\n');
        sb.append("Disk alt Hit: ").append(mDiskAltHits).append('\n');
        sb.append("Network Requests: ").append(mNetworkRequests).append('\n');
        sb.append("Network Loaded: ").append(mNetworkLoads).append('\n');
        sb.append('\n');
        //mMemoryPool.report(sb);
        sb.append('\n');
        //mDiskCache.report(sb);
        return sb.toString();
    }

    /**
     * Prefetch image (uri+spec) to be available in disk cache.<br>
     *
     * @param uri the URI of the image to prefetch
     * @param spec the spec to prefetch the image by
     */
    public void prefetchImage(String uri, ImageLoadSpec spec) {
        try {
            String imageKey = ImageRequest.getUriUniqueKey(spec, uri);
            ImageRequest request = mLoadingRequests.get(imageKey);
            if (request == null) {
                File file = mDiskCache.getCacheFile(uri, spec);
                if (!file.exists()) {
                    request = new ImageRequest(uri, spec, file);
                    mLoadingRequests.put(imageKey, request);

                    FILLogger.debug("Add prefetch request... [{}]", request);