public interface DiskCache {

    /**
     * An interface for lazily creating a disk cache.
     */
    interface Factory {
        /**
         * Returns a new disk cache, or {@code null} if no disk cache could be created.
         */
        DiskCache build();
    }

    /**
     * An interface to actually write data to a key in the disk cache.
     */
    interface Writer {
        /**
         * Writes data to the file and returns true if the write was successful and should be committed, and
         * false if the write should be aborted.
         *
         * @param file The File the Writer should write to.
         */
        boolean write(File file);
    }

    /**
     * Get the cache for the value at the given key.
     *
     * <p>
     *     Note - This is potentially dangerous, someone may write a new value to the file at any point in timeand we
     *     won't know about it.
     * </p>
     *
     * @param key The key in the cache.
     * @return An InputStream representing the data at key at the time get is called.
     */
    File get(String key);

    /**
     * Write to a key in the cache. {@link Writer} is used so that the cache implementation can perform actions after
     * the write finishes, like commit (via atomic file rename).
     *
     * @param key The key to write to.
     * @param writer An interface that will write data given an OutputStream for the key.
     */
    void put(String key, Writer writer);

    /**
     * Remove the key and value from the cache.
     *
     * @param key The key to remove.
     */
    void delete(String key);

    /**
     * Clear the cache.
     */
    void clear();
} public class DiskCacheStrategy {

	public static final int NONE = 1;
	public static final int SOURCE = 2;
	public static final int RESULT = 4;
	public static final int ALL = 8;

	@IntDef({DiskCacheStrategy.SOURCE,
			DiskCacheStrategy.RESULT,
			DiskCacheStrategy.ALL,
			DiskCacheStrategy.NONE})
	@Retention(RetentionPolicy.SOURCE)
	public @interface Enforce {}

} public class BitmapLruCache extends LruCacheAdapter<String, Bitmap> {

	private LruCache<String, Bitmap> impl;
	
	public BitmapLruCache(float percentOfAvailableMemory) {
		super(new MyCache((int)(Runtime.getRuntime().maxMemory() * percentOfAvailableMemory)));
		impl = getLruCache();
	}
	
	public BitmapLruCache(int maxSize) {
		super(new MyCache(maxSize));
		impl = getLruCache();
	}

	@Override
	public boolean has(String key) {
		if (key == null) return false;
		Bitmap value = get(key);
		return (value != null && !value.isRecycled());
	}
	
	private static class MyCache extends LruCache<String, Bitmap> {
		public MyCache(int maxSize) {
			super(maxSize);
		}
		
		@Override
		protected int sizeOf(String key, Bitmap value) {
			if (value == null) return 0;
			return getBitmapSize(value);
		}
		
		/**
	     * Get the size in bytes of a bitmap.
	     * @param bitmap
	     * @return size in bytes
	     */
	    private int getBitmapSize(Bitmap bitmap) {
			return bitmap.getByteCount();
	    }
	}
	
} public interface MemoryCache<K, V> {

	/**
	 * Returns the sum of the sizes of all the contents of the cache in bytes.
	 */
	int getCurrentSize();

	/**
	 * Returns the current maximum size in bytes of the cache.
	 */
	int getMaxSize();

	public boolean has(K key);
	public V get(K key);
	public void put(K key, V value);
	public void remove(K key);
	public void clear();


	
} public class SimpleUrlConnectionFactory implements UrlFactory {

	private static final int MAX_REDIRECTS = 5;

	private Map<String, String> requestProps;

	public SimpleUrlConnectionFactory() {

	}

	public SimpleUrlConnectionFactory(Map<String, String> requestProps) {
		this.requestProps = requestProps;
	}

	public InputStream getStream(Uri uri) throws IOException {
		return createConnection(uri, null, 0);
	}

	private InputStream createConnection(Uri uri, Uri lastUri, int numRedirects) throws IOException {
		if (numRedirects >= MAX_REDIRECTS) {
			throw new IOException("Max number of directs");
		} else if (lastUri != null && uri.toString().equals(lastUri.toString())) {
			throw new IOException("Infinite redirect loop");
		}

		URL u = new URL(uri.toString());
		HttpURLConnection conn = (HttpURLConnection)u.openConnection();
		conn.setConnectTimeout(2500);
		conn.setReadTimeout(2500);
		conn.setUseCaches(false);
		conn.setDoInput(true);
		conn.setInstanceFollowRedirects(false);

		if (requestProps != null) {
			Iterator<Map.Entry<String, String>> it = requestProps.entrySet().iterator();
			while (it.hasNext()) {
				Map.Entry<String, String> entry = it.next();
				if (!entry.getKey().equals("Authorization")) {
					conn.addRequestProperty(entry.getKey(), entry.getValue());
				} else if ( lastUri == null || lastUri.getHost().equals(uri.getHost()) ) {
					conn.addRequestProperty(entry.getKey(), entry.getValue());
				}
			}
		}

		conn.connect();
		final int statusCode = conn.getResponseCode();
		if (statusCode / 100 == 2) {
			return conn.getInputStream();
		} else if (statusCode / 100 == 3) {
			String redirectUrlString = conn.getHeaderField("Location");
			if (TextUtils.isEmpty(redirectUrlString)) {
				throw new IOException("Received empty or null redirect url");
			}
			return createConnection(Uri.parse(redirectUrlString), uri, ++numRedirects);
		} else {
			if (statusCode == -1) {
				throw new IOException("Unable to retrieve response code from HttpUrlConnection.");
			}
			throw new IOException("Request failed " + statusCode + ": "
					+ conn.getResponseMessage());
		}
	}
} public class UriProvider extends SavingStreamProvider {

    private final MirageRequest request;
    private static final int IO_BUFFER_SIZE = 8 * 1024;

    public UriProvider(MirageRequest request) {
        super(request);
        this.request = request;
    }

    @Override
    protected InputStream stream() throws IOException {
        InputStream in = new BufferedInputStream(getInputStream(), IO_BUFFER_SIZE);
        return in;
    }

    private InputStream getInputStream() throws IOException {
        return request.urlFactory().getStream(request.uri());
    }
} public class MirageRequest {

	private static final KeyMaker defaultKeymaker;
	static {
		defaultKeymaker = new SimpleKeyMaker();
	}

	private final Object LOCK = new Object();
	private Mirage mirage;
	private Uri uri;
	private boolean skipWritingMemoryCache = false;
	private boolean skipReadingMemoryCache = false;
	private boolean skipReadingDiskCache = false;
	private Executor executor;
	private MemoryCache<String, Bitmap> memoryCache;
	private DiskCache diskCache;
	private Target<?> target;
	private BitmapFactory.Options options;
	private Rect outPadding;
	private int diskCacheStrategy = DiskCacheStrategy.RESULT;
	private KeyMaker keyMaker;
	private UrlFactory urlFactory;
	private List<BitmapProcessor> processors;
	private ResizeProcessor resizeProcessor;
    private int resizeTargetDimen = -1;
    private boolean resizeSampleUndershoot = false;
	private BitmapProvider bitmapProvider;
	private Lifecycle lifecycle;


	/**
	 * Default constructor. You probably shouldn't call this directly but
	 * let {@link com.climate.mirage.Mirage#load(String)} return an instance for you.
	 */
	public MirageRequest() {
		keyMaker = defaultKeymaker;
	}

	/**
	 * Sets the object back to an empty state and releases resources
	 */
	public void recycle() {
		mirage = null;
		uri = null;
		skipWritingMemoryCache = false;
		skipReadingMemoryCache = true;
		skipReadingDiskCache = false;
		executor = null;
		memoryCache = null;
		diskCache = null;
		target = null;
		options = null;
		outPadding = null;
		diskCacheStrategy = DiskCacheStrategy.RESULT;
		keyMaker = defaultKeymaker;
		urlFactory = null;
        resizeTargetDimen = -1;
        resizeSampleUndershoot = false;
		if (processors != null) {
			processors.clear();
		}
		if (resizeProcessor != null) {
			resizeProcessor.setDimensions(0, 0, ResizeProcessor.STRATEGY_SCALE_FREE);
		}
        bitmapProvider = null;
		lifecycle = null;
	}

	@Nullable
	public Uri uri() {
		return uri;
	}

	/**
	 * Set the uri of the resource to load. Valid schemes are
	 * http, https, file, and content
	 *
	 * @param uri the uri of the resource to load
	 * @return class instance to daisy chain
	 */
	public MirageRequest uri(Uri uri) {
		this.uri = uri;
		return this;
	}

	/**
	 * If the lifecycle is different than the activity, you can add a
	 * custom one. If it's the activity, it's automatically taken care of.
	 * @return class instance to daisy chain
	 */
	public MirageRequest lifecycle(Lifecycle lifecycle) {
		this.lifecycle = lifecycle;
		return this;
	}

	@Nullable
	public Lifecycle lifecycle() {
		return lifecycle;
	}

	public MirageRequest provider(BitmapProvider provider) {
		this.bitmapProvider = provider;
		return this;
	}

	public BitmapProvider provider() {
	    return bitmapProvider;
    }

	public MirageRequest mirage(Mirage mirage) {
		this.mirage = mirage;
		return this;
	}

	public boolean isSkipWritingMemoryCache() {
		return skipWritingMemoryCache;
	}

	/**
	 * Sets that the loaded resource should not be added to the memory cache.
	 * If wish to not check the cache during loading call {@link #skipReadingMemoryCache(boolean)} (boolean)}
	 *
	 * @param skip true if you do not wish to place the loaded resource into the cache.
	 *             This is a good idea when the resource is very large and would eject lots
	 *             of other items from the cache
	 * @return class instance to daisy chain
	 */
	public MirageRequest skipWritingMemoryCache(boolean skip) {
		this.skipWritingMemoryCache = skip;
		return this;
	}

	public boolean isSkipReadingMemoryCache() {
		return skipReadingMemoryCache;
	}

	/**
	 * Sets if the memory cache should be checked during loading. If wish to not place
	 * the loaded bitmap into the memory cache call {@link #skipWritingMemoryCache(boolean)}
	 *
	 * @param skip true if you wish to ignore checking the cache during load
	 * @return class instance to daisy chain
	 */
	public MirageRequest skipReadingMemoryCache(boolean skip) {
		this.skipReadingMemoryCache = skip;
		return this;
	}

	public int diskCacheStrategy() {
		return diskCacheStrategy;
	}

	/**
	 * The strategy used to cache the resource that is loaded.
	 * Valid arguments are from {@link com.climate.mirage.cache.disk.DiskCacheStrategy}
	 *
	 * @param strategy The strategy to use for caching
	 * @return class instance to daisy chain
	 */
	public MirageRequest diskCacheStrategy(@DiskCacheStrategy.Enforce int strategy) {
		this.diskCacheStrategy = strategy;
		return this;
	}

	public boolean isSkipReadingDiskCache() {
		return skipReadingDiskCache;
	}

	/**
	 * Sets if the disk cache should be checked during loading. If wish to not place
	 * the loaded bitmap into the disk cache call {@link #diskCacheStrategy(int)} and set to
	 * {@value com.climate.mirage.cache.disk.DiskCacheStrategy#NONE}
	 *
	 * @param skip true if you wish to ignore checking the cache during load
	 * @return class instance to daisy chain
	 */
	public MirageRequest skipReadingDiskCache(boolean skip) {
		this.skipReadingDiskCache = skip;
		return this;
	}

	public KeyMaker keyMaker() {
		return keyMaker;
	}

	/**
	 * Sets the strategy for providing the keys for the disk cache.
	 *
	 * @param keyMaker strategy for providing the keys for the disk cache.
	 * @return class instance to daisy chain
	 */
	public MirageRequest keyMaker(KeyMaker keyMaker) {
		this.keyMaker = keyMaker;
		return this;
	}

	/**
	 * Resizes the loaded bitmap once in memory to the sizes specified here. If
	 * you are trying to resize to conserve memory use the {@link #options(android.graphics.BitmapFactory.Options)}
	 * method to resample the bitmap during streaming.
	 *
	 * @param width width the new bitmap should be
	 * @param height height the new bitmap should be
	 * @return class instance to daisy chain
	 */
	public MirageRequest resize(int width, int height) {
		return resize(width, height, ResizeProcessor.STRATEGY_RATIO_MAINTAINED);
	}

	/**
	 * Resizes the loaded bitmap once in memory to the sizes specified here. If
	 * you are trying to resize to conserve memory use the {@link #options(android.graphics.BitmapFactory.Options)}
	 * method to resample the bitmap during streaming.
	 *
	 * @param width width the new bitmap should be
	 * @param height height the new bitmap should be
	 * @param strategy defines how the resize should take place. Is one of the constants from
     *                 {@link ResizeProcessor}
	 *                 and can combined like <code>STRATEGY_SCALE_DOWN_ONLY | STRATEGY_RATIO_MAINTAINED</code>
	 * @return class instance to daisy chain
	 */
	public MirageRequest resize(int width, int height, int strategy) {
		if (width < 1 || height < 1) throw new IllegalArgumentException("dimens must be greater than 0");
		if (resizeProcessor == null) {
			resizeProcessor = new ResizeProcessor();
			resizeProcessor.setDimensions(width, height, strategy);
		} else {
			resizeProcessor.setDimensions(width, height, strategy);
		}
		addProcessor(resizeProcessor);
		return this;
	}

	/**
	 * Sets the {@link android.graphics.BitmapFactory.Options} to use during decompression
	 *
	 * @param options see {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect, android.graphics.BitmapFactory.Options)}
	 * @param outPadding see {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect, android.graphics.BitmapFactory.Options)}
	 * @return class instance to daisy chain
	 */
	public MirageRequest options(BitmapFactory.Options options, Rect outPadding) {
		this.options = options;
		this.outPadding = outPadding;
		return this;
	}

	/**
	 * Sets the {@link android.graphics.BitmapFactory.Options} to use during decompression
	 *
	 * @param options see {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect, android.graphics.BitmapFactory.Options)}
	 * @return class instance to daisy chain
	 */
	public MirageRequest options(BitmapFactory.Options options) {
		return options(options, null);
	}

	/**
	 * Set's the inSampleSize used by
	 * {@link android.graphics.BitmapFactory.Options} to use during decompression
	 *
	 * @param sampleSize see {@link android.graphics.BitmapFactory.Options#inSampleSize}
	 * @return class instance to daisy chain
	 */
	public MirageRequest inSampleSize(int sampleSize) {
		BitmapFactory.Options opts = new BitmapFactory.Options();
		opts.inSampleSize = sampleSize;
		return options(opts, null);
	}

    /**
     * Method to dynamically adjust the inSampleSize based upon the read dimension of the InputStream.
     * When this method is called and the images aren't in memory cache or result cache,
     * it causes the load system to do a double pass. The first pass
     * is to decode only the bounds of the image. The next pass dynamically sets the
     * {@link android.graphics.BitmapFactory.Options#inSampleSize} and loads the image again.
     *
     * The width and height params here are only estimates as inSampleSize only allows the image
     * to scale in powers of 2, it can not produce an exact size. If an exact size is needed, also
     * call {@link #resize(int, int)} which runs in memory
     *
     * @param resizeTargetDimen the desired (non exact) length of the greatest side of the image.
     *                          So if the image is 2000x1000, the targetMaxSideSize will match up
     *                          to 2000.
     * @param isUndershootSizes true if the inSampleSize should err on the size on making the images smaller.
     * @return class instance to daisy chain
     */
	public MirageRequest dynamicResample(int resizeTargetDimen, boolean isUndershootSizes) {
        this.resizeTargetDimen = resizeTargetDimen;
        this.resizeSampleUndershoot = isUndershootSizes;
        return this;
    }

	public MirageRequest dynamicResample(int resizeTargetDimen) {
        return dynamicResample(resizeTargetDimen, false);
	}

    public boolean isInSampleSizeDynamic() {
        return resizeTargetDimen > 0;
    }

	public int getResizeTargetDimen() {
		return resizeTargetDimen;
	}

	public boolean isResizeSampleUndershoot() {
		return resizeSampleUndershoot;
	}

	public BitmapFactory.Options options() {
		return options;
	}

	public Rect outPadding() {
		return outPadding;
	}

	/**
	 * Sets which executor this particular request will run on.
	 *
	 * @param executor see {@link java.util.concurrent.Executor}
	 * @return class instance to daisy chain
	 */
	public MirageRequest executor(Executor executor) {
		this.executor = executor;
		return this;
	}

	public Executor executor() {
		return this.executor;
	}

	/**
	 * A factory to retrieve a {@link java.net.URLConnection} instance. If you need to
	 * customize the URLConnection object to add authorization headers or anything else
	 * pass in a {@link com.climate.mirage.load.UrlFactory}
	 *
	 * @param urlFactory the factory to use for this request
	 * @return class instance to daisy chain
	 */
	public MirageRequest urlFactory(UrlFactory urlFactory) {
		this.urlFactory = urlFactory;
		return this;
	}

	public UrlFactory urlFactory() {
		return urlFactory;
	}

	/**
	 * Sets the memory cache to be used for this particular request. If you set a cache here
	 * the default cache from {@link com.climate.mirage.Mirage} will not be used.
	 *
	 * @param memoryCache a specific cache for use for this request
	 * @return class instance to daisy chain
	 */
	public MirageRequest memoryCache(MemoryCache<String, Bitmap> memoryCache) {
		this.memoryCache = memoryCache;
		return this;
	}

	public MemoryCache<String, Bitmap> memoryCache() {
		return memoryCache;
	}

	/**
	 * Sets the disk cache to be used for this particular request. If you set a cache here
	 * the default cache from {@link com.climate.mirage.Mirage} will not be used.
	 *
	 * @param diskCache a specific cache for use for this request
	 * @return class instance to daisy chain
	 */
	public MirageRequest diskCache(DiskCache diskCache) {
		this.diskCache = diskCache;
		return this;
	}

	public DiskCache diskCache() {
		return diskCache;
	}

	public Target target() {
		return target;
	}

	/**
	 * Defines a target/callback for when the load completes. Once load completes it's up to the
	 * target to define what to do with the loaded resource.
	 *
	 * @param target A generic callback which handles the loaded resource
	 * @return The instance and type of the target passed in
	 */
	public <T extends Target> T into(T target) {
		this.target = target;
		return target;
	}

	/**
	 * A convenience method to set the target that the load will go into.
	 *
	 * @param imageView plain old {@link android.widget.ImageView}
	 * @return the {@link com.climate.mirage.targets.ImageViewTarget} to configure before loading begins
	 */
	public ImageViewTarget into(ImageView imageView) {
		return into(new ImageViewTarget(this, imageView));
	}

    // TODO: make one for just plain View which goes into the setBackground()

	/**
	 * A convenience method to set the target that the load will go into.
	 *
	 * @param textView plain old {@link android.widget.TextView}
	 * @return the {@link com.climate.mirage.targets.ImageViewTarget} to configure before loading begins
	 */
	public TextViewTarget into(TextView textView) {
		return into(new TextViewTarget(this, textView));
	}

	/**
	 * Sends the request into action onto a background thread.

	 * @return The task for running the action or null if the resource was found in memory
	 */
	public MirageTask go() {
		if (mirage == null) throw new IllegalStateException("Must set a mirage instance before calling go");
		return mirage.go(this);
	}

	/**
	 * Sends the request into action using the current thread. This must not happen
	 * on the UI Thread or an exception is thrown
	 *
	 * @return The loaded image
	 */
	public Bitmap goSync() throws MirageIOException, InterruptedIOException {
		if (mirage == null) throw new IllegalStateException("Must set a mirage instance before calling goSync");
		return mirage.goSync(this);
	}

	public MirageTask<Void, Void, Bitmap> createGoTask() {
        if (mirage == null) throw new IllegalStateException("Must set a mirage instance before calling createGoTask");
        return mirage.createGoTask(this);
	}

    public void executeGoTask(MirageTask<Void, Void, Bitmap> task) {
        if (mirage == null) throw new IllegalStateException("Must set a mirage instance before calling executeGoTask");
        mirage.executeGoTask(task, this);
    }

	/**
	 * Downloads the image only to file. The image is not read into memory
	 *
	 * @return The task for running the action
	 */
	public MirageTask downloadOnly() {
		if (mirage == null) throw new IllegalStateException("Must set a mirage instance before calling goSync");
		return mirage.downloadOnly(this);
	}

	/**
	 * Downloads the image only to file using the current thread. This must not happen
	 * on the UI Thread or an exception is thrown.
	 *
	 * @return The File where the loaded image was stored
	 */
	public File downloadOnlySync() throws MirageIOException {
		if (mirage == null) throw new IllegalStateException("Must set a mirage instance before calling goSync");
		return mirage.downloadOnlySync(this);
	}

	/**
	 * Gets the defined processors
	 *
	 * @return List of processors
	 */
	public List<BitmapProcessor> getProcessors() {
		return processors;
	}

	/**
	 * Adds a {@link com.climate.mirage.processors.BitmapProcessor} to be used once the
	 * image is loaded from the source. The processor is then ran on the source to return
	 * back a resulting bitmap.
	 *
	 * @param processor A processor to manipulate the image
	 */
	public MirageRequest addProcessor(BitmapProcessor processor) {
		synchronized (LOCK) {
			if (this.processors == null) {
				this.processors = Collections.synchronizedList(new ArrayList<BitmapProcessor>());
			}
		}
		processors.add(processor);
		return this;
	}

	/**
	 * The key used for saving the resource as a source in the cache
	 *
	 * @return the key used for saving the the resource as a source in the cache
	 */
	public String getSourceKey() {
		return keyMaker.getSourceKey(this);
	}

	/**
	 * The key used for saving the resource as a result in the cache
	 *
	 * @return The key used for saving the resource as a result in the cache
	 */
	public String getResultKey() {
		return keyMaker.getResultKey(this);
	}

	/**
	 * Helper method to determine if the calling request should save the source file
	 *
	 * @return true if the source should be saved. false if not, which could mean
	 * 			to save the result
	 */
	public boolean isRequestShouldSaveSource() {
		if (diskCacheStrategy() == DiskCacheStrategy.NONE) return false;
		return (diskCacheStrategy() == DiskCacheStrategy.SOURCE
				|| diskCacheStrategy() == DiskCacheStrategy.ALL
				|| getSourceKey().equals(getResultKey())
				&& diskCache() != null);
	}

} @Implements(AsyncTask.class)
public class CancelAsyncTaskShadow<Params, Progress, Result>
        extends ShadowAsyncTask<Params, Progress, Result> {

    @RealObject
    private AsyncTask<Params, Progress, Result> realAsyncTask;
    private AsyncTask.Status status = AsyncTask.Status.PENDING;

    public CancelAsyncTaskShadow() {
        super();
    }

    @Implementation
    public AsyncTask<Params, Progress, Result> execute(Params... params) {
        status = AsyncTask.Status.FINISHED;
        getBridge().onCancelled(null);
        return this.realAsyncTask;
    }

    @Implementation
    public AsyncTask<Params, Progress, Result> executeOnExecutor(Executor executor, Params... params) {
        status = AsyncTask.Status.FINISHED;
        getBridge().onCancelled(null);
        return this.realAsyncTask;
    }

    @Override
    public AsyncTask.Status getStatus() {
        return status;
    }

    private ShadowAsyncTaskBrideExt<Params, Progress, Result> getBridge() {
        return new ShadowAsyncTaskBrideExt(this.realAsyncTask);
    }

} @Implements(AsyncTask.class)
public class NoExecuteAsyncTaskShadow<Params, Progress, Result>
        extends ShadowAsyncTask<Params, Progress, Result> {

    @RealObject
    private AsyncTask<Params, Progress, Result> realAsyncTask;
    private AsyncTask.Status status = AsyncTask.Status.PENDING;
    private Handler handler = new Handler();

    public NoExecuteAsyncTaskShadow() {
        super();
    }

    @Implementation
    public AsyncTask<Params, Progress, Result> execute(Params... params) {
//        status = AsyncTask.Status.FINISHED;
//        handler.postDelayed(delayed, 10);
        return this.realAsyncTask;
    }

    @Implementation
    public AsyncTask<Params, Progress, Result> executeOnExecutor(Executor executor, Params... params) {
//        status = AsyncTask.Status.FINISHED;
        return this.realAsyncTask;
    }

    @Override
    public AsyncTask.Status getStatus() {
        return status;
    }

//    private Runnable delayed = new Runnable() {
//        @Override
//        public void run() {
//            getBridge().onPostExecute(null);
//        }
//    };
//
//    private ShadowAsyncTaskBridge<Params, Progress, Result> getBridge() {
//        return new ShadowAsyncTaskBridge(this.realAsyncTask);
//    }
} @Implements(AsyncTask.class)
public class OnlyPostExecuteAsyncTaskShadow<Params, Progress, Result>
        extends ShadowAsyncTask<Params, Progress, Result> {

    @RealObject
    private AsyncTask<Params, Progress, Result> realAsyncTask;
    private AsyncTask.Status status = AsyncTask.Status.PENDING;

    public OnlyPostExecuteAsyncTaskShadow() {
        super();
    }

    @Implementation
    public AsyncTask<Params, Progress, Result> execute(Params... params) {
        status = AsyncTask.Status.FINISHED;
        getBridge().onPostExecute(null);
        return this.realAsyncTask;
    }

    @Implementation
    public AsyncTask<Params, Progress, Result> executeOnExecutor(Executor executor, Params... params) {
        status = AsyncTask.Status.FINISHED;
        getBridge().onPostExecute(null);
        return this.realAsyncTask;
    }

    @Override
    public AsyncTask.Status getStatus() {
        return status;
    }

    private ShadowAsyncTaskBridge<Params, Progress, Result> getBridge() {
        return new ShadowAsyncTaskBridge(this.realAsyncTask);
    }

} public class ImageViewTarget extends ViewTarget<ImageView> {

	public ImageViewTarget(ImageView imageView) {
		super(imageView);
	}

	public ImageViewTarget(MirageRequest request, ImageView imageView) {
		super(request, imageView);
	}

	@Override
	protected void onPreparingLoad(ImageView view) {
		if (getPlaceHolderResourceId() != 0) {
			view.setImageResource(getPlaceHolderResourceId());
		} else if (getPlaceHolderDrawable() != null) {
			view.setImageDrawable(getPlaceHolderDrawable());
		} else {
			view.setImageDrawable(null);
		}
	}

	@Override
	protected void onResult(ImageView view, Drawable drawable, Mirage.Source source, MirageRequest request) {
		if (animation() != null) {
			if (!animation().animate(view, drawable, source)) {
				view.setImageDrawable(drawable);
			}
		} else {
			view.setImageDrawable(drawable);
		}
	}

	@Override
	protected void onError(ImageView view, Exception e, Mirage.Source source, MirageRequest request) {
		if (animation() != null) {
			Drawable drawable;
			if (getErrorResourceId() > 0) {
				drawable = view.getContext().getResources().getDrawable(getErrorResourceId());
			} else {
				drawable = getErrorDrawable();
			}
			if (drawable != null) {
				if (!animation().animate(view, drawable, source)) {
					view.setImageDrawable(drawable);
				}
			} else {
				view.setImageDrawable(null);
			}
		} else {
			if (getErrorResourceId() != 0) {
				view.setImageResource(getErrorResourceId());
			} else if (getErrorDrawable() != null) {
				view.setImageDrawable(getErrorDrawable());
			} else {
				view.setImageDrawable(null);
			}
		}
	}

	@Override
	public ViewTarget<ImageView> fade() {
		return animation(new ImageViewCrossFadeAnimation());
	}
} public interface Target<Result> {

	/**
	 * This is called right before the AsyncTask fires and from the same thread as the
	 * {@link com.climate.mirage.Mirage#go(com.climate.mirage.requests.MirageRequest)}
	 * was called on.
	 */
	public void onPreparingLoad();

	/**
	 * Called on the main UI thread when loading was not canceled and download has completed.
	 *
	 * @param resouce The resource that was loaded
	 * @param source The location the resource came from. e.g Memory cache, disk cache, or web
	 * @param request the request that was used to load the resource. Do not hold onto a reference as
	 *                this will be recycled immediately after this method is called
	 */
	public void onResult(Result resouce, Mirage.Source source, MirageRequest request);


	/**
	 * Called on the main UI thread when loading was not canceled and download has an error.
	 *
	 * @param e The exception that occured
	 * @param source The location the error came from. e.g Memory cache, disk cache, or web
	 * @param request the request that was used to load the resource. Do not hold onto a reference as
	 *                this will be recycled immediately after this method is called
	 */
	public void onError(Exception e, Mirage.Source source, MirageRequest request);


    /**
     * Called if the loading task has been canceled. Targets can do any clean up here.
     */
    public void onCancel();

} public class MirageExecutor extends ThreadPoolExecutor {


	private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
	private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
	private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
	private static final int KEEP_ALIVE = 1;
	private static final ThreadFactory threadFactory = new ThreadFactory() {
		private final AtomicInteger mCount = new AtomicInteger(1);

		@Override
		public Thread newThread(Runnable r) {
			return new Thread(r, "MirageTask #" + mCount.getAndIncrement());
		}
	};

	private static final BlockingQueue<Runnable> sPoolWorkQueue =
			new LinkedBlockingQueue<Runnable>(128);

	public MirageExecutor() {
		super(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
				TimeUnit.SECONDS, sPoolWorkQueue, threadFactory);
	}

	@Override
	public String toString() {
		String org = super.toString();
		// TODO: list out all the running tasks
		return org;
	}
} abstract public class MirageTask<Params, Progress, Result>  extends AsyncTask<Params, Progress, Result>   {

	public static interface Callback<Result> {
		public void onCancel(MirageTask task, MirageRequest request);
		public void onPostExecute(MirageTask task, MirageRequest request, Result bitmap);
	}

	private static final String TAG = MirageTask.class.getSimpleName();
	private Exception exception; // out of memory exceptions and NPE
	private Callback<Result> callback;
	private MirageRequest request;
    private boolean didInternalCancel = false;
    private Lifecycle lifecycle;
	private LifecycleObserver lifecycleObserver;

	public MirageTask(MirageRequest request, Callback<Result> callback) {
		this.request = request;
		this.callback = callback;
		this.lifecycle = request.lifecycle();
		if (lifecycle != null) {
			lifecycleObserver = new LifecycleObserver() {
				@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
				public void onDestroy() {
					mirageCancel();
				}
			};
			lifecycle.addObserver(lifecycleObserver);
		}
	}

	@Override
	protected Result doInBackground(Params... params) {
		try {
			return doTask(params);
		} catch (IOException ex) {
			Log.e("MirageTask", "Logging IOException", ex);
			exception = ex;
			return null;
		} catch (Exception ex) {
			Log.e("MirageTask", "Logging Exception", ex);
			exception = ex;
			return null;
		}
	}

    /**
     * A call to this will ensure that the cancel calls happen
     * even if the task hasn't been executed yet. The code always
     * needs the cancel callbacks because our targets must be notified
     */
    public void mirageCancel() {
        // if the task is already running, let the onCancel call it
        // so we dont object pool recycle the request object in the
        // middle of doing working still
        if (getStatus() != Status.RUNNING) {
            internalCancel();
        }
        cancel(true);
    }

	@Override
	protected void onCancelled(Result result) {
		super.onCancelled(result);
        internalCancel();
	}

	@Override
	protected void onPostExecute(Result result) {
		super.onPostExecute(result);
		removeLifecycleObserver();
		if (isCancelled()) return;
		if (exception != null) {
			onPostError(exception);
		} else {
			onPostSuccess(result);
		}
		callback.onPostExecute(this, request, result);
	}

	abstract public Result doTask(Params... params) throws MirageIOException, InterruptedIOException;

	abstract protected void onPostSuccess(Result bitmap);
	abstract protected void onPostError(Exception exception);

    private void internalCancel() {
        if (didInternalCancel) return;
        didInternalCancel = true;
        if (request.target() != null) request.target().onCancel();
        callback.onCancel(this, request);
		removeLifecycleObserver();
    }

    private void removeLifecycleObserver() {
    	if (lifecycle != null) {
    		lifecycle.removeObserver(lifecycleObserver);
		}
    	lifecycle = null;
    	lifecycleObserver = null;
	}
}
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.LifecycleRegistry;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.widget.ImageView;
import com.climate.mirage.cache.disk.DiskCache;
import com.climate.mirage.cache.disk.DiskCacheStrategy;
import com.climate.mirage.cache.memory.BitmapLruCache;
import com.climate.mirage.cache.memory.MemoryCache;
import com.climate.mirage.load.SimpleUrlConnectionFactory;
import com.climate.mirage.load.UriProvider;
import com.climate.mirage.requests.MirageRequest;
import com.climate.mirage.shadows.CancelAsyncTaskShadow;
import com.climate.mirage.shadows.NoExecuteAsyncTaskShadow;
import com.climate.mirage.shadows.OnlyPostExecuteAsyncTaskShadow;
import com.climate.mirage.targets.ImageViewTarget;
import com.climate.mirage.targets.Target;
import com.climate.mirage.tasks.MirageExecutor;
import com.climate.mirage.tasks.MirageTask;
import junit.framework.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.io.File;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
package com.climate.mirage;






public class MirageTest extends RobolectricTest {

    @Before
    public void setUp() {
        Mirage.set(null);
    }

    @After
    public void tearDown() {
        Mirage.set(null);
    }

    @Test
    public void testGetsDefault() throws Exception {
        Mirage mirage = Mirage.get(RuntimeEnvironment.application);
        Assert.assertNotNull(mirage.getDefaultDiskCache());
        Assert.assertNotNull(mirage.getDefaultMemoryCache());
        Assert.assertNotNull(mirage.getDefaultExecutor());
    }

    @Test
    public void testSetDefault() throws Exception {
        Mirage mirage = new Mirage(RuntimeEnvironment.application);
        Mirage.set(mirage);
        Assert.assertSame(mirage, Mirage.get(RuntimeEnvironment.application));
    }


    @Test
    public void testLoadString() throws Exception {
        Mirage mirage = new Mirage(RuntimeEnvironment.application);
        MirageRequest request = mirage.load("http://www.google.com/");