| package com.bumptech.glide.load.engine; |
| |
| import android.os.Handler; |
| import android.os.Message; |
| |
| import com.bumptech.glide.load.Key; |
| import com.bumptech.glide.request.ResourceCallback; |
| import com.bumptech.glide.util.Util; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Future; |
| |
| /** |
| * A class that manages a load by adding and removing callbacks for for the load and notifying callbacks when the |
| * load completes. |
| */ |
| class EngineJob implements EngineRunnable.EngineRunnableManager { |
| private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory(); |
| private static final Handler MAIN_THREAD_HANDLER = new Handler(new MainThreadCallback()); |
| |
| private static final int MSG_COMPLETE = 1; |
| private static final int MSG_EXCEPTION = 2; |
| |
| private final List<ResourceCallback> cbs = new ArrayList<ResourceCallback>(); |
| private final EngineResourceFactory engineResourceFactory; |
| private final EngineJobListener listener; |
| private final Key key; |
| private final ExecutorService diskCacheService; |
| private final ExecutorService sourceService; |
| private final boolean isCacheable; |
| |
| private boolean isCancelled; |
| // Either resource or exception (particularly exception) may be returned to us null, so use booleans to track if |
| // we've received them instead of relying on them to be non-null. See issue #180. |
| private Resource<?> resource; |
| private boolean hasResource; |
| private Exception exception; |
| private boolean hasException; |
| // A set of callbacks that are removed while we're notifying other callbacks of a change in status. |
| private Set<ResourceCallback> ignoredCallbacks; |
| private EngineRunnable engineRunnable; |
| private EngineResource<?> engineResource; |
| |
| private volatile Future<?> future; |
| |
| public EngineJob(Key key, ExecutorService diskCacheService, ExecutorService sourceService, boolean isCacheable, |
| EngineJobListener listener) { |
| this(key, diskCacheService, sourceService, isCacheable, listener, DEFAULT_FACTORY); |
| } |
| |
| public EngineJob(Key key, ExecutorService diskCacheService, ExecutorService sourceService, boolean isCacheable, |
| EngineJobListener listener, EngineResourceFactory engineResourceFactory) { |
| this.key = key; |
| this.diskCacheService = diskCacheService; |
| this.sourceService = sourceService; |
| this.isCacheable = isCacheable; |
| this.listener = listener; |
| this.engineResourceFactory = engineResourceFactory; |
| } |
| |
| public void start(EngineRunnable engineRunnable) { |
| this.engineRunnable = engineRunnable; |
| future = diskCacheService.submit(engineRunnable); |
| } |
| |
| @Override |
| public void submitForSource(EngineRunnable runnable) { |
| future = sourceService.submit(runnable); |
| } |
| |
| public void addCallback(ResourceCallback cb) { |
| Util.assertMainThread(); |
| if (hasResource) { |
| cb.onResourceReady(engineResource); |
| } else if (hasException) { |
| cb.onException(exception); |
| } else { |
| cbs.add(cb); |
| } |
| } |
| |
| public void removeCallback(ResourceCallback cb) { |
| Util.assertMainThread(); |
| if (hasResource || hasException) { |
| addIgnoredCallback(cb); |
| } else { |
| cbs.remove(cb); |
| if (cbs.isEmpty()) { |
| cancel(); |
| } |
| } |
| } |
| |
| // We cannot remove callbacks while notifying our list of callbacks directly because doing so would cause a |
| // ConcurrentModificationException. However, we need to obey the cancellation request such that if notifying a |
| // callback early in the callbacks list cancels a callback later in the request list, the cancellation for the later |
| // request is still obeyed. Using a set of ignored callbacks allows us to avoid the exception while still meeting |
| // the requirement. |
| private void addIgnoredCallback(ResourceCallback cb) { |
| if (ignoredCallbacks == null) { |
| ignoredCallbacks = new HashSet<ResourceCallback>(); |
| } |
| ignoredCallbacks.add(cb); |
| } |
| |
| private boolean isInIgnoredCallbacks(ResourceCallback cb) { |
| return ignoredCallbacks != null && ignoredCallbacks.contains(cb); |
| } |
| |
| // Exposed for testing. |
| void cancel() { |
| if (hasException || hasResource || isCancelled) { |
| return; |
| } |
| engineRunnable.cancel(); |
| Future currentFuture = future; |
| if (currentFuture != null) { |
| currentFuture.cancel(true); |
| } |
| isCancelled = true; |
| listener.onEngineJobCancelled(this, key); |
| } |
| |
| // Exposed for testing. |
| boolean isCancelled() { |
| return isCancelled; |
| } |
| |
| @Override |
| public void onResourceReady(final Resource<?> resource) { |
| this.resource = resource; |
| MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget(); |
| } |
| |
| private void handleResultOnMainThread() { |
| if (isCancelled) { |
| resource.recycle(); |
| return; |
| } else if (cbs.isEmpty()) { |
| throw new IllegalStateException("Received a resource without any callbacks to notify"); |
| } |
| engineResource = engineResourceFactory.build(resource, isCacheable); |
| hasResource = true; |
| |
| // Hold on to resource for duration of request so we don't recycle it in the middle of notifying if it |
| // synchronously released by one of the callbacks. |
| engineResource.acquire(); |
| listener.onEngineJobComplete(key, engineResource); |
| |
| for (ResourceCallback cb : cbs) { |
| if (!isInIgnoredCallbacks(cb)) { |
| engineResource.acquire(); |
| cb.onResourceReady(engineResource); |
| } |
| } |
| // Our request is complete, so we can release the resource. |
| engineResource.release(); |
| } |
| |
| @Override |
| public void onException(final Exception e) { |
| this.exception = e; |
| MAIN_THREAD_HANDLER.obtainMessage(MSG_EXCEPTION, this).sendToTarget(); |
| } |
| |
| private void handleExceptionOnMainThread() { |
| if (isCancelled) { |
| return; |
| } else if (cbs.isEmpty()) { |
| throw new IllegalStateException("Received an exception without any callbacks to notify"); |
| } |
| hasException = true; |
| |
| listener.onEngineJobComplete(key, null); |
| |
| for (ResourceCallback cb : cbs) { |
| if (!isInIgnoredCallbacks(cb)) { |
| cb.onException(exception); |
| } |
| } |
| } |
| |
| // Visible for testing. |
| static class EngineResourceFactory { |
| public <R> EngineResource<R> build(Resource<R> resource, boolean isMemoryCacheable) { |
| return new EngineResource<R>(resource, isMemoryCacheable); |
| } |
| } |
| |
| private static class MainThreadCallback implements Handler.Callback { |
| |
| @Override |
| public boolean handleMessage(Message message) { |
| if (MSG_COMPLETE == message.what || MSG_EXCEPTION == message.what) { |
| EngineJob job = (EngineJob) message.obj; |
| if (MSG_COMPLETE == message.what) { |
| job.handleResultOnMainThread(); |
| } else { |
| job.handleExceptionOnMainThread(); |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| } |
| } |