Skip to content

Commit eaca27b

Browse files
author
Hanno J. Gödecke
authored
fix(android): etag caching (#7)
* trying to log network traffic with proxyman * intercepting traffic * make a head request first, and work with signature correctly * cleanup + doc
1 parent 11b82dd commit eaca27b

File tree

7 files changed

+135
-133
lines changed

7 files changed

+135
-133
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ coverage
4949

5050
# TypeScript incremental compilation cache
5151
*.tsbuildinfo
52+
53+
ReactNativeFastImageExampleServer/catswap-cdn.sh

ReactNativeFastImageExample/android/app/src/debug/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<application
88
android:usesCleartextTraffic="true"
99
tools:targetApi="28"
10+
android:networkSecurityConfig="@xml/network_security_config"
1011
tools:ignore="GoogleAppIndexingWarning">
1112
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
1213
</application>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<network-security-config>
2+
<debug-overrides>
3+
<trust-anchors>
4+
<!-- Trust user added CAs while debuggable only -->
5+
<certificates src="user" />
6+
<certificates src="system" />
7+
</trust-anchors>
8+
</debug-overrides>
9+
10+
<base-config cleartextTrafficPermitted="true">
11+
<trust-anchors>
12+
<certificates src="system" />
13+
</trust-anchors>
14+
</base-config>
15+
16+
<domain-config>
17+
<!-- Make sure your URL Server here -->
18+
<domain includeSubdomains="true">s3.nl-ams.scw.cloud</domain>
19+
<trust-anchors>
20+
<certificates src="user"/>
21+
<certificates src="system"/>
22+
</trust-anchors>
23+
</domain-config>
24+
</network-security-config>

android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java

Lines changed: 74 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import com.bumptech.glide.signature.ObjectKey;
2020
import com.dylanvann.fastimage.custom.EtagCallback;
2121
import com.dylanvann.fastimage.custom.EtagRequester;
22-
import com.dylanvann.fastimage.custom.PersistEtagCallbackWrapper;
2322
import com.dylanvann.fastimage.custom.persistence.ObjectBox;
2423
import com.facebook.react.bridge.ReadableArray;
2524
import com.facebook.react.bridge.ReadableMap;
@@ -86,7 +85,6 @@ public void setSrc(FastImageViewWithUrl view, @Nullable ReadableMap source) {
8685
return;
8786
}
8887

89-
//final GlideUrl glideUrl = FastImageViewConverter.getGlideUrl(view.getContext(), source);
9088
final FastImageSource imageSource = FastImageViewConverter.getImageSource(view.getContext(), source);
9189
if (imageSource.getUri().toString().length() == 0) {
9290
ThemedReactContext context = (ThemedReactContext) view.getContext();
@@ -117,11 +115,7 @@ private void load(final FastImageViewWithUrl view, @NonNull final ReadableMap so
117115
final FastImageSource imageSource = FastImageViewConverter.getImageSource(view.getContext(), source);
118116
final GlideUrl glideUrl = imageSource.getGlideUrl();
119117

120-
// Cancel existing request.
121118
view.glideUrl = glideUrl;
122-
// if (requestManager != null) {
123-
// requestManager.clear(view);
124-
// }
125119

126120
final String key = glideUrl.toStringUrl();
127121
FastImageOkHttpProgressGlideModule.expect(key, this);
@@ -138,6 +132,7 @@ private void load(final FastImageViewWithUrl view, @NonNull final ReadableMap so
138132
final int viewId = view.getId();
139133
eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_START_EVENT, new WritableNativeMap());
140134

135+
final RequestOptions options = FastImageViewConverter.getOptions(context, imageSource, source);
141136
if (requestManager != null) {
142137
final String url = imageSource.getGlideUrl().toStringUrl();
143138
if (!url.startsWith("http")) {
@@ -149,118 +144,102 @@ private void load(final FastImageViewWithUrl view, @NonNull final ReadableMap so
149144
// - android.resource://
150145
// - data:image/png;base64
151146
.load(imageSource.getSourceForLoad())
152-
.apply(FastImageViewConverter.getOptions(context, imageSource, source))
147+
.apply(options)
153148
.listener(new FastImageRequestListener(key))
154149
.into(view);
155150
return;
156151
}
157152

158-
final RequestOptions options = FastImageViewConverter.getOptions(context, imageSource, source);
159-
160-
// getEtag handles persistence of etag
161-
getEtag(url, new EtagCallback() {
162-
@Override
163-
public void onError(String error) {
164-
loadImage(view, url, null, options, key);
165-
}
166-
167-
@Override
168-
public void onEtag(@Nullable final String etag) {
169-
loadImage(view, url, etag, options, key);
170-
}
171-
});
153+
loadImage(view, url, options, key);
172154
}
173155
}
174156

175157
/**
176-
* Refreshes an image. Won't do anything if etag hasn't changed.
177-
* When there was a new image the new image will be shown + the image
178-
* and etag cache will be updated.
179-
* @param url
180-
*/
181-
private void refresh(final FastImageViewWithUrl view, final String url) {
182-
EtagRequester.requestEtag(url, new PersistEtagCallbackWrapper(url, new EtagCallback() {
183-
@Override
184-
public void onError(final String error) {
185-
loadImage(view, url, null, null, null);
186-
}
187-
188-
@Override
189-
public void onEtag(final String etag) {
190-
loadImage(view, url, etag, null, null);
191-
}
192-
})
193-
);
158+
* This will make a head request to the URL to get the ETAG.
159+
* The request is then forwarded to glide, which uses the
160+
* ETAG as signature, see {@link #loadImageWithSignature}.
161+
**/
162+
private void loadImage(final FastImageViewWithUrl view, final String url, @Nullable final RequestOptions options, final @Nullable String key) {
163+
String prevEtag = ObjectBox.getEtagByUrl(url);
164+
165+
// We need to make a head request to the URL with the ETAG attached.
166+
// - When we get a new etag Glide will send out another request
167+
// - If the signature (etag) doesn't change, Glide won't bother sending out a request
168+
EtagRequester.requestEtag(url, prevEtag, new EtagCallback() {
169+
@Override
170+
public void onEtag(String etag) {
171+
// Note: here at this point the etag in the ObjectBox has been updated
172+
// to the new etag. That's why we pass down the the previous.
173+
loadImageWithSignature(view, url, etag, prevEtag, options, key);
174+
}
175+
176+
@Override
177+
public void onError(String error) {
178+
loadImageWithSignature(view, url, prevEtag, prevEtag, options, key);
179+
}
180+
});
194181
}
195182

196183
/**
197-
*
198-
* @param view
199-
* @param url
200-
* @param etag Optional
201-
* @param options Optional
202-
* @param key Optional, when set it will emit events
184+
* This loads the actual image either from server or from cache
185+
* depending on whether a cache entry for the given signature
186+
* exists yet.
187+
* If a prev signature is passed it will show the image for the
188+
* url + prevSignature as long as the new image from the url (with
189+
* the new signature) is being loaded.
203190
*/
204-
private void loadImage(final FastImageViewWithUrl view, final String url, @Nullable final String etag, @Nullable final RequestOptions options, final @Nullable String key) {
205-
getActivityFromContext(view.getContext()).runOnUiThread(new Runnable() {
206-
@Override
207-
public void run() {
208-
if (requestManager == null) {
209-
Log.e(FastImageViewManager.class.getSimpleName(), "Can't refresh as requestManager was null!");
210-
return;
211-
}
191+
private void loadImageWithSignature(
192+
final FastImageViewWithUrl view,
193+
final String url,
194+
@Nullable String signature,
195+
@Nullable String prevSignature,
196+
@Nullable final RequestOptions options,
197+
@Nullable final String key)
198+
{
199+
getActivityFromContext(view.getContext()).runOnUiThread(() -> {
200+
if (requestManager == null) {
201+
Log.e(FastImageViewManager.class.getSimpleName(), "Can't refresh as requestManager was null!");
202+
return;
203+
}
204+
205+
// cancel any previous requests
206+
requestManager.clear(view);
212207

208+
// Request the new image
209+
RequestBuilder<Drawable> imageRequest = requestManager
210+
.load(url);
213211

212+
// When we have a previous signature we want to show the previous
213+
// image, until the new one is loaded. This is done with a
214+
// thumbnail request. Without this there would be a "white flickering"
215+
// until the (new) image is loaded.
216+
if (prevSignature != null) {
217+
// Create a "thumbnail" which is literally the cached image while we load the new image
214218
RequestBuilder<Drawable> thumbnailRequest = requestManager
215219
.load(url);
216-
217-
// add etag as signature when its set
218-
String prevEtag = ObjectBox.getEtagByUrl(url);
219-
if (prevEtag != null) {
220-
thumbnailRequest = thumbnailRequest.signature(new ObjectKey(prevEtag));
221-
}
222-
223-
RequestBuilder<Drawable> imageRequest = requestManager
224-
.load(url)
220+
thumbnailRequest = thumbnailRequest.signature(new ObjectKey(prevSignature));
221+
imageRequest = imageRequest
225222
.thumbnail(thumbnailRequest)
226223
.skipMemoryCache(true);
224+
}
227225

228-
if (etag != null) {
229-
imageRequest = imageRequest.signature(new ObjectKey(etag));
230-
} else if (prevEtag != null) {
231-
// in case we received no etag (e.g. loading error due to no network)
232-
// we still want to consider getting cache with the prev etag.
233-
imageRequest = imageRequest.signature(new ObjectKey(prevEtag));
234-
}
235-
236-
if (options != null) {
237-
imageRequest = imageRequest.apply(options);
238-
}
239-
240-
if (key != null) {
241-
imageRequest = imageRequest.listener(new FastImageRequestListener(key));
242-
}
243-
244-
// finally, load the image
245-
imageRequest.into(view);
226+
if (signature != null) {
227+
imageRequest = imageRequest.signature(new ObjectKey(signature));
246228
}
229+
if (options != null) {
230+
imageRequest = imageRequest.apply(options);
231+
}
232+
if (key != null) {
233+
imageRequest = imageRequest.listener(new FastImageRequestListener(key));
234+
}
235+
236+
// finally, load the image
237+
imageRequest.into(view);
247238
});
248239
}
249240

250-
/**
251-
* Returns the etag from cache. If there is no cached etag it will request
252-
* the server to get it, save it to the cache, and return it.
253-
* @param url
254-
* @param callback
255-
*/
256-
private void getEtag(String url, EtagCallback callback) {
257-
String etag = ObjectBox.getEtagByUrl(url);
258-
259-
if (etag == null) {
260-
EtagRequester.requestEtag(url, new PersistEtagCallbackWrapper(url, callback));
261-
} else {
262-
callback.onEtag(etag);
263-
}
241+
private void refresh(final FastImageViewWithUrl view, final ReadableMap source) {
242+
load(view, source);
264243
}
265244

266245
@ReactProp(name = "tintColor", customType = "Color")
@@ -389,8 +368,7 @@ public void receiveCommand(FastImageViewWithUrl root, int commandId, @Nullable R
389368
switch (commandId) {
390369
case FORCE_REFRESH_IMAGE: {
391370
if (root.source != null) {
392-
final FastImageSource imageSource = FastImageViewConverter.getImageSource(root.getContext(), root.source);
393-
refresh(root, imageSource.getGlideUrl().toStringUrl());
371+
refresh(root, root.source);
394372
}
395373
return;
396374
}

android/src/main/java/com/dylanvann/fastimage/custom/EtagRequester.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package com.dylanvann.fastimage.custom;
22

3+
import androidx.annotation.NonNull;
4+
35
import com.dylanvann.fastimage.custom.persistence.ObjectBox;
46

57
import java.io.IOException;
68

9+
import javax.annotation.Nullable;
10+
711
import okhttp3.Call;
812
import okhttp3.Callback;
913
import okhttp3.OkHttpClient;
@@ -17,12 +21,11 @@ public class EtagRequester {
1721
* @param url
1822
* @param callback
1923
*/
20-
public static void requestEtag(final String url, final EtagCallback callback) {
21-
final String prevEtag = ObjectBox.getEtagByUrl(url);
22-
24+
public static void requestEtag(@NonNull final String url, @Nullable final String prevEtag, @NonNull final EtagCallback callback) {
2325
OkHttpClient client = SharedOkHttpClient.getInstance(null).getClient();
2426
Request.Builder request = new Request.Builder()
25-
.url(url);
27+
.url(url)
28+
.head();
2629

2730
if (prevEtag != null) {
2831
request.addHeader("If-None-Match", prevEtag);

android/src/main/java/com/dylanvann/fastimage/custom/PersistEtagCallbackWrapper.java

Lines changed: 0 additions & 33 deletions
This file was deleted.

android/src/main/java/com/dylanvann/fastimage/custom/SharedOkHttpClient.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,49 @@
11
package com.dylanvann.fastimage.custom;
22

33
import android.content.Context;
4+
import android.util.Log;
5+
6+
import com.dylanvann.fastimage.custom.persistence.ObjectBox;
47

58
import java.io.File;
69

710
import okhttp3.Cache;
811
import okhttp3.OkHttpClient;
12+
import okhttp3.Request;
13+
import okhttp3.Response;
914

1015
public class SharedOkHttpClient {
1116
private static SharedOkHttpClient instance;
1217

1318
private final okhttp3.OkHttpClient client;
19+
1420
private SharedOkHttpClient(Context context) {
1521
this.client = new okhttp3.OkHttpClient.Builder()
1622
.cache(new Cache(
1723
new File(context.getCacheDir(), "http_cache"),
1824
50L * 1024L * 1024L // 50 MiB
1925
))
26+
// Add an interceptor that will keep our etag up2date
27+
.addInterceptor(chain -> {
28+
Request request = chain.request();
29+
30+
// Note: we don't add the the etag to the request
31+
// Cache invalidation is handled on Glides signature level
32+
// (See FastImageViewManager)
33+
Response response = chain.proceed(request);
34+
35+
String url = request.url().toString();
36+
String prevEtag = ObjectBox.getEtagByUrl(url);
37+
38+
39+
// update etag if changes
40+
String responseEtag = response.header("etag");
41+
if (responseEtag != null && !responseEtag.equals(prevEtag)) {
42+
ObjectBox.putOrUpdateEtag(url, responseEtag);
43+
}
44+
45+
return response;
46+
})
2047
.build();
2148
}
2249

0 commit comments

Comments
 (0)