com.bumptech.glide.integration.concurrent.GlideFutures Maven / Gradle / Ivy
package com.bumptech.glide.integration.concurrent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
import androidx.concurrent.futures.CallbackToFutureAdapter.Resolver;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.FutureTarget;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.util.Executors;
import com.google.common.base.Function;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.Executor;
/** Utilities for getting ListenableFutures out of Glide. */
public final class GlideFutures {
/**
* Preloads the resource for {@code builder} and returns a {@link ListenableFuture} that can be
* used to monitor status.
*
* Shorthand for simply calling {@link #submitAndExecute(RequestManager, RequestBuilder,
* ResourceConsumer, Executor)} with an empty {@code action}.
*/
// Wildcard resource types can't be directly instantiated, we don't need to care about the type
// here.
@SuppressWarnings({"rawtypes", "unchecked"})
public static ListenableFuture preload(
final RequestManager requestManager, RequestBuilder> builder, Executor executor) {
return submitAndExecute(
requestManager,
builder,
new ResourceConsumer() {
@Override
public void act(Object resource) {}
},
executor);
}
/**
* Acts on a resource loaded by Glide.
*
* @param The type of resource (Bitmap, Drawable etc).
*/
public interface ResourceConsumer {
void act(T resource);
}
/**
* Submits the provided request, performs the provided {@code action} and returns a {@link
* ListenableFuture} that can be used to cancel the request or monitor its status.
*
* Cancellation is best effort and may result in some resources not being returned back to
* Glide's pool. In particular, if the request is cancelled after the resource is loaded by Glide,
* but before {@code action} is run on {@code executor}, the resource will not be returned. We
* have the unfortunate choice between unsafely returning resources to the pool immediately when
* cancel is called while they may still be in use via {@link
* com.google.common.util.concurrent.ClosingFuture} or occasionally failing to return resources to
* the pool. Because failing to return resources to the pool is inefficient, but safe, that's the
* route we've chosen. A more sophisticated implementation may allow us to avoid the resource
* inefficiency.
*
*
If you do not need to interact with resource, use {@link #preload(RequestManager,
* RequestBuilder, Executor)}. {@code preload} is more efficient because it knows that the
* resource is never used and can always clear the resource immediately on cancellation, unlike
* this method.
*
*
An example usage:
*
*
{@code
* ListenableFuture future =
* submitAndExecute(
* requestManager,
* requestBuilder,
* (bitmap) -> doSomethingWithBitmap(bitmap),
* backgroundExecutor);
* ;
* }
*
* @param The type of resource that will be loaded (Bitmap, Drawable, etc).
*/
public static ListenableFuture submitAndExecute(
final RequestManager requestManager,
RequestBuilder requestBuilder,
final ResourceConsumer action,
Executor executor) {
// If the request completes normally, then the target is cleared and the resource is returned.
// If the request fails while loading the image, there's no need to clear.
// If the request fails while calling the action, the target is cleared and the resource is
// returned.
// If the request is cancelled before the resource is loaded, then the resource is returned
// If the request is cancelled after the resource is loaded but before the transform runs,
// then the resource is dropped (but not leaked)
// If the request is cancelled after the transform method starts, then the resource is returned.
return FluentFuture.from(submitInternal(requestBuilder))
.transform(
new Function, Void>() {
@Override
public Void apply(TargetAndResult targetAndResult) {
try {
action.act(targetAndResult.result);
} finally {
requestManager.clear(targetAndResult.target);
}
return null;
}
},
executor);
}
/**
* Convert a pending load request into a ListenableFuture.
*
* Sample code:
*
*
{@code
* ListenableFuture image =
* GlideFutures.submit(requestManager.asFile().load(url));
* }
*
* @param requestBuilder A request builder for the resource to load. It must be tied to an
* application Glide instance, and must not have a listener set.
* @param The type of resource that will be loaded (Bitmap, Drawable, etc).
*/
public static ListenableFuture submit(final RequestBuilder requestBuilder) {
return transformFromTargetAndResult(submitInternal(requestBuilder));
}
private static ListenableFuture transformFromTargetAndResult(
ListenableFuture> future) {
return Futures.transform(
future,
new Function, T>() {
@Override
public T apply(TargetAndResult input) {
return input.result;
}
},
Executors.directExecutor());
}
private static ListenableFuture> submitInternal(
final RequestBuilder requestBuilder) {
return CallbackToFutureAdapter.getFuture(
new Resolver>() {
// Only used for toString
@SuppressWarnings("FutureReturnValueIgnored")
@Override
public Object attachCompleter(@NonNull Completer> completer) {
GlideLoadingListener listener = new GlideLoadingListener<>(completer);
final FutureTarget futureTarget = requestBuilder.addListener(listener).submit();
completer.addCancellationListener(
new Runnable() {
@Override
public void run() {
futureTarget.cancel(/* mayInterruptIfRunning= */ true);
}
},
MoreExecutors.directExecutor());
return futureTarget;
}
});
}
/** Listener to convert Glide load results into ListenableFutures. */
private static final class GlideLoadingListener implements RequestListener {
private final Completer> completer;
GlideLoadingListener(Completer> completer) {
this.completer = completer;
}
@Override
public boolean onLoadFailed(
@Nullable GlideException e, Object model, @NonNull Target target, boolean isFirst) {
completer.setException(e != null ? e : new RuntimeException("Unknown error"));
return true;
}
@Override
public boolean onResourceReady(
@NonNull T resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirst) {
try {
completer.set(new TargetAndResult<>(target, resource));
} catch (Throwable t) {
completer.setException(t);
}
return true;
}
}
private static final class TargetAndResult {
private final Target target;
private final T result;
TargetAndResult(Target target, T result) {
this.target = target;
this.result = result;
}
}
private GlideFutures() {}
}