<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/ImageDownloader.java<|end_filename|>
package com.guardanis.imageloader;

import android.os.Handler;

import java.io.File;

public class ImageDownloader implements Runnable {

    public interface DownloadEventListener {
        public void onDownloadCompleted(ImageDownloader request, String targetUrl);
        public void onDownloadFailed(ImageDownloader request, String targetUrl);
    }

    protected ImageRequest imageRequest;
    protected DownloadEventListener downloadEventListener;
    protected Handler handler;

    public ImageDownloader(Handler handler, ImageRequest imageRequest, DownloadEventListener downloadEventListener){
        this.handler = handler;
        this.imageRequest = imageRequest;
        this.downloadEventListener = downloadEventListener;
    }

    @Override
    public void run() {
        try{
            File downloadedFile = ImageLoader.getInstance(imageRequest.getContext())
                    .download(imageRequest);

            if(downloadedFile != null){
                handler.post(new Runnable(){
                    public void run() {
                        ImageUtils.log(imageRequest.getContext(),
                                "Download success: " + imageRequest.getTargetUrl());

                        downloadEventListener.onDownloadCompleted(ImageDownloader.this,
                                imageRequest.getTargetUrl());
                    }
                });

                return;
            }
        }
        catch(Throwable e){ e.printStackTrace(); }

        handler.post(new Runnable(){
            public void run() {
                ImageUtils.log(imageRequest.getContext(),
                        "Download failed: " + imageRequest.getTargetUrl());

                downloadEventListener.onDownloadFailed(ImageDownloader.this,
                        imageRequest.getTargetUrl());
            }
        });
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/transitions/drawables/TransitionDrawable.java<|end_filename|>
package com.guardanis.imageloader.transitions.drawables;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;

import com.guardanis.imageloader.R;
import com.guardanis.imageloader.stubs.AnimatedStubDrawable;
import com.guardanis.imageloader.stubs.StubDrawable;
import com.guardanis.imageloader.transitions.modules.TransitionModule;

import java.util.HashMap;
import java.util.Map;

import pl.droidsonroids.gif.GifDrawable;

public class TransitionDrawable extends BitmapDrawable {

    protected enum TransitionStage {
        AWAITING_START, TRANSITIONING, FINISHED;
    }

    protected Drawable oldDrawable;
    protected Drawable targetDrawable;

    protected Matrix baseCanvasMatrix = new Matrix();
    protected Matrix canvasMatrixOverride;

    protected Map<Class, TransitionModule> modules = new HashMap<Class, TransitionModule>();

    protected TransitionStage transitionStage = TransitionStage.AWAITING_START;
    protected long animationStart = System.currentTimeMillis();

    protected int overriddenMaxAlpha = 0xFF;

    public TransitionDrawable(Context context, Drawable from, Drawable to, Bitmap canvas) {
        super(context.getResources(), canvas);

        this.oldDrawable = from;
        this.targetDrawable = to;
    }

    public TransitionDrawable registerModule(TransitionModule module){
        if(module != null)
            modules.put(module.getClass(), module);

        return this;
    }

    public void start(Context context){
        this.animationStart = System.currentTimeMillis();
        this.transitionStage = TransitionStage.TRANSITIONING;

        invalidateSelf();

        if(targetDrawable instanceof GifDrawable && context.getResources().getBoolean(R.bool.ail__gif_auto_start_enabled))
            ((GifDrawable) targetDrawable).start();
    }

    public void overrideCanvasMatrix(Matrix canvasMatrixOverride){
        this.canvasMatrixOverride = canvasMatrixOverride;
    }

    @Override
    public void draw(Canvas canvas) {
        if(canvasMatrixOverride != null)
            canvas.setMatrix(canvasMatrixOverride);

        if(transitionStage == TransitionStage.TRANSITIONING){
            boolean unfinishedExists = false;

            for(TransitionModule module : modules.values())
                if(System.currentTimeMillis() < animationStart + module.getDuration())
                    unfinishedExists = true;

            if(unfinishedExists){
                updateModulesAndDraw(canvas, animationStart);

                invalidateSelf();
            }
            else{
                transitionStage = TransitionStage.FINISHED;
                oldDrawable = null;

                handlePostTransitionDrawing(canvas);
            }
        }
        else if(transitionStage == TransitionStage.AWAITING_START)
            updateOldAndDraw(canvas, System.currentTimeMillis());
        else handlePostTransitionDrawing(canvas);

        canvas.getMatrix(baseCanvasMatrix);
    }

    protected void updateModulesAndDraw(Canvas canvas, long startTime){
        updateOldAndDraw(canvas, startTime);

        canvas.save();

        for(TransitionModule module : modules.values())
            module.onPredrawTarget(this, canvas, targetDrawable, startTime);

        drawTarget(canvas);

        canvas.restore();
        safelyRevertTargetDrawables();
    }

    protected void updateOldAndDraw(Canvas canvas, long startTime){
        if (oldDrawable != null){
            canvas.save();

            for(TransitionModule module : modules.values())
                module.onPredrawOld(this, canvas, oldDrawable, startTime);

            drawOldDrawable(canvas);

            canvas.restore();
            safelyRevertOldDrawables();
        }
    }

    protected void drawOldDrawable(Canvas canvas){
        canvas.save();

        int[] translation = calculateStubTranslation(oldDrawable);

        canvas.translate(translation[0], translation[1]);

        oldDrawable.draw(canvas);

        canvas.restore();
    }

    protected void drawTarget(Canvas canvas){
        if(targetDrawable instanceof StubDrawable || targetDrawable instanceof GifDrawable)
            targetDrawable.draw(canvas);
        else super.draw(canvas);
    }

    protected void handlePostTransitionDrawing(Canvas canvas){
        updateModulesAndDraw(canvas, animationStart);

        if(targetDrawable instanceof AnimatedStubDrawable || targetDrawable instanceof GifDrawable)
            invalidateSelf();
    }

    protected int[] calculateStubTranslation(Drawable drawable){
        int halfXDistance = (Math.max(getBounds().right, drawable.getBounds().right) - Math.min(getBounds().right, drawable.getBounds().right)) / 2;
        if(getBounds().right < drawable.getBounds().right)
            halfXDistance *= -1;

        int halfYDistance = (Math.max(getBounds().bottom, drawable.getBounds().bottom) - Math.min(getBounds().bottom, drawable.getBounds().bottom)) / 2;
        if(getBounds().bottom < drawable.getBounds().bottom)
            halfYDistance *= -1;

        return new int[]{ halfXDistance, halfYDistance };
    }

    protected void safelyRevertOldDrawables(){
        for(TransitionModule module : modules.values()){
            try{
                module.revertPostDrawOld(this, oldDrawable);
            }
            catch(NullPointerException e){ } // Likely old drawable is just null
            catch(Throwable e){ e.printStackTrace(); }
        }
    }

    protected void safelyRevertTargetDrawables(){
        for(TransitionModule module : modules.values()){
            try{
                module.revertPostDrawTarget(this, targetDrawable);
            }
            catch(NullPointerException e){ e.printStackTrace(); }
            catch(Throwable e){ e.printStackTrace(); }
        }
    }

    @Override
    public void setAlpha(int alpha){
        int correctedAlpha = Math.min(alpha, overriddenMaxAlpha);

        super.setAlpha(correctedAlpha);
    }

    public void overrideMaxAlphaOut(int overriddenMaxAlpha){
        this.overriddenMaxAlpha = Math.min(overriddenMaxAlpha, this.overriddenMaxAlpha);

        if(oldDrawable != null && oldDrawable instanceof TransitionDrawable)
            ((TransitionDrawable) oldDrawable).overrideMaxAlphaOut(this.overriddenMaxAlpha);
    }

    public int getOverriddenMaxAlpha(){
        return overriddenMaxAlpha;
    }

    public Drawable getTargetDrawable(){
        return targetDrawable;
    }

    public Matrix getBaseCanvasMatrix(){
        return baseCanvasMatrix;
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/processors/ExternalImageProcessor.java<|end_filename|>
package com.guardanis.imageloader.processors;

import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;

import com.guardanis.imageloader.ImageRequest;
import com.guardanis.imageloader.filters.ImageFilter;

import java.util.List;

import pl.droidsonroids.gif.GifDrawable;

public class ExternalImageProcessor extends ImageFileProcessor {

    @Override
    public Drawable process(ImageRequest request, List<ImageFilter<Bitmap>> bitmapImageFilters) throws Exception {
        switch(request.getTargetImageType()){
            case GIF:
                return new GifDrawable(request.getOriginalRequestFile());
            case BITMAP:
            case SVG:
            default:
                return processBitmap(request, request.getOriginalRequestFile(), bitmapImageFilters);
        }
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/stubs/DefaultLoadingDrawable.java<|end_filename|>
package com.guardanis.imageloader.stubs;

import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;

import com.guardanis.imageloader.R;

public class DefaultLoadingDrawable extends AnimatedStubDrawable {

    protected static final int MIN_ARC = 5;
    protected static final int MAX_ARC = 360;
    protected static final float ARC_SPEED = 7.25f;

    protected static final float ROTATION_SPEED = 2.85f;

    protected LoadingDrawableState state;

    protected Paint loadingPaint = new Paint();
    protected float rotationDegrees = 0;
    protected float arcAngle = 270;
    protected RectF drawableBounds = new RectF();
    protected boolean arcModeLower = true;

    private int[] lastBounds = new int[]{ -1, -1 };

    protected int[] sizeRange;

    public DefaultLoadingDrawable(Resources resources){
        this(resources, resources.getColor(R.color.ail__default_stub_loading_tint));
    }

    public DefaultLoadingDrawable(Resources resources, int color){
        this.state = new LoadingDrawableState(resources);

        this.sizeRange = new int[]{ (int) resources.getDimension(R.dimen.ail__default_stub_loading_arc_min_size),
                (int) resources.getDimension(R.dimen.ail__default_stub_loading_arc_max_size)};

        setBounds(0, 0,
                (int) resources.getDimension(R.dimen.ail__default_stub_loading_bounds),
                (int) resources.getDimension(R.dimen.ail__default_stub_loading_bounds));

        setupPaints(resources, color);
    }

    protected void setupPaints(Resources resources, int color){
        loadingPaint.setAntiAlias(true);
        loadingPaint.setColor(color);
        loadingPaint.setStyle(Paint.Style.STROKE);
        loadingPaint.setStrokeWidth(resources.getDimension(R.dimen.ail__default_stub_loading_stroke_width));
    }

    @Override
    public void draw(Canvas canvas) {
        if(!(lastBounds[0] == canvas.getWidth() && lastBounds[1] == canvas.getHeight()))
            setupRect(canvas);

        if(canvasMatrixOverride != null)
            canvas.setMatrix(canvasMatrixOverride);

        canvas.save();

        canvas.rotate(rotationDegrees, canvas.getWidth() / 2, canvas.getHeight() / 2);

        canvas.drawArc(drawableBounds, 0, arcAngle, false, loadingPaint);

        canvas.restore();

        canvas.getMatrix(baseCanvasMatrix);

        updatePositions();
        invalidateSelf();
    }

    protected void setupRect(Canvas canvas){
        int desiredSize = Math.min(canvas.getWidth() / 8, canvas.getHeight() / 5) / 2;
        desiredSize = Math.min(desiredSize, sizeRange[1]);
        desiredSize = Math.max(desiredSize, sizeRange[0]);

        int[] center = new int[]{ canvas.getWidth() / 2,
                canvas.getHeight() / 2 };

        drawableBounds.left = center[0] - desiredSize;
        drawableBounds.right = center[0] + desiredSize;
        drawableBounds.top = center[1] - desiredSize;
        drawableBounds.bottom = center[1] + desiredSize;

        lastBounds = new int[]{ canvas.getWidth(),
                canvas.getHeight() };
    }

    protected void updatePositions(){
        rotationDegrees = (rotationDegrees + ROTATION_SPEED) % 360;

        if(arcModeLower){
            arcAngle -= ARC_SPEED;

            if(arcAngle <= MIN_ARC)
                arcModeLower = false;
        }
        else {
            arcAngle += ARC_SPEED;

            if(MAX_ARC <= arcAngle)
                arcModeLower = true;
        }
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSPARENT;
    }

    @Override
    public void setAlpha(int alpha) {
        loadingPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter filter) {
        loadingPaint.setColorFilter(filter);
    }

    @Override
    public ConstantState getConstantState() {
        state.changingConfigurations = super.getChangingConfigurations();
        return state;
    }

    final static class LoadingDrawableState extends Drawable.ConstantState {

        private Resources resources;
        private int changingConfigurations;

        public LoadingDrawableState(Resources resources){
            this.resources = resources;
        }

        @Override
        public Drawable newDrawable() {
            return new DefaultLoadingDrawable(resources);
        }

        @Override
        public Drawable newDrawable(Resources resources) {
            return new DefaultLoadingDrawable(resources);
        }

        @Override
        public int getChangingConfigurations() {
            return changingConfigurations;
        }
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/ImageLoader.java<|end_filename|>
package com.guardanis.imageloader;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.view.View;

import com.guardanis.imageloader.stubs.builders.DefaultErrorStubBuilder;
import com.guardanis.imageloader.stubs.builders.DefaultLoadingStubBuilder;
import com.guardanis.imageloader.stubs.builders.EmptyStubBuilder;
import com.guardanis.imageloader.stubs.builders.StubBuilder;

import java.io.File;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ImageLoader implements ImageDownloader.DownloadEventListener {

    private static ImageLoader instance = null;

    public static synchronized ImageLoader getInstance(Context context) {
        if(instance == null)
            instance = new ImageLoader(context);

        return instance;
    }

    private static final String PREFS = "ImageLoaderPrefs";
    private static final String PREF__STUB_LOADER = "stub__loader";
    private static final String PREF__STUB_ERROR = "stub__error";

    protected Context context;
    protected FileCache fileCache;
    protected Map<View, String> views = Collections.synchronizedMap(new WeakHashMap<View, String>());

    protected Map<String, ImageDownloader> downloadRequests = new HashMap<String, ImageDownloader>();
    protected Map<String, List<ImageRequest>> delayedRequests = new HashMap<String, List<ImageRequest>>();
    protected ExecutorService executorService;

    private StubBuilder loadingStubBuilder;
    private StubBuilder errorStubBuilder;

    private final Handler handler = new Handler(Looper.getMainLooper());

    protected ImageLoader(Context context) {
        this.context = context.getApplicationContext();
        this.fileCache = new FileCache(context);

        this.executorService = Executors.newFixedThreadPool(context.getResources()
                .getInteger(R.integer.ail__thread_pool_size));

        this.loadingStubBuilder = loadStubBuilderClass(PREF__STUB_LOADER, DefaultLoadingStubBuilder.class);
        this.errorStubBuilder = loadStubBuilderClass(PREF__STUB_ERROR, DefaultErrorStubBuilder.class);
    }

    private <T extends StubBuilder> StubBuilder loadStubBuilderClass(String key, Class<T> defaultClass){
        try{
            return (StubBuilder) Class.forName(context.getSharedPreferences(PREFS, 0)
                    .getString(key, defaultClass.getName()))
                    .newInstance();
        }
        catch(Throwable e){
            ImageUtils.log(context, e.getMessage());

            return new EmptyStubBuilder();
        }
    }

    /**
     * Register a default loading StubBuilder class. Class must contain an empty constructor.
     */
    public <T extends StubBuilder> ImageLoader registerLoadingStub(Class<T> stubBuilderClass){
        try{
            StubBuilder builder = (StubBuilder) Class.forName(stubBuilderClass.getName())
                    .newInstance();

            this.loadingStubBuilder = builder;
        }
        catch(Throwable e){
            ImageUtils.log(context, e.getMessage());

            throw new RuntimeException(e);
        }

        context.getSharedPreferences(PREFS, 0)
                .edit()
                .putString(PREF__STUB_LOADER, stubBuilderClass.getName())
                .commit();

        return this;
    }

    /**
     * Register a default error StubBuilder class. Class must contain an empty constructor.
     */
    public <T extends StubBuilder> ImageLoader registerErrorStub(Class<T> stubBuilderClass){
        try{
            StubBuilder builder = (StubBuilder) Class.forName(stubBuilderClass.getName())
                    .newInstance();

            this.errorStubBuilder = builder;
        }
        catch(Throwable e){
            ImageUtils.log(context, e.getMessage());

            throw new RuntimeException(e);
        }

        context.getSharedPreferences(PREFS, 0)
                .edit()
                .putString(PREF__STUB_ERROR, stubBuilderClass.getName())
                .commit();

        return this;
    }

    /**
     * Submit an ImageRequest to the ImageLoader's downloading service for managed downloads
     */
    public void submit(ImageRequest request){
        if(request.isTargetLocal()
                || (isImageDownloaded(request) && fileCache.isCachedFileValid(request.getTargetUrl(), request.getMaxCacheDurationMs())))
            executorService.submit(request);
        else submitDownloadRequest(request);
    }

    protected void submitDownloadRequest(ImageRequest request){
        if(delayedRequests.get(request.getTargetUrl()) == null)
            delayedRequests.put(request.getTargetUrl(), new ArrayList<ImageRequest>());

        delayedRequests.get(request.getTargetUrl())
                .add(request);

        context.getSharedPreferences(PREFS, 0)
                .edit()
                .putBoolean(request.getTargetUrl(), false)
                .commit(); // Prevent deletions from messing up

        if(downloadRequests.get(request.getTargetUrl()) == null){
            ImageDownloader downloadRequest = new ImageDownloader(handler, request, this);
            downloadRequests.put(request.getTargetUrl(), downloadRequest);

            executorService.submit(downloadRequest);
        }
    }

    @Override
    public synchronized void onDownloadCompleted(ImageDownloader downloadRequest, String targetUrl) {
        context.getSharedPreferences(PREFS, 0)
                .edit()
                .putBoolean(targetUrl, true)
                .commit();

        if(delayedRequests.get(targetUrl) != null){
            for(ImageRequest request : delayedRequests.get(targetUrl))
                executorService.submit(request);

            delayedRequests.remove(targetUrl);
        }

        downloadRequests.remove(targetUrl);
    }

    @Override
    public synchronized void onDownloadFailed(ImageDownloader downloadRequest, String targetUrl) {
        if(delayedRequests.get(targetUrl) != null){
            // For now, just execute pending requests, as they won't do anything but set appropriate stub
            for(ImageRequest request : delayedRequests.get(targetUrl))
                executorService.submit(request);

            delayedRequests.remove(targetUrl);
        }

        downloadRequests.remove(targetUrl);
    }

    /**
     * Perform the download for the supplied ImageRequest and return the File containing the result
     */
    public File download(ImageRequest request){
        try{
            URL downloadUrl = getCorrectDownloadUrl(request.getTargetUrl());

            HttpURLConnection conn = openImageDownloadConnection(downloadUrl);
            setHttpRequestProperties(request, conn);

            return download(conn, request.getOriginalRequestFile());
        }
        catch(Throwable ex){ ex.printStackTrace(); }

        return null;
    }

    @Deprecated
    public File download(String url, File file) {
        try{
            URL downloadUrl = getCorrectDownloadUrl(url);
            HttpURLConnection connection = openImageDownloadConnection(downloadUrl);

            return download(connection, file);
        }
        catch(Throwable ex){ ex.printStackTrace(); }

        return null;
    }

    /**
     * Perform the download of the supplied HttpURLConnection into the File parameter
     * supplied and return the File containing the result (same as param).
     */
    public File download(HttpURLConnection conn, File file){
        InputStream is = null;

        try{
            is = conn.getInputStream();
            ImageUtils.saveStream(file, is);

            return file;
        }
        catch(Throwable ex){ ex.printStackTrace(); }
        finally{ closeConnection(is); }

        return null;
    }

    protected URL getCorrectDownloadUrl(String url) throws Exception {
        URL imageUrl = new URL(url);

        for(String regex : Arrays.asList(context.getResources()
                .getStringArray(R.array.ail__known_location_redirects_regex))){
            Matcher matcher = Pattern.compile(regex)
                    .matcher(url);

            if(matcher.find())
                return getAdjustedLocationImageUrl(imageUrl);
        }

        return imageUrl;
    }

    protected URL getAdjustedLocationImageUrl(URL imageUrl) throws Exception {
        HttpURLConnection.setFollowRedirects(false);
        HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection();

        String locationHeader = conn.getHeaderField("Location");

        conn.disconnect();

        HttpURLConnection.setFollowRedirects(true);

        if (locationHeader == null)
            return imageUrl;

        return new URL(locationHeader);
    }

    protected HttpURLConnection openImageDownloadConnection(URL url) throws Exception {
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(context.getResources().getInteger(R.integer.ail__connection_timeout_ms));
        conn.setReadTimeout(context.getResources().getInteger(R.integer.ail__read_timeout_ms));
        conn.setInstanceFollowRedirects(true);

        return conn;
    }

    protected void setHttpRequestProperties(ImageRequest request, HttpURLConnection conn){
        Map<String, String> requestParams = request.getHttpRequestParams();

        for(String key : requestParams.keySet())
            conn.setRequestProperty(key, requestParams.get(key));
    }

    public static void closeConnection(InputStream is) {
        try{
            if(is != null)
                is.close();
        }
        catch(Exception e){ }
    }

    public void claimViewTarget(ImageRequest request) {
        claimViewTarget(request.getTargetView(), request.getTargetUrl(), request.getStartedAtMs());
    }

    private void claimViewTarget(View view, String targetUrl, long startedAtMs) {
        views.put(view, combineViewNameParams(targetUrl, startedAtMs));
    }

    public boolean isViewStillUsable(ImageRequest request) {
        return isViewStillUsable(request.getTargetView(), request.getTargetUrl(), request.getStartedAtMs());
    }

    private boolean isViewStillUsable(View target, String targetUrl, long startedAtMs) {
        return combineViewNameParams(targetUrl, startedAtMs)
                .equals(views.get(target));
    }

    private String combineViewNameParams(String targetUrl, long startedAtMs) {
        return targetUrl + "_" + startedAtMs;
    }

    public FileCache getFileCache() {
        return fileCache;
    }

    public boolean isImageDownloaded(ImageRequest request) {
        return request.getOriginalRequestFile().exists()
                && context.getSharedPreferences(PREFS, 0).getBoolean(request.getTargetUrl(), false);
    }

    public Handler getHandler() {
        return handler;
    }

    public StubBuilder getLoadingStubBuilder() {
        return loadingStubBuilder;
    }

    public StubBuilder getErrorStubBuilder() {
        return errorStubBuilder;
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/filters/BitmapColorFilter.java<|end_filename|>
package com.guardanis.imageloader.filters;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;

public class BitmapColorFilter extends ImageFilter<Bitmap> {

    private ColorFilter filter;

    public BitmapColorFilter(Context context, ColorFilter filter) {
        super(context);
        this.filter = filter;
    }

    @Override
    public Bitmap filter(Bitmap unedited) {
        if(unedited != null){
            Bitmap adjusted = Bitmap.createBitmap(unedited.getWidth(),
                    unedited.getHeight(),
                    Bitmap.Config.ARGB_8888);

            Paint paint = new Paint();
            paint.setColorFilter(filter);

            Canvas canvas = new Canvas(unedited);
            canvas.drawBitmap(adjusted, 0, 0, paint);
        }

        return unedited;
    }

    @Override
    public String getAdjustmentInfo(){
        return getClass().getSimpleName() + "_" + filter.getClass().getSimpleName(); // may not be enough...
    }
}


<|start_filename|>imageloader-sample/src/main/java/com/guardanis/imageloader/sample/MainActivity.kt<|end_filename|>
package com.guardanis.imageloader.sample

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView
import android.util.Log
import android.view.View
import com.guardanis.imageloader.ImageRequest

class MainActivity: AppCompatActivity() {

    private val TAG = "imageloader"

    override fun onCreate(savedInstance: Bundle?) {
        super.onCreate(savedInstance)

        setContentView(R.layout.activity_main)
        standardClicked(null)
        vectorAssetClicked(null)
    }

    fun standardClicked(view: View?) {
        loadImages("https://upload.wikimedia.org/wikipedia/commons/d/d9/Test.png")
    }

    fun svgClicked(view: View?) {
        loadImages("https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Test.svg/1024px-Test.svg.png")
    }

    fun gifClicked(view: View?) {
        loadImages("https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")
    }

    fun vectorAssetClicked(view: View?) {
        ImageRequest.create(findViewById<AppCompatImageView>(R.id.main__test__vector_medium))
                .setTargetDrawable(R.drawable.ic_android_test)
                .setFadeTransition(150)
                .setTranslateTransition(-1f, -1f, 350)
                .execute()

        ImageRequest.create(findViewById<AppCompatImageView>(R.id.main__test__vector_large))
                .setTargetDrawable(R.drawable.ic_android_test)
                .setFadeTransition(150)
                .setScaleTransition(0f, 1f, 750)
                .execute()
    }

    private fun loadImages(url: String) {
        ImageRequest.create(findViewById<AppCompatImageView>(R.id.main__test_small))
                .setTargetUrl(url)
                .setFadeTransition(150)
                .setTranslateTransition(-1f, -1f, 350)
                .execute()

        ImageRequest.create(findViewById<AppCompatImageView>(R.id.main__test_medium))
                .setTargetUrl(url)
                .setFadeTransition(150)
                .setScaleTransition(0f, 1f, 750)
                .execute()

        ImageRequest.create(findViewById<AppCompatImageView>(R.id.main__test_large))
                .setTargetUrl(url)
                .setFadeTransition(150)
                .setTranslateTransition(1f, 1f, 350)
                .setMaxCacheDurationMs(1)
                .setRequestStartedCallback({
                    Log.d(TAG, "Main test started for $url")
                })
                .setSuccessCallback({ _, _ ->
                    Log.d(TAG, "Main test success for $url")
                })
                .setErrorCallback({ _, error->
                    Log.d(TAG, "Main test error for $url")

                    error?.printStackTrace()
                })
                .execute()
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/processors/ImageAssetProcessor.java<|end_filename|>
package com.guardanis.imageloader.processors;

import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;

import com.guardanis.imageloader.ImageRequest;
import com.guardanis.imageloader.ImageUtils;
import com.guardanis.imageloader.filters.ImageFilter;

import java.io.File;
import java.util.List;

import pl.droidsonroids.gif.GifDrawable;

public class ImageAssetProcessor extends ImageFileProcessor {

    @Override
    public Drawable process(ImageRequest request, List<ImageFilter<Bitmap>> bitmapImageFilters) throws Exception {
        switch(request.getTargetImageType()){
            case GIF:
                return new GifDrawable(request.getContext()
                        .getAssets()
                        .openFd(request.getTargetUrl()));
            case SVG:
            case BITMAP:
            default:
                return processAsset(request, bitmapImageFilters);
        }
    }

    protected Drawable processAsset(ImageRequest request, List<ImageFilter<Bitmap>> bitmapImageFilters) throws Exception {
        File editedImageFile = request.getEditedRequestFile();
        int requiredImageWidth = request.getTargetImageWidth();

        if(editedImageFile.exists())
            return decodeBitmapDrawable(request.getContext(),
                    editedImageFile,
                    requiredImageWidth);

        Bitmap asset = request.getTargetImageType() == ImageUtils.ImageType.SVG
                ? ImageUtils.decodeSVGAsset(request.getContext(), request.getTargetUrl(), requiredImageWidth)
                : ImageUtils.decodeBitmapAsset(request.getContext(), request.getTargetUrl(), requiredImageWidth);

        return process(request.getContext(), asset, editedImageFile, bitmapImageFilters);
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/ImageUtils.java<|end_filename|>
package com.guardanis.imageloader;

import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import androidx.annotation.DrawableRes;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import android.util.Log;

import com.caverock.androidsvg.SVG;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Locale;

public class ImageUtils {

    public enum ImageType {
        BITMAP, SVG, GIF, CUSTOM;
    }

    private static final int MAX_REDUCTION_FACTOR = 16;

    private static final String TAG = "AIL";

    protected ImageUtils() { }

    public static void saveStream(File file, InputStream is) throws Exception {
        OutputStream os = new FileOutputStream(file);
        copyStream(is, os);
        os.close();
    }

    public static void saveBitmapAsync(final Context context, final File imageFile, final Bitmap bitmap) {
        try{
            if(bitmap == null || !(isInternalStorageMounted() || safelyCreateImageFile(imageFile)))
                return;

            new Thread(new Runnable(){
                public void run(){
                    try{
                        File temp = new FileCache(context)
                                .getFile(System.currentTimeMillis() + "_temp_" + imageFile.getAbsolutePath());

                        bitmap.compress(Bitmap.CompressFormat.PNG, 100, new FileOutputStream(temp));

                        temp.renameTo(imageFile);
                    }
                    catch(Exception e){ e.printStackTrace(); }
                }
            }).start();
        }
        catch(Exception e){ e.printStackTrace(); }
    }

    private static boolean isInternalStorageMounted() {
        return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY)
                || Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    }

    private static boolean safelyCreateImageFile(File imageFile) throws Exception {
        return imageFile.exists()
                || imageFile.getParentFile().mkdirs()
                || imageFile.createNewFile();
    }

    public static void copyStream(InputStream is, OutputStream os) {
        final int buffer_size = 1024;

        try{
            byte[] bytes = new byte[buffer_size];
            for(; ;){
                int count = is.read(bytes, 0, buffer_size);

                if(count == -1)
                    break;

                os.write(bytes, 0, count);
            }
        }
        catch(Exception ex){ }
    }

    public static Bitmap decodeFile(File file, int requiredWidth) {
        if(file.getName().toLowerCase(Locale.US).endsWith(".svg"))
            return decodeSvgFile(file, requiredWidth);
        else return decodeBitmapFile(file, requiredWidth, 1);
    }

    public static Bitmap decodeSVGAsset(Context context, String assetFileName) {
        return decodeSVGAsset(context, assetFileName, -1);
    }

    public static Bitmap decodeSVGAsset(Context context, String assetFileName, int requiredWidth) {
        try{
            SVG svg = SVG.getFromAsset(context.getAssets(), assetFileName);
            return decodeBitmap(svg, requiredWidth);
        }
        catch(OutOfMemoryError e){ e.printStackTrace(); }
        catch(Exception e){ e.printStackTrace(); }

        return null;
    }

    private static Bitmap decodeSvgFile(File file, int requiredWidth) {
        try{
            return decodeSvg(new FileInputStream(file),
                    requiredWidth);
        }
        catch(Throwable e){ e.printStackTrace(); }

        return null;
    }

    public static Bitmap decodeSvgResource(Context context, int resourceId, int requiredWidth) {
        try{
            return decodeSvg(context.getResources().openRawResource(resourceId),
                    requiredWidth);
        }
        catch(Throwable e){ e.printStackTrace(); }

        return null;
    }

    private static Bitmap decodeSvg(InputStream stream, int requiredWidth) {
        try{
            Bitmap bitmap = decodeBitmap(SVG.getFromInputStream(stream),
                    requiredWidth);

            stream.close();
            return bitmap;
        }
        catch(Throwable e){ e.printStackTrace(); }

        return null;
    }

    public static Bitmap decodeBitmap(SVG svg, int requiredWidth) {
        float documentWidth = svg.getDocumentWidth();
        float documentHeight = svg.getDocumentHeight();

        if((documentWidth == -1 || documentHeight == -1) && (svg.getDocumentViewBox() != null && 0 < svg.getDocumentViewBox().width())){
            documentWidth = svg.getDocumentViewBox().width();
            documentHeight = svg.getDocumentViewBox().height();
        }

        if(documentWidth != -1){
            if(requiredWidth < 1)
                requiredWidth = (int) documentWidth;

            float scaleFactor = (float) requiredWidth / documentWidth;
            int adjustedHeight = (int) (scaleFactor * documentHeight);

            Bitmap newBitmap = Bitmap.createBitmap(requiredWidth,
                    adjustedHeight,
                    Bitmap.Config.ARGB_8888);

            Canvas canvas = new Canvas(newBitmap);

            canvas.drawPicture(svg.renderToPicture(),
                    new Rect(0, 0, requiredWidth, adjustedHeight));

            return newBitmap;
        }

        return null;
    }

    private static Bitmap decodeBitmapFile(File file, int requiredWidth, int reductionFactor) {
        try{
            BitmapFactory.Options options = decodeOptions(new FileInputStream(file));

            return decodeBitmap(new FileInputStream(file),
                    options,
                    requiredWidth,
                    reductionFactor);
        }
        catch(OutOfMemoryError e){
            e.printStackTrace();

            System.gc();

            return reductionFactor < MAX_REDUCTION_FACTOR
                    ? decodeBitmapFile(file, requiredWidth, reductionFactor < 1 ? 2 : reductionFactor * 2)
                    : null;
        }
        catch(Exception e){ e.printStackTrace(); }

        return null;
    }

    public static Bitmap decodeBitmapAsset(Context context, String asset, int requiredWidth) {
        return decodeBitmapAsset(context, asset, requiredWidth, 1);
    }

    private static Bitmap decodeBitmapAsset(Context context, String asset, int requiredWidth, int reductionFactor) {
        try{
            BitmapFactory.Options options = decodeOptions(context.getAssets().open(asset));

            return decodeBitmap(context.getAssets().open(asset),
                    options,
                    requiredWidth,
                    reductionFactor);
        }
        catch(OutOfMemoryError e){
            e.printStackTrace();

            System.gc();

            return reductionFactor < MAX_REDUCTION_FACTOR
                    ? decodeBitmapAsset(context, asset, requiredWidth, reductionFactor < 1 ? 2 : reductionFactor * 2)
                    : null;
        }
        catch(Exception e){ e.printStackTrace(); }

        return null;
    }

    private static BitmapFactory.Options decodeOptions(InputStream stream) throws Exception {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeStream(stream, null, options);

        stream.close();

        return options;
    }

    private static Bitmap decodeBitmap(InputStream stream, BitmapFactory.Options options, int requiredWidth, int reductionFactor) throws Exception {
        int inSampleSize = calculateInSampleSize(options, requiredWidth, reductionFactor);

        options.inSampleSize = inSampleSize;
        options.inJustDecodeBounds = false;

        Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);

        stream.close();

        return bitmap;
    }

    public static Bitmap decodeDrawableResource(Context context, @DrawableRes int resId, int requiredWidth){
        Drawable drawable = ContextCompat.getDrawable(context, resId);

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
            drawable = DrawableCompat.wrap(drawable).mutate();

        double drawableAspectRatio = (double) drawable.getIntrinsicWidth() / (double) drawable.getIntrinsicHeight();

        Bitmap bitmap = Bitmap.createBitmap(requiredWidth, (int) (requiredWidth * drawableAspectRatio), Bitmap.Config.ARGB_8888);

        Canvas canvas = new Canvas(bitmap);

        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }

    public static Bitmap decodeBitmapResource(Resources resources, int resId, int requiredWidth){
        return decodeBitmapResource(resources, resId, requiredWidth, 1);
    }

    public static Bitmap decodeBitmapResource(Resources resources, int resId, int requiredWidth, int reductionFactor){
        try{
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(resources, resId, options);

            options.inSampleSize = calculateInSampleSize(options,
                    requiredWidth,
                    reductionFactor);

            options.inJustDecodeBounds = false;

            return BitmapFactory.decodeResource(resources, resId, options);
        }
        catch(OutOfMemoryError e){
            e.printStackTrace();

            System.gc();

            return reductionFactor < MAX_REDUCTION_FACTOR
                    ? decodeBitmapResource(resources, resId, requiredWidth, reductionFactor < 1 ? 2 : reductionFactor * 2)
                    : null;
        }
        catch(Exception e){ e.printStackTrace(); }

        return null;
    }

    public static int calculateInSampleSize(BitmapFactory.Options options, int requiredWidth, int reductionFactor) {
        int inSampleSize = 1;

        if(1 < requiredWidth && requiredWidth < options.outWidth)
            while((options.outWidth / inSampleSize) > requiredWidth)
                inSampleSize *= 2;

        return inSampleSize * reductionFactor;
    }

    public static StateListDrawable buildStateListDrawable(Context context, SVG svgAssetNormal, SVG svgAssetPressed) {
        return buildStateListDrawable(buildBitmapDrawable(context, svgAssetNormal),
                buildBitmapDrawable(context, svgAssetPressed));
    }

    public static StateListDrawable buildStateListDrawable(Drawable dNormal, Drawable dPressed) {
        StateListDrawable states = new StateListDrawable();
        states.addState(new int[]{ android.R.attr.state_pressed }, dPressed);
        states.addState(new int[]{ }, dNormal);

        return states;
    }

    public static BitmapDrawable buildBitmapDrawable(Context context, SVG svg) {
        return new BitmapDrawable(context.getResources(),
                decodeBitmap(svg, (int) svg.getDocumentWidth()));
    }

    public static void log(Context context, String message){
        if(context.getResources().getBoolean(R.bool.ail__debug_log_enabled))
            Log.d(TAG, message);
    }

    public static void log(Context context, Throwable e){
        if(context.getResources().getBoolean(R.bool.ail__debug_log_enabled))
            e.printStackTrace();
    }

    public static ImageType getImageType(Context context, String url) {
        url = url.toLowerCase(Locale.US);

        if(url.endsWith(".svg"))
            return ImageType.SVG;
        else if(url.endsWith(".gif"))
            return ImageType.GIF;

        try{
            Uri uri = Uri.parse(URLEncoder.encode(url, "UTF-8"));

            if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT) || uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)) {
                String mimeType = context.getContentResolver()
                        .getType(uri);

                return ImageType.valueOf(mimeType.toUpperCase(Locale.US));
            }
        }
        catch(Exception e){ }

        log(context, String.format("Mime type not found for: %1$s, Treating as a Bitmap", url));

        return ImageType.BITMAP;
    }

}

<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/transitions/modules/FadingTransitionModule.java<|end_filename|>
package com.guardanis.imageloader.transitions.modules;

import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.annotation.Nullable;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;

import com.guardanis.imageloader.stubs.StubDrawable;
import com.guardanis.imageloader.transitions.drawables.TransitionDrawable;

public class FadingTransitionModule extends TransitionModule {

    protected static final int TRANSITION_OUT_SPEED_MULTIPLIER = 2;

    protected int startAlpha;
    protected int endAlpha;

    protected int oldSDrawableStartingAlpha = 0xFF;
    protected int targetSDrawableStartingAlpha = 0xFF;

    public FadingTransitionModule(long duration) {
        this(0x00, 0xFF, duration);
    }

    public FadingTransitionModule(int startAlpha, int endAlpha, long duration) {
        super(duration);

        this.startAlpha = startAlpha;
        this.endAlpha = endAlpha;

        registerInterpolator(TransitionModule.INTERPOLATOR_OUT, new AccelerateInterpolator());
        registerInterpolator(TransitionModule.INTERPOLATOR_IN, new DecelerateInterpolator());
    }

    @Override
    public void onStart(@Nullable Drawable old, Drawable target) {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
            oldSDrawableStartingAlpha = old.getAlpha();
            targetSDrawableStartingAlpha = target.getAlpha();
        }
    }

    @Override
    public void onPredrawOld(TransitionDrawable transitionDrawable, Canvas canvas, @Nullable Drawable old, long startTime) {
        if(old != null){
            int alpha = (int) Math.max(endAlpha - (oldSDrawableStartingAlpha * interpolate(TransitionModule.INTERPOLATOR_OUT, startTime) * TRANSITION_OUT_SPEED_MULTIPLIER), 0);
            int correctedAlpha = Math.min(alpha, transitionDrawable.getOverriddenMaxAlpha());

            if(old instanceof TransitionDrawable)
                ((TransitionDrawable) old).overrideMaxAlphaOut(correctedAlpha);

            old.setAlpha(correctedAlpha);
        }
    }

    @Override
    public void revertPostDrawOld(TransitionDrawable transitionDrawable, @Nullable Drawable old) {
        if(old != null)
            old.setAlpha(oldSDrawableStartingAlpha);
    }

    @Override
    public void onPredrawTarget(TransitionDrawable transitionDrawable, Canvas canvas, Drawable target, long startTime) {
        int alpha = (int) (startAlpha + ((endAlpha - startAlpha) * interpolate(TransitionModule.INTERPOLATOR_IN, startTime)));

        if(target instanceof StubDrawable)
            target.setAlpha(alpha);
        else
            transitionDrawable.setAlpha(alpha);
    }

    @Override
    public void revertPostDrawTarget(TransitionDrawable transitionDrawable, Drawable target) {
        if(target instanceof StubDrawable)
            target.setAlpha(targetSDrawableStartingAlpha);
        else
            transitionDrawable.setAlpha(targetSDrawableStartingAlpha);
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/transitions/modules/RotationTransitionModule.java<|end_filename|>
package com.guardanis.imageloader.transitions.modules;

import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import android.view.animation.DecelerateInterpolator;

import com.guardanis.imageloader.stubs.StubDrawable;
import com.guardanis.imageloader.transitions.drawables.TransitionDrawable;

public class RotationTransitionModule extends TransitionModule {

    protected float rotateFrom;
    protected float rotateTo;
    protected float difference;

    public RotationTransitionModule(int from, int to, long duration) {
        super(duration);
        this.rotateFrom = from;
        this.rotateTo = to;
        this.difference = to - from;

        registerInterpolator(TransitionModule.INTERPOLATOR_IN, new DecelerateInterpolator());
    }

    @Override
    public void onStart(@Nullable Drawable old, Drawable target) { }

    @Override
    public void onPredrawOld(TransitionDrawable transitionDrawable, Canvas canvas, @Nullable Drawable old, long startTime) { }

    @Override
    public void revertPostDrawOld(TransitionDrawable transitionDrawable, @Nullable Drawable old) { }

    @Override
    public void onPredrawTarget(TransitionDrawable transitionDrawable, Canvas canvas, Drawable target, long startTime) {
        if(target instanceof StubDrawable)
            return;

        float percentCompleted = interpolate(TransitionModule.INTERPOLATOR_IN, startTime);

        canvas.rotate(rotateFrom + (percentCompleted * difference),
                transitionDrawable.getBitmap().getWidth() / 2,
                transitionDrawable.getBitmap().getHeight() / 2);
    }

    @Override
    public void revertPostDrawTarget(TransitionDrawable transitionDrawable, Drawable target) { }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/filters/BitmapRotationFilter.java<|end_filename|>
package com.guardanis.imageloader.filters;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;

import com.guardanis.imageloader.ImageUtils;

public class BitmapRotationFilter extends ImageFilter<Bitmap> {

    private int rotationDegrees;

    public BitmapRotationFilter(Context context, int rotationDegrees) {
        super(context);
        this.rotationDegrees = rotationDegrees;
    }

    @Override
    public Bitmap filter(Bitmap unedited) {
        if(rotationDegrees % 360 == 0)
            return unedited;

        Matrix mtx = new Matrix();

        mtx.setRotate(rotationDegrees,
                unedited.getWidth() / 2,
                unedited.getHeight() / 2);

        try{
            return Bitmap.createBitmap(unedited, 0, 0,
                    unedited.getWidth(), unedited.getHeight(),
                    mtx, true);
        }
        catch(OutOfMemoryError e){ // Need to work on this one with large, non-downsampled, images... Fuck.
            ImageUtils.log(context, e);

            System.gc();
        }

        return unedited;
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/filters/ImageFilter.java<|end_filename|>
package com.guardanis.imageloader.filters;

import android.content.Context;
import android.graphics.Bitmap;

public abstract class ImageFilter<T> {

    protected Context context;
    
    public ImageFilter(Context context){
        this.context = context;
    }

    public abstract T filter(T unedited);

    public String getAdjustmentInfo(){
        return getClass().getSimpleName();
    }

    protected Bitmap mutate(Bitmap bitmap){
        return bitmap.copy(bitmap.getConfig(), true);
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/stubs/AnimatedStubDrawable.java<|end_filename|>
package com.guardanis.imageloader.stubs;

public abstract class AnimatedStubDrawable extends StubDrawable {

    // Base class for determining if a TransitionDrawable should call
    // continue invalidating after transition has completed

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/stubs/builders/DefaultErrorStubBuilder.java<|end_filename|>
package com.guardanis.imageloader.stubs.builders;

import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.core.content.ContextCompat;

import com.guardanis.imageloader.R;

public class DefaultErrorStubBuilder implements StubBuilder {

    @Override
    public Drawable build(Context context) {
        return ContextCompat.getDrawable(context,
                R.drawable.ail__image_loader_stub_error);
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/transitions/TransitionController.java<|end_filename|>
package com.guardanis.imageloader.transitions;

import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.view.View;
import android.view.animation.Interpolator;
import android.widget.ImageView;

import com.guardanis.imageloader.ImageRequest;
import com.guardanis.imageloader.R;
import com.guardanis.imageloader.stubs.StubDrawable;
import com.guardanis.imageloader.transitions.drawables.TransitionDrawable;
import com.guardanis.imageloader.transitions.modules.TransitionModule;

import java.util.HashMap;
import java.util.Map;

public class TransitionController {

    protected ImageRequest request;

    protected Map<Class, TransitionModule> modules = new HashMap<Class, TransitionModule>();

    public TransitionController(ImageRequest request){
        this.request = request;
    }

    public void registerModule(TransitionModule module){
        if(module != null)
            modules.put(module.getClass(), module);
    }

    /**
     * @param c the class of the module (e.g. FadingTransitionModule)
     * @param interpolatorId the int ID from TransitionModule keys or custom entries (e.g. TransitionModule.INTERPOLATOR_IN)
     * @param interpolator the interpolator to substitute with
     */
    public void registerModuleInterpolator(Class c, int interpolatorId, Interpolator interpolator){
        if(modules != null)
            modules.get(c)
                    .registerInterpolator(interpolatorId, interpolator);
    }

    public void unregisterModules(){
        modules = new HashMap<Class, TransitionModule>();
    }

    public void transitionTo(Drawable to){
        if(modules.size() < 1)
            setTargetViewDrawable(to);
        else{
            Bitmap targetBitmap = getTargetBitmap(to, request.getTargetView());

            Drawable currentlyInView = request.isExitTransitionEnabled()
                    ? getCurrentTargetDrawable(targetBitmap)
                    : null;

            TransitionDrawable drawable = new TransitionDrawable(request.getContext(),
                    currentlyInView,
                    to,
                    targetBitmap);

            if(currentlyInView != null){
                if(!request.isRequestForBackgroundImage() && currentlyInView instanceof TransitionDrawable)
                    ((TransitionDrawable) currentlyInView).overrideCanvasMatrix(((TransitionDrawable) currentlyInView).getBaseCanvasMatrix());
                else if(currentlyInView instanceof StubDrawable)
                    ((StubDrawable) currentlyInView).overrideCanvasMatrix(((StubDrawable) currentlyInView).getBaseCanvasMatrix());
            }

            for(TransitionModule module : modules.values())
                drawable.registerModule(module);

            setTransitionDrawable(drawable);
        }
    }

    protected Drawable getCurrentTargetDrawable(Bitmap targetBitmap){
        Drawable current = request.isRequestForBackgroundImage()
                ? request.getTargetView().getBackground()
                : ((ImageView)request.getTargetView()).getDrawable();

        if(current == null)
            return ContextCompat.getDrawable(request.getContext(), R.drawable.ail__default_image_placeholder);
        else if(current instanceof TransitionDrawable && ((TransitionDrawable) current).getTargetDrawable() instanceof StubDrawable)
            return ((TransitionDrawable) current).getTargetDrawable();
        else
            return current;
    }

    protected Bitmap getTargetBitmap(Drawable drawable, @Nullable View targetView){
        if(drawable instanceof BitmapDrawable)
            return ((BitmapDrawable) drawable).getBitmap();
        else {
            Bitmap bitmap = null;

            if(drawable.getIntrinsicWidth() < 1 || drawable.getIntrinsicHeight() < 1){
                if(!(targetView == null || targetView.getLayoutParams() == null || targetView.getLayoutParams().width < 1 || targetView.getLayoutParams().height < 1))
                    bitmap = Bitmap.createBitmap(targetView.getLayoutParams().width,
                            targetView.getLayoutParams().height,
                            Bitmap.Config.ARGB_8888);
                else if(drawable.getBounds().right - drawable.getBounds().left < 1)
                    bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
                else
                    bitmap = Bitmap.createBitmap(drawable.getBounds().right - drawable.getBounds().left,
                        drawable.getBounds().bottom - drawable.getBounds().top,
                        Bitmap.Config.ARGB_8888);
            }
            else
                bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                    drawable.getIntrinsicHeight(),
                    Bitmap.Config.ARGB_8888);

            Canvas canvas = new Canvas(bitmap);

            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);

            return bitmap;
        }
    }

    protected void setTransitionDrawable(final TransitionDrawable drawable){
        request.getTargetView().post(new Runnable(){
            public void run(){
                drawable.start(request.getContext());

                if(request.isRequestForBackgroundImage())
                    setBackgroundDrawable(drawable);
                else
                    ((ImageView) request.getTargetView())
                        .setImageDrawable(drawable);
            }
        });
    }

    protected void setTargetViewDrawable(final Drawable drawable){
        request.getTargetView().post(new Runnable(){
            public void run(){
                if(request.isRequestForBackgroundImage())
                    setBackgroundDrawable(drawable);
                else
                    ((ImageView) request.getTargetView())
                        .setImageDrawable(drawable);
            }
        });
    }

    @SuppressLint("NewApi")
    protected void setBackgroundDrawable(Drawable drawable) {
        if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN)
            request.getTargetView()
                    .setBackgroundDrawable(drawable);
        else
            request.getTargetView()
                    .setBackground(drawable);
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/stubs/StubDrawable.java<|end_filename|>
package com.guardanis.imageloader.stubs;

import android.graphics.Matrix;
import android.graphics.drawable.Drawable;

public abstract class StubDrawable extends Drawable {

    // Base class for determining if a TransitionDrawable should call
    // super.onDraw(Canvas) or draw from the target Drawable

    protected Matrix baseCanvasMatrix = new Matrix();
    protected Matrix canvasMatrixOverride;

    public void overrideCanvasMatrix(Matrix canvasMatrixOverride){
        this.canvasMatrixOverride = canvasMatrixOverride;
    }

    public Matrix getBaseCanvasMatrix(){
        return baseCanvasMatrix;
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/processors/ImageFileProcessor.java<|end_filename|>
package com.guardanis.imageloader.processors;

import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;

import com.guardanis.imageloader.ImageRequest;
import com.guardanis.imageloader.ImageUtils;
import com.guardanis.imageloader.filters.ImageFilter;

import java.io.File;
import java.util.List;

import pl.droidsonroids.gif.GifDrawable;

public class ImageFileProcessor extends ImageProcessor {

    @Override
    public Drawable process(ImageRequest request, List<ImageFilter<Bitmap>> bitmapImageFilters) throws Exception {
        File target = new File(request.getTargetUrl());

        switch(request.getTargetImageType()){
            case GIF:
                return new GifDrawable(target);
            case BITMAP:
            case SVG:
            default:
                return processBitmap(request, target, bitmapImageFilters);
        }
    }

    protected Drawable processBitmap(ImageRequest request, File originalFile, List<ImageFilter<Bitmap>> bitmapImageFilters) throws Exception {
        File editedImageFile = request.getEditedRequestFile();
        int requiredImageWidth = request.getTargetImageWidth();

        if(editedImageFile.exists())
            return decodeBitmapDrawable(request.getContext(),
                    editedImageFile,
                    requiredImageWidth);

        if(originalFile.exists())
            return process(request.getContext(),
                    ImageUtils.decodeFile(originalFile, requiredImageWidth),
                    editedImageFile,
                    bitmapImageFilters);

        return null;
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/transitions/modules/TransitionModule.java<|end_filename|>
package com.guardanis.imageloader.transitions.modules;

import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import android.view.animation.Interpolator;

import com.guardanis.imageloader.transitions.drawables.TransitionDrawable;

import java.util.HashMap;
import java.util.Map;

public abstract class TransitionModule {

    public static final int INTERPOLATOR_IN = 0;
    public static final int INTERPOLATOR_OUT = 1;

    protected long duration;

    protected Map<Integer, Interpolator> interpolators = new HashMap<Integer, Interpolator>();

    public TransitionModule(long duration){
        this.duration = duration;
    }

    public TransitionModule registerInterpolator(int id, Interpolator interpolator){
        interpolators.put(id, interpolator);

        return this;
    }

    public Interpolator getInterpolator(int id){
        return interpolators.get(id);
    }

    public abstract void onStart(@Nullable Drawable old, Drawable target);

    public abstract void onPredrawOld(TransitionDrawable transitionDrawable, Canvas canvas, @Nullable Drawable old, long startTime);

    public abstract void revertPostDrawOld(TransitionDrawable transitionDrawable, @Nullable Drawable old);

    public abstract void onPredrawTarget(TransitionDrawable transitionDrawable, Canvas canvas, Drawable target, long startTime);

    public abstract void revertPostDrawTarget(TransitionDrawable transitionDrawable, Drawable target);

    protected float calculatePercentCompleted(long startTimeMs){
        return Math.min(1f, (System.currentTimeMillis() - startTimeMs) / (float) duration);
    }

    protected float interpolate(int id, long startTimeMs) {
        return interpolate(id, calculatePercentCompleted(startTimeMs));
    }

    protected float interpolate(int id, float percentCompleted){
        return interpolators.get(id) == null
                ? 1f
                : interpolators.get(id).getInterpolation(percentCompleted);
    }

    public long getDuration(){
        return duration;
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/processors/ImageProcessor.java<|end_filename|>
package com.guardanis.imageloader.processors;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;

import com.guardanis.imageloader.ImageRequest;
import com.guardanis.imageloader.ImageUtils;
import com.guardanis.imageloader.filters.ImageFilter;

import java.io.File;
import java.util.List;

public abstract class ImageProcessor {

    public abstract Drawable process(ImageRequest request, List<ImageFilter<Bitmap>> bitmapImageFilters) throws Exception;

    protected Drawable process(Context context, Bitmap bitmap, File targetSaveFile, List<ImageFilter<Bitmap>> bitmapImageFilters){
        Bitmap edited = applyFilters(context, bitmap, bitmapImageFilters);

        if(edited != null && 0 < bitmapImageFilters.size())
            ImageUtils.saveBitmapAsync(context, targetSaveFile, edited);

        return new BitmapDrawable(context.getResources(), edited);
    }

    protected Bitmap decodeBitmap(File file, int requiredImageWidth) {
        return ImageUtils.decodeFile(file, requiredImageWidth);
    }

    protected BitmapDrawable decodeBitmapDrawable(Context context, File file, int requiredImageWidth) {
        return new BitmapDrawable(context.getResources(),
                decodeBitmap(file, requiredImageWidth));
    }

    protected Bitmap applyFilters(Context context, Bitmap bitmap, List<ImageFilter<Bitmap>> bitmapImageFilters) {
        if(0 < bitmapImageFilters.size() && bitmap != null){
            try{
                for(ImageFilter<Bitmap> filter : bitmapImageFilters)
                    bitmap = filter.filter(bitmap);
            }
            catch(Throwable e){
                ImageUtils.log(context, e);

                // Let it fail gracefully if our filters couldn't recover
                bitmap = null;
            }
        }

        return bitmap;
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/transitions/modules/ScalingTransitionModule.java<|end_filename|>
package com.guardanis.imageloader.transitions.modules;

import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import android.view.animation.DecelerateInterpolator;

import com.guardanis.imageloader.stubs.StubDrawable;
import com.guardanis.imageloader.transitions.drawables.TransitionDrawable;

public class ScalingTransitionModule extends TransitionModule {

    protected float scaleFrom;
    protected float scaleTo;
    protected float difference;

    public ScalingTransitionModule(float from, float to, long duration) {
        super(duration);
        this.scaleFrom = from;
        this.scaleTo = to;
        this.difference = to - from;

        registerInterpolator(TransitionModule.INTERPOLATOR_IN, new DecelerateInterpolator());
    }

    @Override
    public void onStart(@Nullable Drawable old, Drawable target) { }

    @Override
    public void onPredrawOld(TransitionDrawable transitionDrawable, Canvas canvas, @Nullable Drawable old, long startTime) { }

    @Override
    public void revertPostDrawOld(TransitionDrawable transitionDrawable, @Nullable Drawable old) { }

    @Override
    public void onPredrawTarget(TransitionDrawable transitionDrawable, Canvas canvas, Drawable target, long startTime) {
        if(target instanceof StubDrawable)
            return;

        float percentCompleted = interpolate(TransitionModule.INTERPOLATOR_IN, startTime);

        float scale = scaleFrom + (percentCompleted * difference);

        canvas.translate((transitionDrawable.getBitmap().getWidth() - (scale * transitionDrawable.getBitmap().getWidth())) / 2,
                (transitionDrawable.getBitmap().getHeight() - (scale * transitionDrawable.getBitmap().getHeight())) / 2);

        canvas.scale(scale, scale);
    }

    @Override
    public void revertPostDrawTarget(TransitionDrawable transitionDrawable, Drawable target) { }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/processors/ImageResourceProcessor.java<|end_filename|>
package com.guardanis.imageloader.processors;

import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;

import com.guardanis.imageloader.ImageRequest;
import com.guardanis.imageloader.ImageUtils;
import com.guardanis.imageloader.filters.ImageFilter;

import java.io.File;
import java.util.List;

import pl.droidsonroids.gif.GifDrawable;

public class ImageResourceProcessor extends ImageFileProcessor {

    @Override
    public Drawable process(ImageRequest request, List<ImageFilter<Bitmap>> bitmapImageFilters) throws Exception {
        switch(request.getTargetImageType()){
            case GIF:
                return new GifDrawable(request.getContext().getResources(), request.getTargetResourceId());
            case SVG:
                return process(request.getContext(),
                        ImageUtils.decodeSvgResource(request.getContext(),
                                request.getTargetResourceId(),
                                request.getTargetImageWidth()),
                        request.getEditedRequestFile(),
                        bitmapImageFilters);
            case BITMAP:
            default:
                return processBitmapResource(request, bitmapImageFilters);
        }
    }

    protected Drawable processBitmapResource(ImageRequest request, List<ImageFilter<Bitmap>> bitmapImageFilters) throws Exception {
        File editedImageFile = request.getEditedRequestFile();
        int requiredImageWidth = request.getTargetImageWidth();

        if(editedImageFile.exists())
            return decodeBitmapDrawable(request.getContext(), editedImageFile, requiredImageWidth);

        return process(request.getContext(),
                ImageUtils.decodeBitmapResource(request.getContext().getResources(),
                        request.getTargetResourceId(),
                        request.getTargetImageWidth()),
                editedImageFile,
                bitmapImageFilters);
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/ImageRequest.java<|end_filename|>
package com.guardanis.imageloader;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.ColorFilter;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.view.View;
import android.view.animation.Interpolator;
import android.widget.ImageView;

import com.guardanis.imageloader.ImageUtils.ImageType;
import com.guardanis.imageloader.filters.BitmapBlurFilter;
import com.guardanis.imageloader.filters.BitmapCenterCropFilter;
import com.guardanis.imageloader.filters.BitmapCircularCropFilter;
import com.guardanis.imageloader.filters.BitmapColorFilter;
import com.guardanis.imageloader.filters.BitmapColorOverlayFilter;
import com.guardanis.imageloader.filters.BitmapColorOverrideFilter;
import com.guardanis.imageloader.filters.BitmapRotationFilter;
import com.guardanis.imageloader.filters.ImageFilter;
import com.guardanis.imageloader.processors.ExternalImageProcessor;
import com.guardanis.imageloader.processors.ImageAssetProcessor;
import com.guardanis.imageloader.processors.ImageDrawableResourceProcessor;
import com.guardanis.imageloader.processors.ImageFileProcessor;
import com.guardanis.imageloader.processors.ImageProcessor;
import com.guardanis.imageloader.processors.ImageResourceProcessor;
import com.guardanis.imageloader.stubs.builders.StubBuilder;
import com.guardanis.imageloader.transitions.TransitionController;
import com.guardanis.imageloader.transitions.modules.FadingTransitionModule;
import com.guardanis.imageloader.transitions.modules.RotationTransitionModule;
import com.guardanis.imageloader.transitions.modules.ScalingTransitionModule;
import com.guardanis.imageloader.transitions.modules.TransitionModule;
import com.guardanis.imageloader.transitions.modules.TranslateTransitionModule;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ImageRequest<V extends View> implements Runnable {

    public interface RequestStartedCallback {
        public void onRequestStarted(ImageRequest request);
    }

    public interface ImageSuccessCallback {
        public void onImageReady(ImageRequest request, Drawable result);
    }

    public interface ImageErrorCallback {
        public void onImageLoadingFailure(ImageRequest request, @Nullable Throwable e);
    }

    protected static final int DEFAULT_BLUR_RADIUS = 15;
    protected static final int DEFAULT_CROSS_FADE_DURATION = 300;
    protected static final int DEFAULT_SCALE_DURATION = 300;
    protected static final int DEFAULT_ROTATION_DURATION = 300;

    protected static final int WIDTH_UNKNOWN = -1;

    protected Context context;

    protected String targetUrl;
    protected int targetResourceId = -1;

    protected ImageType targetImageType = ImageType.BITMAP;
    protected ImageProcessor imageProcessor;
    protected boolean loadingTargetLocally = false;

    protected V targetView;
    protected boolean setImageAsBackground = false;
    protected int requiredImageWidth = WIDTH_UNKNOWN;

    protected long maxCacheDurationMs = -1;
    private boolean lruCacheEnabled = true;

    protected List<ImageFilter<Bitmap>> bitmapImageFilters = new ArrayList<ImageFilter<Bitmap>>();

    protected boolean showStubOnExecute = true;
    protected boolean showStubOnError = false;
    protected boolean disableExecutionStubIfDownloaded = true;
    protected StubBuilder loadingStubBuilder;
    protected StubBuilder errorStubBuilder;

    protected TransitionController transitionController = new TransitionController(this);
    protected boolean exitTransitionsEnabled = true;
    protected boolean transitionOnSuccessEnabled = true;

    protected Map<String, String> httpRequestParams = new HashMap<String, String>();

    protected RequestStartedCallback requestStartedCallback;

    protected ImageSuccessCallback successCallback;
    protected boolean triggerSuccessForValidViewsOnly = true;

    protected ImageErrorCallback errorCallback;

    protected boolean tagRequestPreventionEnabled = false;

    protected long startedAtMs;

    public ImageRequest(Context context) {
        this.context = context;
        this.showStubOnExecute = context.getResources().getBoolean(R.bool.ail__show_stub_on_execute);
        this.showStubOnError = context.getResources().getBoolean(R.bool.ail__show_stub_on_error);
        this.lruCacheEnabled = context.getResources().getBoolean(R.bool.ail__lru_cache_enabled);
        this.tagRequestPreventionEnabled = context.getResources().getBoolean(R.bool.ail__tag_request_prevention_enabled);
    }

    public ImageRequest(Context context, V targetView) {
        this(context);

        this.targetView = targetView;
    }

    public ImageRequest<V> setTargetUrl(final String targetUrl) {
        this.targetUrl = targetUrl;
        this.targetImageType = ImageUtils.getImageType(context, targetUrl);
        this.loadingTargetLocally = false;

        this.imageProcessor = new ExternalImageProcessor();

        return this;
    }

    public ImageRequest<V> setTargetAsset(String targetAssetUrl){
        this.targetUrl = targetAssetUrl;
        this.targetImageType = ImageUtils.getImageType(context, targetUrl);
        this.loadingTargetLocally = true;
        this.showStubOnExecute = context.getResources()
                .getBoolean(R.bool.ail__local_execution_stubs);

        this.imageProcessor = new ImageAssetProcessor();

        return this;
    }

    public ImageRequest<V> setTargetFile(String file){
        return setTargetFile(new File(file));
    }

    public ImageRequest<V> setTargetFile(File file){
        this.targetUrl = file.getAbsolutePath();
        this.targetImageType = ImageUtils.getImageType(context, targetUrl);
        this.loadingTargetLocally = true;
        this.showStubOnExecute = context.getResources()
                .getBoolean(R.bool.ail__local_execution_stubs);

        this.imageProcessor = new ImageFileProcessor();

        return this;
    }

    public ImageRequest<V> setTargetResource(int targetResourceId){
        // MIME types and extensions are unavailable for resource files
        return setTargetResource(targetResourceId, ImageType.BITMAP);
    }

    public ImageRequest<V> setTargetResource(int targetResourceId, ImageType type){
        this.targetResourceId = targetResourceId;
        this.targetImageType = type;
        this.loadingTargetLocally = true;
        this.showStubOnExecute = context.getResources()
                .getBoolean(R.bool.ail__local_execution_stubs);

        this.imageProcessor = new ImageResourceProcessor();

        return this;
    }

    /**
     * @param drawableResourceId A bitmap or Vector Asset from local drawable resources
     */
    public ImageRequest<V> setTargetDrawable(@DrawableRes int drawableResourceId){
        this.targetResourceId = drawableResourceId;
        this.targetImageType = ImageType.CUSTOM;
        this.loadingTargetLocally = true;
        this.showStubOnExecute = context.getResources()
                .getBoolean(R.bool.ail__local_execution_stubs);

        this.imageProcessor = new ImageDrawableResourceProcessor();

        return this;
    }

    /**
     * Override the ImageProcessor that was set by calling the setTargetUrl, setTargetAsset, setTargetFile, or setTargetResource methods.
     * If those methods are called after this, they will override this call. This is really only useful for implementing third-party image libraries.
     */
    public ImageRequest<V> overrideImageProcessor(@NonNull ImageProcessor imageProcessor){
        this.imageProcessor = imageProcessor;
        return this;
    }

    public ImageRequest<V> setTargetView(V targetView) {
        this.targetView = targetView;
        return this;
    }

    /**
     * Add HttpUrlConnection Request Parameter key/value pair. Adding a key/value pair will override previous values for said key.
     */
    public ImageRequest<V> addHttpRequestParam(String key, String value) {
        httpRequestParams.put(key, value);
        return this;
    }

    /**
     * Force request to load image at specific size
     */
    public ImageRequest<V> setRequiredImageWidth(int requiredImageWidth) {
        this.requiredImageWidth = requiredImageWidth;
        return this;
    }

    /**
     * Force request to load image via setBackground
     */
    public ImageRequest<V> setImageAsBackground() {
        this.setImageAsBackground = true;
        return this;
    }

    public ImageRequest<V> setImageAsBackground(boolean setImageAsBackground) {
        this.setImageAsBackground = setImageAsBackground;
        return this;
    }

    public ImageRequest<V> setShowStubOnExecute(boolean showStubOnExecute) {
        this.showStubOnExecute = showStubOnExecute;
        return this;
    }

    public ImageRequest<V> setShowStubOnError(boolean showStubOnError) {
        this.showStubOnError = showStubOnError;
        return this;
    }

    public ImageRequest<V> setDisableExecutionStubIfDownloaded(boolean disableExecutionStubIfDownloaded) {
        this.disableExecutionStubIfDownloaded = disableExecutionStubIfDownloaded;
        return this;
    }

    /**
     * Set the maximum allowed time a cached file from this request is valid for in milliseconds.
     * Files requested beyond this limit will be deleted and re-downloaded.
     * Anything < 0 will never re-download (default behavior).
     */
    public ImageRequest<V> setMaxCacheDurationMs(long maxCacheDurationMs) {
        this.maxCacheDurationMs = maxCacheDurationMs;
        return this;
    }

    /**
     * Enable/disable use of LruCache for caching images in memory
     */
    public ImageRequest<V> setLruCacheEnabled(boolean lruCacheEnabled) {
        this.lruCacheEnabled = lruCacheEnabled;
        return this;
    }

    /**
     * Set a callback to be triggered on the main thread once downloading and/or processing this
     * ImageRequest has begun. This will not be triggered if tag-request-prevention is enabled
     * and the target view has already been claimed for this target.
     */
    public ImageRequest<V> setRequestStartedCallback(RequestStartedCallback requestStartedCallback) {
        this.requestStartedCallback = requestStartedCallback;
        return this;
    }

    /**
     * Set a callback to be triggered on the main thread once the resulting image has been set
     * into the target View. This will not be triggered if the View is null.
     */
    public ImageRequest<V> setSuccessCallback(ImageSuccessCallback successCallback) {
        this.successCallback = successCallback;
        return this;
    }

    /**
     * Set whether or not to trigger the success callback when the View is no longer valid for the request.
     * The default behavior is true, to prevent invalid requests from triggering. Only valid when a
     * target View is present.
     */
    public ImageRequest<V> setTriggerSuccessForValidViewsOnly(boolean triggerSuccessForValidViewsOnly){
        this.triggerSuccessForValidViewsOnly = triggerSuccessForValidViewsOnly;
        return this;
    }

    /**
     * Set a callback to be triggered in the event of a loading error. This will not
     * be triggered if the target View is null.
     */
    public ImageRequest<V> setErrorCallback(ImageErrorCallback errorCallback) {
        this.errorCallback = errorCallback;
        return this;
    }

    public ImageRequest<V> addBlurFilter(){
        return addBlurFilter(DEFAULT_BLUR_RADIUS);
    }

    public ImageRequest<V> addBlurFilter(int blurRadius){
        return addImageFilter(new BitmapBlurFilter(context,
                blurRadius));
    }

    public ImageRequest<V> addColorOverlayFilter(int colorOverlay) {
       return addImageFilter(new BitmapColorOverlayFilter(context,
                colorOverlay));
    }

    public ImageRequest<V> addRotationFilter(int rotationDegrees) {
        return addImageFilter(new BitmapRotationFilter(context,
                rotationDegrees));
    }

    public ImageRequest<V> addCircularCropFilter() {
        return addImageFilter(new BitmapCircularCropFilter(context));
    }

    /**
     * Replace all color values with the supplied color value while maintaining proper opacity at each pixel.
     * Intended for single color-scaled images.
     * @param replacementColor the 24-bit color value to replace with (0xAARRGGBB)
     */
    public ImageRequest<V> addColorOverrideFilter(int replacementColor){
        return addImageFilter(new BitmapColorOverrideFilter(context, replacementColor));
    }

    public ImageRequest<V> addColorFilter(ColorFilter filter) {
        return addImageFilter(new BitmapColorFilter(context, filter));
    }

    public ImageRequest<V> addCenterCropFilter(int width, int height) {
        return addImageFilter(new BitmapCenterCropFilter(context, new int[]{ width, height }));
    }

    public ImageRequest<V> addImageFilter(ImageFilter<Bitmap> imageFilter){
        bitmapImageFilters.add(imageFilter);
        return this;
    }

    @Deprecated
    public ImageRequest<V> overrideStubs(final StubHolder stubHolder){
        setLoadingStub(new StubBuilder() {
            public Drawable build(Context context) {
                return stubHolder.getLoadingDrawable(context);
            }
        });

        setErrorStub(new StubBuilder() {
            public Drawable build(Context context) {
                return stubHolder.getErrorDrawable(context);
            }
        });

        return this;
    }

    public ImageRequest<V> setLoadingStub(StubBuilder loadingStubBuilder){
        this.loadingStubBuilder = loadingStubBuilder;
        return this;
    }

    public ImageRequest<V> setErrorStub(StubBuilder errorStubBuilder){
        this.errorStubBuilder = errorStubBuilder;
        return this;
    }

    public ImageRequest<V> setDefaultImageTransition(){
        transitionController.unregisterModules();
        return this;
    }

    public ImageRequest<V> setFadeTransition(){
        return setFadeTransition(DEFAULT_CROSS_FADE_DURATION);
    }

    public ImageRequest<V> setFadeTransition(long duration){
        return addImageTransitionModule(new FadingTransitionModule(duration));
    }

    public ImageRequest<V> setScaleTransition(float from, float to){
        return setScaleTransition(from, to, DEFAULT_SCALE_DURATION);
    }

    public ImageRequest<V> setScaleTransition(float from, float to, long duration){
        return addImageTransitionModule(new ScalingTransitionModule(from, to, duration));
    }

    public ImageRequest<V> setRotationTransition(int from, int to){
        return setRotationTransition(from, to, DEFAULT_ROTATION_DURATION);
    }

    public ImageRequest<V> setRotationTransition(int from, int to, long duration){
        return addImageTransitionModule(new RotationTransitionModule(from, to, duration));
    }

    /**
     * Translate to 'normal' position from supplied parameters.
     */
    public ImageRequest<V> setTranslateTransition(float fromX, float fromY, long duration){
        return setTranslateTransition(new float[]{ fromX, fromY },
                new float[]{ 0, 0 },
                duration);
    }

    public ImageRequest<V> setTranslateTransition(float fromX, float fromY, float toX, float toY, long duration){
        return setTranslateTransition(new float[]{ fromX, fromY },
                new float[]{ toX, toY },
                duration);
    }

    public ImageRequest<V> setTranslateTransition(float[] from, float[] to, long duration){
        return addImageTransitionModule(new TranslateTransitionModule(from, to, duration));
    }

    public ImageRequest<V> addImageTransitionModule(TransitionModule module){
        this.transitionController.registerModule(module);
        return this;
    }

    public ImageRequest<V> setExitTransitionsEnabled(boolean exitTransitionsEnabled){
        this.exitTransitionsEnabled = exitTransitionsEnabled;
        return this;
    }

    /**
     * Set whether or not the loader should automatically transition the View to the request's completed image
     * for cases where you want to handle the returned Bitmap before it's actually placed within the View.
     * See setSuccessCallback(ImageSuccessCallback) for more info
     */
    public ImageRequest<V> setTransitionOnSuccessEnabled(boolean transitionOnSuccessEnabled){
        this.transitionOnSuccessEnabled = transitionOnSuccessEnabled;
        return this;
    }

    /**
     * Override an Interpolator for a previously added TransitionModule
     * @param c the class of the module (e.g. FadingTransitionModule)
     * @param interpolatorId the int ID from TransitionModule keys or custom entries (e.g. TransitionModule.INTERPOLATOR_IN)
     * @param interpolator the interpolator to substitute with
     */
    public ImageRequest<V> registerImageTransitionInterpolator(Class c, int interpolatorId, Interpolator interpolator){
        this.transitionController.registerModuleInterpolator(c, interpolatorId, interpolator);
        return this;
    }

    /**
     * Enabling this will prevent subsequent requests for the same URL from running when the target is already set correctly.
     * This uses the target View's tag holder. Don't use this with other requests that don't have this flag enabled.
     */
    public ImageRequest<V> setTagRequestPreventionEnabled(boolean tagRequestPreventionEnabled){
        this.tagRequestPreventionEnabled = tagRequestPreventionEnabled;
        return this;
    }

    @Override
    public void run() {
        boolean processableWithView = targetView != null
                && ImageLoader.getInstance(context).isViewStillUsable(this);

        boolean processableWithoutView = targetView == null
                && 0 < getRequiredImageWidth();

        if (!(processableWithView || processableWithoutView))
            return;

        post(new Runnable() {
            public void run() {
                if (requestStartedCallback != null)
                    requestStartedCallback.onRequestStarted(ImageRequest.this);
            }
        });

        try{
            String cacheKey = getEditedRequestFileCacheName();

            Drawable processed = null;

            if(lruCacheEnabled)
                processed = MemoryCache.getInstance(context)
                        .get(cacheKey);

            if(processed == null){
                processed = imageProcessor.process(this, bitmapImageFilters);

                if(lruCacheEnabled
                        && processed != null
                        && processed instanceof BitmapDrawable)
                    MemoryCache.getInstance(context)
                            .put(cacheKey, (BitmapDrawable) processed);
            }

            onRequestCompleted(processed);
        }
        catch(Throwable e){
            ImageUtils.log(context, e);

            onRequestFailed(e);
        }
    }

    protected int getRequiredImageWidth(){
        if(0 < requiredImageWidth)
            return requiredImageWidth;

        return !(targetView == null || targetView.getLayoutParams() == null)
                ? targetView.getLayoutParams().width
                : WIDTH_UNKNOWN;
    }

    protected void onRequestCompleted(@Nullable final Drawable targetDrawable){
        if(targetDrawable == null){
            onRequestFailed(new RuntimeException("ImageRequest completed with null image!"));

            return;
        }

        if(targetView != null){
            if(ImageLoader.getInstance(context).isViewStillUsable(this)){
                if(transitionOnSuccessEnabled)
                    transitionController.transitionTo(targetDrawable);
            }
            else if(triggerSuccessForValidViewsOnly)
                return;
        }

        post(new Runnable(){
            public void run(){
                if(successCallback != null)
                    successCallback.onImageReady(ImageRequest.this, targetDrawable);
            }
        });
    }

    protected void onRequestFailed(Throwable throwable) {
        final Throwable error = throwable == null
                ? new RuntimeException("Image could not be loaded")
                : throwable;

        if(targetView != null && ImageLoader.getInstance(context).isViewStillUsable(this))
            handleShowStubOnError();

        if(targetView == null || ImageLoader.getInstance(context).isViewStillUsable(this))
            post(new Runnable(){
                public void run(){
                    if(errorCallback != null)
                        errorCallback.onImageLoadingFailure(ImageRequest.this, error);
                }
            });
    }

    protected void handleShowStubOnError(){
        final Drawable targetDrawable = showStubOnError
                ? getErrorStubBuilder().build(context)
                : ContextCompat.getDrawable(context, R.drawable.ail__default_image_placeholder);

        transitionController.transitionTo(targetDrawable);
    }

    protected String getEditedRequestFileCacheName() {
        String adjustedName = getOriginalRequestFileCacheName();

        for(ImageFilter<Bitmap> filter : bitmapImageFilters)
            adjustedName += "_" + filter.getAdjustmentInfo();

        return adjustedName + "_" + getRequiredImageWidth() + "px";
    }

    public File getEditedRequestFile() {
        return ImageLoader.getInstance(context)
                .getFileCache()
                .getFile(getEditedRequestFileCacheName());
    }

    protected String getOriginalRequestFileCacheName() {
        return 0 < targetResourceId
                ? String.valueOf(targetResourceId)
                : targetUrl;
    }

    public Context getContext(){
        return context;
    }

    public boolean isSetImageAsBackgroundForced(){
        return setImageAsBackground;
    }

    public boolean isRequestForBackgroundImage(){
        return setImageAsBackground || !(targetView instanceof ImageView);
    }

    public V getTargetView(){
        return targetView;
    }

    public Map<String, String> getHttpRequestParams(){
        return httpRequestParams;
    }

    public long getMaxCacheDurationMs(){
        return maxCacheDurationMs;
    }

    /**
     * @return the File associated with the target URL. File won't be null, but may not exist.
     */
    public File getOriginalRequestFile() {
        return ImageLoader.getInstance(context)
                .getFileCache()
                .getFile(getOriginalRequestFileCacheName());
    }

    protected StubBuilder getLoadingStubBuilder(){
        return loadingStubBuilder == null
                ? ImageLoader.getInstance(context).getLoadingStubBuilder()
                : loadingStubBuilder;
    }

    protected StubBuilder getErrorStubBuilder(){
        return errorStubBuilder == null
                ? ImageLoader.getInstance(context).getErrorStubBuilder()
                : errorStubBuilder;
    }

    public String getTargetUrl(){
        return targetUrl;
    }

    public int getTargetResourceId(){
        return targetResourceId;
    }

    public boolean isExitTransitionEnabled(){
        return exitTransitionsEnabled;
    }

    public long getStartedAtMs(){
        return startedAtMs;
    }

    public boolean isTargetLocal(){
        return loadingTargetLocally;
    }

    public int getTargetImageWidth(){
        return getRequiredImageWidth();
    }

    public ImageType getTargetImageType(){
        return targetImageType;
    }

    protected void post(Runnable runnable){
        if(targetView == null)
            ImageLoader.getInstance(context)
                .getHandler()
                .post(runnable);
        else targetView.post(runnable);
    }

    protected boolean isTargetDefined(){
        return ((targetUrl != null && 0 < targetUrl.length()) || 0 < targetResourceId)
                && imageProcessor != null;
    }

    private void handleShowStubOnExecute(){
        if(disableExecutionStubIfDownloaded
                && ImageLoader.getInstance(context).isImageDownloaded(this)
                && ImageLoader.getInstance(context).getFileCache().isCachedFileValid(targetUrl, maxCacheDurationMs))
            return;

        final Drawable targetDrawable = showStubOnExecute
                ? getLoadingStubBuilder().build(context)
                : ContextCompat.getDrawable(context, R.drawable.ail__default_image_placeholder);

        transitionController.transitionTo(targetDrawable);
    }

    public ImageRequest<V> execute() {
        if(!isTargetDefined()) {
            onRequestFailed(new RuntimeException("No target URL or resource defined!"));

            return this;
        }

        final String fullRequestFile = getEditedRequestFileCacheName();

        if(isTargetViewClaimedBy(fullRequestFile)){
            ImageUtils.log(context, "View already claimed by duplicate request. Ignoring this one [" + fullRequestFile + "]");

            return this;
        }

        startedAtMs = System.currentTimeMillis();

        if(targetView == null)
            ImageLoader.getInstance(context)
                    .submit(this);
        else
            ImageLoader.getInstance(context)
                    .getHandler()
                    .post(new Runnable(){
                        public void run(){
                            if(tagRequestPreventionEnabled)
                                targetView.setTag(fullRequestFile);

                            ImageLoader.getInstance(context)
                                    .claimViewTarget(ImageRequest.this);

                            handleShowStubOnExecute();

                            targetView.post(new Runnable() {
                                public void run() {
                                    ImageLoader.getInstance(context)
                                            .submit(ImageRequest.this);
                                }
                            });
                        }
                    });

        return this;
    }

    protected boolean isTargetViewClaimedBy(String fullRequestFile){
        return tagRequestPreventionEnabled
                && !(targetView == null || targetView.getTag() == null)
                && targetView.getTag().equals(fullRequestFile);
    }

    /**
     * @param delay amount to delay the execution by
     */
    public ImageRequest<V> execute(long delay){
        ImageLoader.getInstance(context)
                .getHandler()
                .postDelayed(new Runnable(){
                    public void run(){
                        execute();
                    }
                }, delay);

        return this;
    }

    public static <V extends View> ImageRequest<V> create(@NonNull V targetView){
        return new ImageRequest<V>(targetView.getContext(), targetView);
    }

    public static ImageRequest prefetch(Context context, String url){
        return new ImageRequest(context)
                .setTargetUrl(url)
                .execute();
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/views/SVGImageView.java<|end_filename|>
/*
   Copyright 2013 <NAME>, <NAME> Software Ltd.

   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 com.guardanis.imageloader.views;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import androidx.appcompat.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;

import com.caverock.androidsvg.SVG;
import com.guardanis.imageloader.ImageRequest;
import com.guardanis.imageloader.ImageUtils;
import com.guardanis.imageloader.R;

/**
 * Edited / Dumbed-down version of com.caverock.androidsvg.SVGImageView
 */
public class SVGImageView extends AppCompatImageView {

	private interface LayoutEventListener {
		public void onLaidOut(int width);
	}

	public SVGImageView(Context context) {
		super(context);
		disableHardwareAcceleration(this);
	}

	public SVGImageView(Context context, AttributeSet attrs) {
		super(context, attrs, 0);
		disableHardwareAcceleration(this);

		init(attrs, 0);
	}

	public SVGImageView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		disableHardwareAcceleration(this);

		init(attrs, defStyle);
	}

	private void init(AttributeSet attrs, int defStyle) {
		TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.SVGImageView, defStyle, 0);
		try {
			int resourceId = a.getResourceId(R.styleable.SVGImageView_svg, -1);
			if(resourceId != -1) {
				setImageResource(resourceId);
				return;
			}

			String url = a.getString(R.styleable.SVGImageView_svg);

			if(url != null)
				setImageAsset(url);
		}
		finally { a.recycle(); }
	}

	/**
	 * Load an SVG image from the given filename in the assets directory.
	 */
	public void setImageAsset(String filename){
		try	{
			if(filename.length() < 1)
				setImageDrawable(null);
			else setImageSvg(SVG.getFromAsset(getContext().getAssets(), filename));
		}
		catch (Throwable e) {
			ImageUtils.log(getContext(), "Unable to find asset file: " + filename);

			setImageDrawable(null);
		}
	}

    /**
     * Load an SVG image from an external URL.
     */
    public void setImageUrl(String url){
        new ImageRequest<SVGImageView>(getContext())
				.setTargetUrl(url)
                .setTargetView(this)
                .setFadeTransition()
                .execute();
    }

    /**
     * Load an SVG image from a file on the device.
     */
    public void setImageFile(String imageFile){
        new ImageRequest<SVGImageView>(getContext())
				.setTargetFile(imageFile)
                .setTargetView(this)
                .setFadeTransition()
                .execute();
    }

	private void setImageSvg(final SVG svg) throws Throwable {
		getBestWidth(new LayoutEventListener() {
			public void onLaidOut(int width) {
				setImageBitmap(ImageUtils.decodeBitmap(svg, width));
			}
		});
	}

	private void getBestWidth(final LayoutEventListener eventListener){
		if(getLayoutParams() == null) {
			this.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
				public void onGlobalLayout() {
					eventListener.onLaidOut(getLayoutParams().width);
					removeOnGlobalLayoutListener(SVGImageView.this, this);
				}
			});
			requestLayout();
		}
		else eventListener.onLaidOut(getLayoutParams().width);
	}

	@SuppressLint("NewApi")
	public static void removeOnGlobalLayoutListener(View v, ViewTreeObserver.OnGlobalLayoutListener listener){
		if (Build.VERSION.SDK_INT < 16)
			v.getViewTreeObserver().removeGlobalOnLayoutListener(listener);
		else v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
	}

	@SuppressLint("NewApi")
	public static void disableHardwareAcceleration(View v){
		if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB)
			v.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
	}

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/filters/BitmapColorOverlayFilter.java<|end_filename|>
package com.guardanis.imageloader.filters;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;

public class BitmapColorOverlayFilter extends ImageFilter<Bitmap> {

    private int bitmapColorOverlay;

    public BitmapColorOverlayFilter(Context context, int bitmapColorOverlay) {
        super(context);
        this.bitmapColorOverlay = bitmapColorOverlay;
    }

    @Override
    public Bitmap filter(Bitmap unedited) {
        if(unedited != null){
            if(!unedited.isMutable())
                unedited = mutate(unedited);

            Paint paint = new Paint();
            paint.setColor(bitmapColorOverlay);

            Canvas canvas = new Canvas(unedited);
            canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);
        }

        return unedited;
    }

    @Override
    public String getAdjustmentInfo(){
        return getClass().getSimpleName() + "_" + bitmapColorOverlay;
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/stubs/builders/DefaultLoadingStubBuilder.java<|end_filename|>
package com.guardanis.imageloader.stubs.builders;

import android.content.Context;
import android.graphics.drawable.Drawable;

import com.guardanis.imageloader.stubs.DefaultLoadingDrawable;

public class DefaultLoadingStubBuilder implements StubBuilder {

    @Override
    public Drawable build(Context context) {
        return new DefaultLoadingDrawable(context.getResources());
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/transitions/modules/TranslateTransitionModule.java<|end_filename|>
package com.guardanis.imageloader.transitions.modules;


import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import android.view.animation.DecelerateInterpolator;

import com.guardanis.imageloader.stubs.StubDrawable;
import com.guardanis.imageloader.transitions.drawables.TransitionDrawable;

public class TranslateTransitionModule extends TransitionModule {

    protected float[] translateFrom;
    protected float[] translateTo;
    protected float[] difference;

    protected float[] currentTranslation = new float[2];

    /**
     * Translate the image
     * @param translateFrom [x, y] modifiers for starting position where { position = from[x, y] * [width, height] }, respectively
     * @param translateTo [x, y] modifiers for ending position where { position = to[x, y] * [width, height] }, respectively.
     * @param duration duration, in milliseconds
     */
    public TranslateTransitionModule(float[] translateFrom, float[] translateTo, long duration) {
        super(duration);
        this.translateFrom = translateFrom;
        this.translateTo = translateTo;

        this.difference = new float[]{
                translateTo[0] - translateFrom[0],
                translateTo[1] - translateFrom[1]
        };

        registerInterpolator(TransitionModule.INTERPOLATOR_IN, new DecelerateInterpolator());
    }

    @Override
    public void onStart(@Nullable Drawable old, Drawable target) { }

    @Override
    public void onPredrawOld(TransitionDrawable transitionDrawable, Canvas canvas, @Nullable Drawable old, long startTime) { }

    @Override
    public void revertPostDrawOld(TransitionDrawable transitionDrawable, @Nullable Drawable old) { }

    @Override
    public void onPredrawTarget(TransitionDrawable transitionDrawable, Canvas canvas, Drawable target, long startTime) {
        if(target instanceof StubDrawable)
            return;

        float percentCompleted = interpolate(TransitionModule.INTERPOLATOR_IN, startTime);

        currentTranslation[0] = translateFrom[0] + (percentCompleted * difference[0]);
        currentTranslation[1] = translateFrom[1] + (percentCompleted * difference[1]);

        canvas.translate(canvas.getWidth() * currentTranslation[0],
                canvas.getHeight() * currentTranslation[1]);
    }

    @Override
    public void revertPostDrawTarget(TransitionDrawable transitionDrawable, Drawable target) { }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/filters/BitmapBlurFilter.java<|end_filename|>
package com.guardanis.imageloader.filters;

import android.content.Context;
import android.graphics.Bitmap;
import androidx.renderscript.Allocation;
import androidx.renderscript.Element;
import androidx.renderscript.RenderScript;
import androidx.renderscript.ScriptIntrinsicBlur;

import com.guardanis.imageloader.ImageUtils;

public class BitmapBlurFilter extends ImageFilter<Bitmap> {

    private float blurRadius;
    private static RenderScript renderScript;

    public BitmapBlurFilter(Context context, int blurRadius) {
        super(context);
        this.blurRadius = blurRadius;
    }

    public Bitmap filter(Bitmap unedited) {
        try{
            if(renderScript == null)
                renderScript = RenderScript.create(context);

            if(!unedited.isMutable())
                unedited = mutate(unedited);

            final Allocation input = Allocation.createFromBitmap(renderScript,
                    unedited,
                    Allocation.MipmapControl.MIPMAP_NONE,
                    Allocation.USAGE_SCRIPT);

            final Allocation output = Allocation.createTyped(renderScript,
                    input.getType());

            final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(renderScript,
                    Element.U8_4(renderScript));

            script.setRadius(blurRadius);
            script.setInput(input);
            script.forEach(output);

            output.copyTo(unedited);
        }
        catch(OutOfMemoryError e){ ImageUtils.log(context, e); }
        catch(Exception e){ ImageUtils.log(context, e); }

        return unedited;
    }

    @Override
    public String getAdjustmentInfo(){
        return getClass().getSimpleName() + "_" + blurRadius;
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/stubs/builders/StubBuilder.java<|end_filename|>
package com.guardanis.imageloader.stubs.builders;

import android.content.Context;
import android.graphics.drawable.Drawable;

public interface StubBuilder {

    public Drawable build(Context context);

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/processors/ImageDrawableResourceProcessor.java<|end_filename|>
package com.guardanis.imageloader.processors;

import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;

import com.guardanis.imageloader.ImageRequest;
import com.guardanis.imageloader.ImageUtils;
import com.guardanis.imageloader.filters.ImageFilter;

import java.io.File;
import java.util.List;

public class ImageDrawableResourceProcessor extends ImageFileProcessor {

    @Override
    public Drawable process(ImageRequest request, List<ImageFilter<Bitmap>> bitmapImageFilters) throws Exception {
        File editedImageFile = request.getEditedRequestFile();
        int requiredImageWidth = request.getTargetImageWidth();

        if(editedImageFile.exists())
            return decodeBitmapDrawable(request.getContext(), editedImageFile, requiredImageWidth);

        Bitmap uneditedBitmap = ImageUtils.decodeDrawableResource(request.getContext(), request.getTargetResourceId(), requiredImageWidth);

        return process(request.getContext(), uneditedBitmap, editedImageFile, bitmapImageFilters);
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/filters/BitmapCircularCropFilter.java<|end_filename|>
package com.guardanis.imageloader.filters;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;

public class BitmapCircularCropFilter extends ImageFilter<Bitmap> {

    private float scale = 1f;
    private float[] offsets = new float[]{ 0, 0 };

    public BitmapCircularCropFilter(Context context) {
        super(context);
    }

    /**
     * @param scale scale < 1 = larger (e.g. circle), scale > 1 = smaller (e.g. rounded rectangle)
     */
    public BitmapCircularCropFilter setScaledDimensions(float scale){
        this.scale = scale;
        return this;
    }

    public BitmapCircularCropFilter setOffsets(float x, float y){
        this.offsets = new float[]{ x, y };
        return this;
    }

    @Override
    public Bitmap filter(Bitmap unedited) {
        Bitmap cropped = Bitmap.createBitmap(unedited.getWidth(), unedited.getHeight(), Bitmap.Config.ARGB_8888);

        int xScaled = (int)(scale * unedited.getWidth());
        int yScaled = (int)(scale * unedited.getHeight());

        int xTranslate = (int)(offsets[0] * xScaled) + ((unedited.getWidth() - xScaled) / 2);
        int yTranslate = (int)(offsets[1] * yScaled) + ((unedited.getHeight() - yScaled) / 2);

        Paint paint = new Paint();
        paint.setAntiAlias(true);

        Canvas canvas = new Canvas(cropped);
        canvas.clipPath(getCircularClippingPath(cropped));
        canvas.drawBitmap(unedited, new Rect(xTranslate, yTranslate, xTranslate + xScaled, yTranslate + yScaled),
                new Rect(0, 0, unedited.getWidth(), unedited.getHeight()), paint);

        return cropped;
    }

    private Path getCircularClippingPath(Bitmap bitmap) {
        int targetRadius = Math.min(bitmap.getWidth(), bitmap.getHeight()) / 2;

        Path path = new Path();
        path.addCircle(bitmap.getWidth() / 2f,
                bitmap.getHeight() / 2f,
                targetRadius,
                Path.Direction.CCW);

        return path;
    }

    @Override
    public String getAdjustmentInfo(){
        return super.getAdjustmentInfo() + "-" + scale +"," + offsets[0] + "," + offsets[1] + "-";
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/MemoryCache.java<|end_filename|>
package com.guardanis.imageloader;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import androidx.collection.LruCache;

public class MemoryCache {

    private static MemoryCache instance;
    public static MemoryCache getInstance(Context context){
        if(instance == null)
            instance = new MemoryCache(context);

        return instance;
    }

    protected LruCache<String, BitmapDrawable> cache;

    protected MemoryCache(Context context){
        int maxAvailableMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        int availableMemoryReciprical = context.getResources()
                .getInteger(R.integer.ail__lru_available_memory_reciprical);

        int cacheSize = maxAvailableMemory / availableMemoryReciprical;

        this.cache = new LruCache<String, BitmapDrawable>(cacheSize) {
            protected int sizeOf(String key, BitmapDrawable drawable) {
                return (drawable.getBitmap().getRowBytes() * drawable.getBitmap().getHeight()) / 1024;
            }
        };
    }

    public MemoryCache put(Context context, String key, Bitmap bitmap){
        this.cache.put(key, new BitmapDrawable(context.getResources(), bitmap));
        return this;
    }

    public MemoryCache put(String key, BitmapDrawable drawable){
        this.cache.put(key, drawable);
        return this;
    }

    public BitmapDrawable get(String key){
        return cache.get(key);
    }

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/StubHolder.java<|end_filename|>
package com.guardanis.imageloader;

import android.content.Context;
import android.graphics.drawable.Drawable;

@Deprecated
public interface StubHolder {

    public Drawable getLoadingDrawable(Context context);

    public Drawable getErrorDrawable(Context context);

}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/filters/BitmapColorOverrideFilter.java<|end_filename|>
package com.guardanis.imageloader.filters;

import android.content.Context;
import android.graphics.Bitmap;

public class BitmapColorOverrideFilter extends ImageFilter<Bitmap> {

    private static final int BASE_MASK = 0x00FFFFFF;
    private static final int ALPHA_POS = 24;

    private int replacementColor;

    /**
     * Replace all color values with the supplied color value while maintaining proper opacity at each pixel.
     * Intended for single color-scaled images.
     * @param replacementColor the 24-bit color value to replace with (0xAARRGGBB). Note: alpha will be ignored
     */
    public BitmapColorOverrideFilter(Context context, int replacementColor) {
        super(context);
        this.replacementColor = replacementColor;
    }

    @Override
    public Bitmap filter(Bitmap unedited) {
        if(unedited != null){
            if(!unedited.isMutable())
                unedited = mutate(unedited);

            unedited.setPixels(getReplacementPixels(unedited), 0, unedited.getWidth(),
                    0, 0, unedited.getWidth(), unedited.getHeight());
        }

        return unedited;
    }

    private int[] getReplacementPixels(Bitmap copy){
        int[] pixels = new int [copy.getWidth() * copy.getHeight()];

        copy.getPixels(pixels, 0, copy.getWidth(),
                0, 0, copy.getWidth(), copy.getHeight());

        for(int i = 0; i < pixels.length; i++)
            pixels[i] = ((pixels[i] >> ALPHA_POS) << ALPHA_POS) | (replacementColor & BASE_MASK);

        return pixels;
    }

    @Override
    public String getAdjustmentInfo(){
        return getClass().getSimpleName() + "_" + replacementColor;
    }
}


<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/filters/BitmapCenterCropFilter.java<|end_filename|>
package com.guardanis.imageloader.filters;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import androidx.annotation.NonNull;

public class BitmapCenterCropFilter extends ImageFilter<Bitmap> {

    private int[] desiredBounds;

    /**
     * @param context
     * @param desiredBounds non-null width/height, respectively
     */
    public BitmapCenterCropFilter(Context context, @NonNull int[] desiredBounds) {
        super(context);
        this.desiredBounds = desiredBounds;
    }

    @Override
    public Bitmap filter(Bitmap unedited) {
        float scale = Math.max(desiredBounds[0] / (float) unedited.getWidth(),
                desiredBounds[1] / (float) unedited.getHeight());

        int xTranslate = (int) -((scale * unedited.getWidth()) - desiredBounds[0]) / 2;
        int yTranslate = (int) -((scale * unedited.getHeight()) - desiredBounds[1]) / 2;

        Bitmap adjusted = Bitmap.createBitmap(desiredBounds[0],
                desiredBounds[1],
                unedited.getConfig());

        Canvas canvas = new Canvas(adjusted);
        canvas.translate(xTranslate, yTranslate);
        canvas.scale(scale, scale);

        canvas.drawBitmap(unedited, 0, 0, null);

        return adjusted;
    }

    @Override
    public String getAdjustmentInfo(){
        return getClass().getSimpleName() + "_" + desiredBounds[0] + "," + desiredBounds[1];
    }
}

<|start_filename|>imageloader/src/main/java/com/guardanis/imageloader/FileCache.java<|end_filename|>
package com.guardanis.imageloader;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import androidx.core.content.ContextCompat;

import java.io.File;

public class FileCache {

    private Context context;

    private File cacheDir;

    private boolean externalStorageEnabled;
    private boolean targetStorageExternal = false;

    public FileCache(Context context) {
        this.context = context.getApplicationContext();

        this.externalStorageEnabled = context.getResources()
                .getBoolean(R.bool.ail__external_storage_enabled);
    }

    private void ensureCacheDirectoryValid(){
        if(cacheDir == null
                || (targetStorageExternal && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED))
            updateCacheDirectory();
    }

    private void updateCacheDirectory(){
        if(externalStorageEnabled
                && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
                && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            cacheDir = new File(Environment.getExternalStorageDirectory(), context.getPackageName());

            targetStorageExternal = true;
        }
        else {
            cacheDir = context.getResources().getBoolean(R.bool.ail__use_cache_dir)
                    ? context.getCacheDir()
                    : context.getFilesDir();

            targetStorageExternal = false;
        }

        if(!cacheDir.exists())
            cacheDir.mkdirs();
    }

    public File getFile(String url) {
        ensureCacheDirectoryValid();

        String filename = String.valueOf(url.hashCode())
                + (url.endsWith("svg") ? ".svg" : ""); // Since we're using file extensions and not descriptors for SVGs

        return new File(cacheDir, filename);
    }

    public void clear() {
        ensureCacheDirectoryValid();

        File[] files = cacheDir.listFiles();

        if(files != null)
            for(File file : files)
                file.delete();
    }

    public static void clear(Context context){
        new FileCache(context)
                .clear();
    }

    public boolean isCachedFileValid(String url, long maxCacheDurationMs){
        return maxCacheDurationMs < 0
                || System.currentTimeMillis() - getLastModifiedAt(url) < maxCacheDurationMs;
    }

    public long getLastModifiedAt(String url){
        return getFile(url)
                .lastModified();
    }

    public void delete(String url){
        getFile(url)
                .delete();
    }
}