Skip to content

Commit c38ce36

Browse files
committedAug 16, 2022
Notify RequestCoordinator before Targets/Listeners when loads finish.
This allows callers (ideally only Glide's framework) to reason about the state of the load based on the Request object. Prior to this change, the Request object is not updated for loads involving thumbnails or error request builders until after the Target/RequestListener is called. When RequestListeners/Targets check the state of the Request, it still has the previous state that doesn't match the callback the RequestListener/Target just received. PiperOrigin-RevId: 468013881
1 parent 00da167 commit c38ce36

File tree

3 files changed

+254
-4
lines changed

3 files changed

+254
-4
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package com.bumptech.glide;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
5+
import android.content.Context;
6+
import android.graphics.Bitmap;
7+
import android.graphics.Bitmap.CompressFormat;
8+
import android.graphics.Bitmap.Config;
9+
import android.graphics.Canvas;
10+
import android.graphics.Color;
11+
import android.graphics.drawable.Drawable;
12+
import androidx.annotation.NonNull;
13+
import androidx.annotation.Nullable;
14+
import androidx.test.core.app.ApplicationProvider;
15+
import androidx.test.ext.junit.runners.AndroidJUnit4;
16+
import com.bumptech.glide.load.DataSource;
17+
import com.bumptech.glide.load.engine.GlideException;
18+
import com.bumptech.glide.load.engine.executor.GlideExecutor;
19+
import com.bumptech.glide.request.Request;
20+
import com.bumptech.glide.request.RequestListener;
21+
import com.bumptech.glide.request.target.CustomTarget;
22+
import com.bumptech.glide.request.target.Target;
23+
import com.bumptech.glide.request.transition.Transition;
24+
import com.bumptech.glide.test.ModelGeneratorRule;
25+
import com.bumptech.glide.testutil.ConcurrencyHelper;
26+
import com.bumptech.glide.testutil.TearDownGlide;
27+
import java.io.BufferedOutputStream;
28+
import java.io.File;
29+
import java.io.FileOutputStream;
30+
import java.io.IOException;
31+
import java.io.OutputStream;
32+
import java.util.concurrent.CountDownLatch;
33+
import java.util.concurrent.TimeUnit;
34+
import java.util.concurrent.atomic.AtomicBoolean;
35+
import org.junit.Rule;
36+
import org.junit.Test;
37+
import org.junit.rules.TemporaryFolder;
38+
import org.junit.runner.RunWith;
39+
40+
@RunWith(AndroidJUnit4.class)
41+
public class MultiRequestTest {
42+
private final Context context = ApplicationProvider.getApplicationContext();
43+
@Rule public final TearDownGlide tearDownGlide = new TearDownGlide();
44+
@Rule public final ModelGeneratorRule modelGeneratorRule = new ModelGeneratorRule();
45+
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
46+
private final ConcurrencyHelper concurrency = new ConcurrencyHelper();
47+
48+
@Test
49+
public void thumbnail_onResourceReady_forPrimary_isComplete_whenRequestListenerIsCalled()
50+
throws IOException, InterruptedException {
51+
52+
// Make sure the requests complete in the same order
53+
Glide.init(
54+
context,
55+
new GlideBuilder()
56+
.setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(1).build()));
57+
58+
AtomicBoolean isPrimaryRequestComplete = new AtomicBoolean(false);
59+
CountDownLatch countDownLatch = new CountDownLatch(1);
60+
61+
RequestBuilder<Drawable> request =
62+
Glide.with(context)
63+
.load(newImageFile())
64+
.thumbnail(Glide.with(context).load(newImageFile()))
65+
.listener(
66+
new RequestListener<>() {
67+
@Override
68+
public boolean onLoadFailed(
69+
@Nullable GlideException e,
70+
Object model,
71+
Target<Drawable> target,
72+
boolean isFirstResource) {
73+
return false;
74+
}
75+
76+
@Override
77+
public boolean onResourceReady(
78+
Drawable resource,
79+
Object model,
80+
Target<Drawable> target,
81+
DataSource dataSource,
82+
boolean isFirstResource) {
83+
isPrimaryRequestComplete.set(target.getRequest().isComplete());
84+
countDownLatch.countDown();
85+
return false;
86+
}
87+
});
88+
concurrency.runOnMainThread(() -> request.into(new DoNothingTarget()));
89+
90+
assertThat(countDownLatch.await(3, TimeUnit.SECONDS)).isTrue();
91+
assertThat(isPrimaryRequestComplete.get()).isTrue();
92+
}
93+
94+
@Test
95+
public void thumbnail_onLoadFailed_forPrimary_isNotRunningOrComplete_whenRequestListenerIsCalled()
96+
throws IOException, InterruptedException {
97+
98+
// Make sure the requests complete in the same order
99+
Glide.init(
100+
context,
101+
new GlideBuilder()
102+
.setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(1).build()));
103+
104+
AtomicBoolean isNeitherRunningNorComplete = new AtomicBoolean(false);
105+
CountDownLatch countDownLatch = new CountDownLatch(1);
106+
107+
int missingResourceId = 123;
108+
RequestBuilder<Drawable> requestBuilder =
109+
Glide.with(context)
110+
.load(missingResourceId)
111+
.thumbnail(Glide.with(context).load(newImageFile()))
112+
.listener(
113+
new RequestListener<>() {
114+
@Override
115+
public boolean onLoadFailed(
116+
@Nullable GlideException e,
117+
Object model,
118+
Target<Drawable> target,
119+
boolean isFirstResource) {
120+
Request request = target.getRequest();
121+
isNeitherRunningNorComplete.set(!request.isComplete() && !request.isRunning());
122+
countDownLatch.countDown();
123+
return false;
124+
}
125+
126+
@Override
127+
public boolean onResourceReady(
128+
Drawable resource,
129+
Object model,
130+
Target<Drawable> target,
131+
DataSource dataSource,
132+
boolean isFirstResource) {
133+
return false;
134+
}
135+
});
136+
concurrency.runOnMainThread(() -> requestBuilder.into(new DoNothingTarget()));
137+
138+
assertThat(countDownLatch.await(3, TimeUnit.SECONDS)).isTrue();
139+
assertThat(isNeitherRunningNorComplete.get()).isTrue();
140+
}
141+
142+
private File newImageFile() throws IOException {
143+
Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
144+
Canvas canvas = new Canvas(bitmap);
145+
canvas.drawColor(Color.RED);
146+
File result = temporaryFolder.newFile();
147+
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(result))) {
148+
bitmap.compress(CompressFormat.JPEG, 75, os);
149+
}
150+
return result;
151+
}
152+
153+
// We don't store or do anything with the resource, so we don't need to do anything to release it
154+
// in onLoadCleared.
155+
private static final class DoNothingTarget extends CustomTarget<Drawable> {
156+
@Override
157+
public void onResourceReady(
158+
@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {}
159+
160+
@Override
161+
public void onLoadCleared(@Nullable Drawable placeholder) {}
162+
}
163+
}

‎library/src/main/java/com/bumptech/glide/request/SingleRequest.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -523,14 +523,14 @@ private boolean isFirstReadyResource() {
523523
}
524524

525525
@GuardedBy("requestLock")
526-
private void notifyLoadSuccess() {
526+
private void notifyRequestCoordinatorLoadSucceeded() {
527527
if (requestCoordinator != null) {
528528
requestCoordinator.onRequestSuccess(this);
529529
}
530530
}
531531

532532
@GuardedBy("requestLock")
533-
private void notifyLoadFailed() {
533+
private void notifyRequestCoordinatorLoadFailed() {
534534
if (requestCoordinator != null) {
535535
requestCoordinator.onRequestFailed(this);
536536
}
@@ -639,6 +639,8 @@ private void onResourceReady(
639639
+ " ms");
640640
}
641641

642+
notifyRequestCoordinatorLoadSucceeded();
643+
642644
isCallingCallbacks = true;
643645
try {
644646
boolean anyListenerHandledUpdatingTarget = false;
@@ -660,7 +662,6 @@ private void onResourceReady(
660662
isCallingCallbacks = false;
661663
}
662664

663-
notifyLoadSuccess();
664665
GlideTrace.endSectionAsync(TAG, cookie);
665666
}
666667

@@ -694,6 +695,8 @@ private void onLoadFailed(GlideException e, int maxLogLevel) {
694695
loadStatus = null;
695696
status = Status.FAILED;
696697

698+
notifyRequestCoordinatorLoadFailed();
699+
697700
isCallingCallbacks = true;
698701
try {
699702
// TODO: what if this is a thumbnail request?
@@ -715,7 +718,6 @@ private void onLoadFailed(GlideException e, int maxLogLevel) {
715718
isCallingCallbacks = false;
716719
}
717720

718-
notifyLoadFailed();
719721
GlideTrace.endSectionAsync(TAG, cookie);
720722
}
721723
}

‎library/test/src/test/java/com/bumptech/glide/request/SingleRequestTest.java

+85
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.bumptech.glide.load.engine.Engine;
3434
import com.bumptech.glide.load.engine.GlideException;
3535
import com.bumptech.glide.load.engine.Resource;
36+
import com.bumptech.glide.request.target.CustomTarget;
3637
import com.bumptech.glide.request.target.SizeReadyCallback;
3738
import com.bumptech.glide.request.target.Target;
3839
import com.bumptech.glide.request.transition.Transition;
@@ -46,6 +47,7 @@
4647
import java.util.List;
4748
import java.util.Map;
4849
import java.util.concurrent.Executor;
50+
import java.util.concurrent.atomic.AtomicBoolean;
4951
import org.junit.Before;
5052
import org.junit.Test;
5153
import org.junit.runner.RunWith;
@@ -649,6 +651,89 @@ public void testRequestListenerIsCalledWithFirstImageIfRequestCoordinatorReturns
649651
eq(builder.result), any(Number.class), isAListTarget(), isADataSource(), eq(false));
650652
}
651653

654+
@Test
655+
public void onResourceReady_notifiesRequestCoordinator_beforeCallingRequestListeners() {
656+
AtomicBoolean isRequestCoordinatorVerified = new AtomicBoolean();
657+
SingleRequest<List> request =
658+
builder
659+
.setTarget(new DoNothingTarget())
660+
.addRequestListener(
661+
new RequestListener<>() {
662+
@Override
663+
public boolean onLoadFailed(
664+
@Nullable GlideException e,
665+
Object model,
666+
Target<List> target,
667+
boolean isFirstResource) {
668+
return false;
669+
}
670+
671+
@Override
672+
public boolean onResourceReady(
673+
List resource,
674+
Object model,
675+
Target<List> target,
676+
DataSource dataSource,
677+
boolean isFirstResource) {
678+
verify(builder.requestCoordinator).onRequestSuccess(target.getRequest());
679+
isRequestCoordinatorVerified.set(true);
680+
return false;
681+
}
682+
})
683+
.build();
684+
builder.target.setRequest(request);
685+
request.onResourceReady(
686+
builder.resource, DataSource.DATA_DISK_CACHE, /* isLoadedFromAlternateCacheKey= */ false);
687+
688+
assertThat(isRequestCoordinatorVerified.get()).isTrue();
689+
}
690+
691+
@Test
692+
public void onLoadFailed_notifiesRequestCoordinator_beforeCallingRequestListeners() {
693+
AtomicBoolean isRequestCoordinatorVerified = new AtomicBoolean();
694+
SingleRequest<List> request =
695+
builder
696+
.setTarget(new DoNothingTarget())
697+
.addRequestListener(
698+
new RequestListener<>() {
699+
@Override
700+
public boolean onLoadFailed(
701+
@Nullable GlideException e,
702+
Object model,
703+
Target<List> target,
704+
boolean isFirstResource) {
705+
verify(builder.requestCoordinator).onRequestFailed(target.getRequest());
706+
isRequestCoordinatorVerified.set(true);
707+
return false;
708+
}
709+
710+
@Override
711+
public boolean onResourceReady(
712+
List resource,
713+
Object model,
714+
Target<List> target,
715+
DataSource dataSource,
716+
boolean isFirstResource) {
717+
return false;
718+
}
719+
})
720+
.build();
721+
builder.target.setRequest(request);
722+
request.onLoadFailed(new GlideException("test"));
723+
724+
assertThat(isRequestCoordinatorVerified.get()).isTrue();
725+
}
726+
727+
// We don't need to clear a resource since we're not using it to being with.
728+
private static final class DoNothingTarget extends CustomTarget<List> {
729+
@Override
730+
public void onResourceReady(
731+
@NonNull List resource, @Nullable Transition<? super List> transition) {}
732+
733+
@Override
734+
public void onLoadCleared(@Nullable Drawable placeholder) {}
735+
}
736+
652737
@Test
653738
public void
654739
testRequestListenerIsCalledWithNotIsFirstRequestIfRequestCoordinatorParentReturnsResourceSet() {

0 commit comments

Comments
 (0)
Please sign in to comment.