1 package com.bumptech.glide.load.engine; 2 3 import android.os.Handler; 4 import android.os.Message; 5 6 import com.bumptech.glide.load.Key; 7 import com.bumptech.glide.request.ResourceCallback; 8 import com.bumptech.glide.util.Util; 9 10 import java.util.ArrayList; 11 import java.util.HashSet; 12 import java.util.List; 13 import java.util.Set; 14 import java.util.concurrent.ExecutorService; 15 import java.util.concurrent.Future; 16 17 /** 18 * A class that manages a load by adding and removing callbacks for for the load and notifying callbacks when the 19 * load completes. 20 */ 21 class EngineJob implements EngineRunnable.EngineRunnableManager { 22 private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory(); 23 private static final Handler MAIN_THREAD_HANDLER = new Handler(new MainThreadCallback()); 24 25 private static final int MSG_COMPLETE = 1; 26 private static final int MSG_EXCEPTION = 2; 27 28 private final List<ResourceCallback> cbs = new ArrayList<ResourceCallback>(); 29 private final EngineResourceFactory engineResourceFactory; 30 private final EngineJobListener listener; 31 private final Key key; 32 private final ExecutorService diskCacheService; 33 private final ExecutorService sourceService; 34 private final boolean isCacheable; 35 36 private boolean isCancelled; 37 // Either resource or exception (particularly exception) may be returned to us null, so use booleans to track if 38 // we've received them instead of relying on them to be non-null. See issue #180. 39 private Resource<?> resource; 40 private boolean hasResource; 41 private Exception exception; 42 private boolean hasException; 43 // A set of callbacks that are removed while we're notifying other callbacks of a change in status. 44 private Set<ResourceCallback> ignoredCallbacks; 45 private EngineRunnable engineRunnable; 46 private EngineResource<?> engineResource; 47 48 private volatile Future<?> future; 49 EngineJob(Key key, ExecutorService diskCacheService, ExecutorService sourceService, boolean isCacheable, EngineJobListener listener)50 public EngineJob(Key key, ExecutorService diskCacheService, ExecutorService sourceService, boolean isCacheable, 51 EngineJobListener listener) { 52 this(key, diskCacheService, sourceService, isCacheable, listener, DEFAULT_FACTORY); 53 } 54 EngineJob(Key key, ExecutorService diskCacheService, ExecutorService sourceService, boolean isCacheable, EngineJobListener listener, EngineResourceFactory engineResourceFactory)55 public EngineJob(Key key, ExecutorService diskCacheService, ExecutorService sourceService, boolean isCacheable, 56 EngineJobListener listener, EngineResourceFactory engineResourceFactory) { 57 this.key = key; 58 this.diskCacheService = diskCacheService; 59 this.sourceService = sourceService; 60 this.isCacheable = isCacheable; 61 this.listener = listener; 62 this.engineResourceFactory = engineResourceFactory; 63 } 64 start(EngineRunnable engineRunnable)65 public void start(EngineRunnable engineRunnable) { 66 this.engineRunnable = engineRunnable; 67 future = diskCacheService.submit(engineRunnable); 68 } 69 70 @Override submitForSource(EngineRunnable runnable)71 public void submitForSource(EngineRunnable runnable) { 72 future = sourceService.submit(runnable); 73 } 74 addCallback(ResourceCallback cb)75 public void addCallback(ResourceCallback cb) { 76 Util.assertMainThread(); 77 if (hasResource) { 78 cb.onResourceReady(engineResource); 79 } else if (hasException) { 80 cb.onException(exception); 81 } else { 82 cbs.add(cb); 83 } 84 } 85 removeCallback(ResourceCallback cb)86 public void removeCallback(ResourceCallback cb) { 87 Util.assertMainThread(); 88 if (hasResource || hasException) { 89 addIgnoredCallback(cb); 90 } else { 91 cbs.remove(cb); 92 if (cbs.isEmpty()) { 93 cancel(); 94 } 95 } 96 } 97 98 // We cannot remove callbacks while notifying our list of callbacks directly because doing so would cause a 99 // ConcurrentModificationException. However, we need to obey the cancellation request such that if notifying a 100 // callback early in the callbacks list cancels a callback later in the request list, the cancellation for the later 101 // request is still obeyed. Using a set of ignored callbacks allows us to avoid the exception while still meeting 102 // the requirement. addIgnoredCallback(ResourceCallback cb)103 private void addIgnoredCallback(ResourceCallback cb) { 104 if (ignoredCallbacks == null) { 105 ignoredCallbacks = new HashSet<ResourceCallback>(); 106 } 107 ignoredCallbacks.add(cb); 108 } 109 isInIgnoredCallbacks(ResourceCallback cb)110 private boolean isInIgnoredCallbacks(ResourceCallback cb) { 111 return ignoredCallbacks != null && ignoredCallbacks.contains(cb); 112 } 113 114 // Exposed for testing. cancel()115 void cancel() { 116 if (hasException || hasResource || isCancelled) { 117 return; 118 } 119 engineRunnable.cancel(); 120 Future currentFuture = future; 121 if (currentFuture != null) { 122 currentFuture.cancel(true); 123 } 124 isCancelled = true; 125 listener.onEngineJobCancelled(this, key); 126 } 127 128 // Exposed for testing. isCancelled()129 boolean isCancelled() { 130 return isCancelled; 131 } 132 133 @Override onResourceReady(final Resource<?> resource)134 public void onResourceReady(final Resource<?> resource) { 135 this.resource = resource; 136 MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget(); 137 } 138 handleResultOnMainThread()139 private void handleResultOnMainThread() { 140 if (isCancelled) { 141 resource.recycle(); 142 return; 143 } else if (cbs.isEmpty()) { 144 throw new IllegalStateException("Received a resource without any callbacks to notify"); 145 } 146 engineResource = engineResourceFactory.build(resource, isCacheable); 147 hasResource = true; 148 149 // Hold on to resource for duration of request so we don't recycle it in the middle of notifying if it 150 // synchronously released by one of the callbacks. 151 engineResource.acquire(); 152 listener.onEngineJobComplete(key, engineResource); 153 154 for (ResourceCallback cb : cbs) { 155 if (!isInIgnoredCallbacks(cb)) { 156 engineResource.acquire(); 157 cb.onResourceReady(engineResource); 158 } 159 } 160 // Our request is complete, so we can release the resource. 161 engineResource.release(); 162 } 163 164 @Override onException(final Exception e)165 public void onException(final Exception e) { 166 this.exception = e; 167 MAIN_THREAD_HANDLER.obtainMessage(MSG_EXCEPTION, this).sendToTarget(); 168 } 169 handleExceptionOnMainThread()170 private void handleExceptionOnMainThread() { 171 if (isCancelled) { 172 return; 173 } else if (cbs.isEmpty()) { 174 throw new IllegalStateException("Received an exception without any callbacks to notify"); 175 } 176 hasException = true; 177 178 listener.onEngineJobComplete(key, null); 179 180 for (ResourceCallback cb : cbs) { 181 if (!isInIgnoredCallbacks(cb)) { 182 cb.onException(exception); 183 } 184 } 185 } 186 187 // Visible for testing. 188 static class EngineResourceFactory { build(Resource<R> resource, boolean isMemoryCacheable)189 public <R> EngineResource<R> build(Resource<R> resource, boolean isMemoryCacheable) { 190 return new EngineResource<R>(resource, isMemoryCacheable); 191 } 192 } 193 194 private static class MainThreadCallback implements Handler.Callback { 195 196 @Override handleMessage(Message message)197 public boolean handleMessage(Message message) { 198 if (MSG_COMPLETE == message.what || MSG_EXCEPTION == message.what) { 199 EngineJob job = (EngineJob) message.obj; 200 if (MSG_COMPLETE == message.what) { 201 job.handleResultOnMainThread(); 202 } else { 203 job.handleExceptionOnMainThread(); 204 } 205 return true; 206 } 207 208 return false; 209 } 210 } 211 } 212