react-native-fast-image 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import android.app.Activity;
8
8
  import android.content.Context;
9
9
  import android.content.ContextWrapper;
10
10
  import android.graphics.PorterDuff;
11
+ import android.util.Log;
11
12
 
12
13
  import androidx.annotation.NonNull;
13
14
 
@@ -21,25 +22,53 @@ import com.facebook.react.common.MapBuilder;
21
22
  import com.facebook.react.uimanager.LayoutShadowNode;
22
23
  import com.facebook.react.uimanager.SimpleViewManager;
23
24
  import com.facebook.react.bridge.ReactContext;
25
+ import com.facebook.react.bridge.UiThreadUtil;
24
26
  import com.facebook.react.uimanager.ThemedReactContext;
25
27
  import com.facebook.react.uimanager.PixelUtil;
26
28
  import com.facebook.react.uimanager.PointerEvents;
27
29
  import com.facebook.react.uimanager.annotations.ReactProp;
28
30
  import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
29
31
 
32
+ import java.util.ArrayList;
33
+ import java.util.HashMap;
34
+ import java.util.LinkedHashSet;
30
35
  import java.util.List;
31
36
  import java.util.Locale;
32
37
  import java.util.Map;
33
- import java.util.WeakHashMap;
38
+ import java.util.Set;
39
+ import java.util.concurrent.atomic.AtomicLong;
34
40
 
35
41
  import javax.annotation.Nullable;
36
42
 
37
- class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> implements FastImageProgressListener {
43
+ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> {
38
44
 
39
45
  static final String REACT_CLASS = "FastImageView";
40
46
  static final String REACT_ON_LOAD_START_EVENT = "onFastImageLoadStart";
41
47
  static final String REACT_ON_PROGRESS_EVENT = "onFastImageProgress";
42
- private static final Map<String, List<FastImageViewWithUrl>> VIEWS_FOR_URLS = new WeakHashMap<>();
48
+ static final String LOG_TAG = "FastImage";
49
+ // Download progress, by the key downloads are shared by
50
+ // (FastImageSharedDownloads.key): the views that get it, those with an onProgress from
51
+ // their load's onLoadStart to its onLoad or onError, and the latest step a
52
+ // download reported, until the UI thread sends it. The UI thread adds and
53
+ // removes views, and downloads report steps on their own threads: only
54
+ // used under its lock.
55
+ private static final Map<String, Progress> PROGRESS = new HashMap<>();
56
+ // Numbers the steps downloads report, so a view can tell those reported
57
+ // before its load started (see FastImageViewWithUrl.takesProgress).
58
+ private static final AtomicLong REPORTED_STEPS = new AtomicLong();
59
+ // Whether any view gets progress (set under PROGRESS's lock), so downloads
60
+ // can skip reporting without taking any lock.
61
+ private static volatile boolean tracking = false;
62
+
63
+ private static final class Progress {
64
+ final Set<FastImageViewWithUrl> views = new LinkedHashSet<>();
65
+ // A step waits to be sent: later ones replace it meanwhile, so a url
66
+ // has at most one post to the UI thread at a time.
67
+ boolean posted;
68
+ long loaded;
69
+ long total;
70
+ long step;
71
+ }
43
72
 
44
73
  @NonNull
45
74
  @Override
@@ -143,10 +172,10 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
143
172
  view.setPaused(paused);
144
173
  }
145
174
 
146
- // Set when the image has an onProgress (see onProgress).
175
+ // Set when the image has an onProgress (see onDownloadProgress).
147
176
  @ReactProp(name = "trackProgress")
148
177
  public void setTrackProgress(FastImageViewWithUrl view, boolean trackProgress) {
149
- view.trackProgress = trackProgress;
178
+ view.setTrackProgress(trackProgress);
150
179
  }
151
180
 
152
181
  @ReactProp(name = "resizeMode")
@@ -160,12 +189,9 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
160
189
 
161
190
  @Override
162
191
  public void onDropViewInstance(@NonNull FastImageViewWithUrl view) {
163
- // This will cancel existing requests.
164
- view.clearView(view.requestManager);
165
-
166
- // Same key as when the view was tracked (toStringUrl, not toString,
167
- // which differ for urls that need escaping).
168
- view.untrackUrl(VIEWS_FOR_URLS);
192
+ // Cancels its request, and nothing loads or tracks progress for it
193
+ // any more (the progress map doesn't keep it, or its Activity).
194
+ view.drop();
169
195
 
170
196
  super.onDropViewInstance(view);
171
197
  }
@@ -181,26 +207,100 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
181
207
  .build();
182
208
  }
183
209
 
184
- // Sends the progress to the views loading the url that have an onProgress:
185
- // the others would send an event to JS for every chunk, for nothing (and
186
- // on the New Architecture each would log an unhandled event).
187
- @Override
188
- public void onProgress(String key, long bytesRead, long expectedLength) {
189
- List<FastImageViewWithUrl> viewsForKey = VIEWS_FOR_URLS.get(key);
190
- if (viewsForKey != null) {
191
- for (FastImageViewWithUrl view : viewsForKey) {
192
- if (!view.trackProgress) continue;
193
- WritableMap event = new WritableNativeMap();
194
- event.putInt("loaded", (int) bytesRead);
195
- event.putInt("total", (int) expectedLength);
196
- FastImageEvents.send(view, REACT_ON_PROGRESS_EVENT, event);
210
+ // On the UI thread (FastImageViewWithUrl's updateProgressTracking).
211
+ static void trackProgress(String key, FastImageViewWithUrl view) {
212
+ synchronized (PROGRESS) {
213
+ Progress progress = PROGRESS.get(key);
214
+ if (progress == null) {
215
+ progress = new Progress();
216
+ PROGRESS.put(key, progress);
197
217
  }
218
+ progress.views.add(view);
219
+ tracking = true;
198
220
  }
199
221
  }
200
222
 
201
- @Override
202
- public float getGranularityPercentage() {
203
- return 0.5f;
223
+ // On the UI thread. A key's entry goes once it has no views.
224
+ static void untrackProgress(String key, FastImageViewWithUrl view) {
225
+ synchronized (PROGRESS) {
226
+ Progress progress = PROGRESS.get(key);
227
+ if (progress == null) return;
228
+ progress.views.remove(view);
229
+ if (progress.views.isEmpty()) PROGRESS.remove(key);
230
+ tracking = !PROGRESS.isEmpty();
231
+ }
232
+ }
233
+
234
+ // Whether any view gets progress now (a hint: a view can start loading
235
+ // just after).
236
+ static boolean wantsProgress() {
237
+ return tracking;
238
+ }
239
+
240
+ // The latest step downloads reported (for a load that starts now).
241
+ static long progressStep() {
242
+ return REPORTED_STEPS.get();
243
+ }
244
+
245
+ // A download's progress (FastImageSharedDownloads), on its thread, which
246
+ // only notes it if a view wants it (so downloads of images without an
247
+ // onProgress post nothing). It's sent from the UI thread, where
248
+ // views start and end their loads: a view only gets it during its own load,
249
+ // not after its onLoad or once it loads again, and progress posted during
250
+ // a download comes before the onLoad Glide posts after it.
251
+ static void onDownloadProgress(final String key, long loaded, long total) {
252
+ synchronized (PROGRESS) {
253
+ Progress progress = PROGRESS.get(key);
254
+ if (progress == null) return;
255
+ // While a post is waiting, it keeps the furthest step: another
256
+ // download of the key may report one behind it meanwhile. A step
257
+ // kept keeps its number, so a load that started after it (e.g. a
258
+ // reload, whose download reports behind it) doesn't take it.
259
+ if (!progress.posted || (double) loaded / total >= (double) progress.loaded / progress.total) {
260
+ progress.loaded = loaded;
261
+ progress.total = total;
262
+ progress.step = REPORTED_STEPS.incrementAndGet();
263
+ }
264
+ if (progress.posted) return;
265
+ progress.posted = true;
266
+ }
267
+ UiThreadUtil.runOnUiThread(new Runnable() {
268
+ @Override
269
+ public void run() {
270
+ sendProgress(key);
271
+ }
272
+ });
273
+ }
274
+
275
+ private static void sendProgress(String key) {
276
+ List<FastImageViewWithUrl> views;
277
+ long loaded;
278
+ long total;
279
+ long step;
280
+ synchronized (PROGRESS) {
281
+ Progress progress = PROGRESS.get(key);
282
+ // Sent already, or no view wants it any more.
283
+ if (progress == null || !progress.posted) return;
284
+ progress.posted = false;
285
+ loaded = progress.loaded;
286
+ total = progress.total;
287
+ step = progress.step;
288
+ views = new ArrayList<>(progress.views);
289
+ }
290
+ for (FastImageViewWithUrl view : views) {
291
+ if (!view.takesProgress(step, loaded, total)) continue;
292
+ WritableMap event = new WritableNativeMap();
293
+ event.putInt("loaded", (int) loaded);
294
+ event.putInt("total", (int) total);
295
+ try {
296
+ FastImageEvents.send(view, REACT_ON_PROGRESS_EVENT, event);
297
+ } catch (RuntimeException e) {
298
+ // E.g. the view's React instance is gone: progress isn't worth
299
+ // crashing the app for.
300
+ // (Not the key: it has the request's headers.)
301
+ Log.w(LOG_TAG, "Couldn't send onProgress", e);
302
+ }
303
+ }
204
304
  }
205
305
 
206
306
  private static boolean isValidContextForGlide(final Context context) {
@@ -269,6 +369,6 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
269
369
  @Override
270
370
  protected void onAfterUpdateTransaction(@NonNull FastImageViewWithUrl view) {
271
371
  super.onAfterUpdateTransaction(view);
272
- view.onAfterUpdate(this, view.requestManager, VIEWS_FOR_URLS);
372
+ view.onAfterUpdate(this, view.requestManager);
273
373
  }
274
374
  }
@@ -225,8 +225,9 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
225
225
  }
226
226
  };
227
227
  // A remote image is downloaded to the disk cache first,
228
- // so views that load it meanwhile can wait for the file
229
- // (FastImageSharedDownloads), then decoded from there.
228
+ // then decoded from there. Views that load it meanwhile
229
+ // share the download (FastImageSharedDownloads), and those
230
+ // that get to downloading just after it ended read its file.
230
231
  final boolean download = !imageSource.isWebCache() && imageSource.isRemote();
231
232
  pendingPreloads.add(!download ? decode : new Runnable() {
232
233
  @Override
@@ -267,16 +268,18 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
267
268
 
268
269
  // Downloads the image into Glide's disk cache without decoding it (if it
269
270
  // isn't there), then calls back with its file or the error, on the UI
270
- // thread. For a preload (`shared`), views that load the image meanwhile
271
- // wait for the download and read its file (FastImageSharedDownloads).
271
+ // thread. Views that load the image meanwhile share its download
272
+ // (FastImageSharedDownloads); for a preload (`shared`), those whose
273
+ // request gets to downloading just after it finished read its file.
272
274
  private static void loadFile(Context context, Object model, RequestOptions options, boolean shared, final FileCallback callback) {
273
- final String key = shared && model instanceof FastImageUrl ? ((FastImageUrl) model).url.getCacheKey() : null;
275
+ final String key = shared && model instanceof FastImageUrl
276
+ ? FastImageSharedDownloads.key(((FastImageUrl) model).url)
277
+ : null;
274
278
  FastImageGlide.get(context);
275
279
  Glide.with(context)
276
280
  .asFile()
277
281
  .load(model)
278
282
  .apply(options)
279
- .set(FastImageSharedDownloads.PRELOAD, shared)
280
283
  .listener(new RequestListener<File>() {
281
284
  @Override
282
285
  public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
@@ -46,11 +46,6 @@ import com.facebook.react.bridge.WritableNativeMap;
46
46
  import com.facebook.react.uimanager.PointerEvents;
47
47
  import com.facebook.react.uimanager.ReactPointerEventsView;
48
48
 
49
- import java.util.ArrayList;
50
- import java.util.Collections;
51
- import java.util.List;
52
- import java.util.Map;
53
-
54
49
  import javax.annotation.Nonnull;
55
50
 
56
51
  class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEventsView {
@@ -58,7 +53,6 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
58
53
  private ReadableMap mSource = null;
59
54
  private Drawable mDefaultSource = null;
60
55
 
61
- public GlideUrl glideUrl;
62
56
  // Null when the view was created in a destroyed Activity (nothing loads).
63
57
  @Nullable
64
58
  final RequestManager requestManager;
@@ -108,8 +102,6 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
108
102
  // What the last update was called with, to load once the view has a size.
109
103
  @Nullable
110
104
  private FastImageViewManager mManager;
111
- @Nullable
112
- private Map<String, List<FastImageViewWithUrl>> mViewsForUrlsMap;
113
105
 
114
106
  public void setSources(@Nullable ReadableArray sources) {
115
107
  mNeedsReload = true;
@@ -163,7 +155,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
163
155
  post(new Runnable() {
164
156
  @Override
165
157
  public void run() {
166
- if (mNeedsReload && mManager != null) onAfterUpdate(mManager, requestManager, mViewsForUrlsMap);
158
+ if (mNeedsReload && mManager != null) onAfterUpdate(mManager, requestManager);
167
159
  }
168
160
  });
169
161
  return true;
@@ -214,8 +206,64 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
214
206
 
215
207
  // Pauses GIFs on the frame they're showing (the view's own animation).
216
208
  private boolean mPaused = false;
217
- // Whether to send progress events (the image has an onProgress).
218
- boolean trackProgress = false;
209
+ // Progress (see FastImageViewManager.onDownloadProgress): whether the image
210
+ // has an onProgress, the url its current load downloads (null for a local
211
+ // image, and once the load has ended), the progress key the view gets
212
+ // progress for (the loading url's, while it has an onProgress), the last
213
+ // step downloads reported before the load started, and the fraction the
214
+ // load has had.
215
+ private boolean mTrackProgress = false;
216
+ @Nullable
217
+ private GlideUrl mLoadingUrl;
218
+ @Nullable
219
+ private String mTrackedKey;
220
+ private long mProgressSince;
221
+ private double mProgressFraction;
222
+ // Dropped by React (FastImageViewManager.onDropViewInstance): reloads it
223
+ // posted to itself don't load anything, or track progress, any more.
224
+ private boolean mDropped = false;
225
+
226
+ void setTrackProgress(boolean trackProgress) {
227
+ mTrackProgress = trackProgress;
228
+ updateProgressTracking();
229
+ }
230
+
231
+ // The load ended (onLoad or onError), or the view no longer loads its
232
+ // source: no more progress, and the progress map no longer keeps the view
233
+ // (and its Activity) alive (#384).
234
+ void endProgress() {
235
+ mLoadingUrl = null;
236
+ updateProgressTracking();
237
+ }
238
+
239
+ void drop() {
240
+ mDropped = true;
241
+ clearView(requestManager);
242
+ endProgress();
243
+ }
244
+
245
+ // Whether a step a download reported is for the current load, and doesn't
246
+ // go back: one reported before it started is for an earlier load, e.g. of
247
+ // the same url before resizeMode changed, posted while that load still
248
+ // ran, and one below what the load has had is from another download of
249
+ // the key (e.g. one that started after the first had ended).
250
+ boolean takesProgress(long step, long loaded, long total) {
251
+ if (step <= mProgressSince) return false;
252
+ double fraction = (double) loaded / total;
253
+ if (fraction < mProgressFraction) return false;
254
+ mProgressFraction = fraction;
255
+ return true;
256
+ }
257
+
258
+ private void updateProgressTracking() {
259
+ String key = mTrackProgress && mLoadingUrl != null && !mDropped
260
+ ? FastImageSharedDownloads.key(mLoadingUrl)
261
+ : null;
262
+ if (key == null ? mTrackedKey == null : key.equals(mTrackedKey)) return;
263
+ if (mTrackedKey != null) FastImageViewManager.untrackProgress(mTrackedKey, this);
264
+ mTrackedKey = key;
265
+ if (key != null) FastImageViewManager.trackProgress(key, this);
266
+ }
219
267
 
220
268
  // The `transition` prop (from the next load): how long a loaded image
221
269
  // takes to fade in, in milliseconds (0 for no fade), whether it also fades
@@ -695,6 +743,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
695
743
  // The loading image loaded (FastImageRequestListener).
696
744
  void onImageLoaded() {
697
745
  mLoadEnded = true;
746
+ endProgress();
698
747
  mShownRequest = mLoadingRequest;
699
748
  mShownWidth = mLoadingWidth;
700
749
  mShownHeight = mLoadingHeight;
@@ -734,7 +783,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
734
783
  private boolean isResized() {
735
784
  int width = getWidth() - getPaddingLeft() - getPaddingRight();
736
785
  int height = getHeight() - getPaddingTop() - getPaddingBottom();
737
- return requestManager != null && !mNeedsReload
786
+ return requestManager != null && !mDropped && !mNeedsReload
738
787
  && mShownRequest != null && mShownRequest == mLoadingRequest
739
788
  && mShownWidth > 0 && mShownHeight > 0 && width > 0 && height > 0
740
789
  && (width != mShownWidth || height != mShownHeight);
@@ -752,7 +801,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
752
801
  clearView(requestManager);
753
802
  RequestBuilder<Drawable> builder = shown.clone()
754
803
  .error(current.clone())
755
- .listener(new FastImageRequestListener(null, null, true, false));
804
+ .listener(new FastImageRequestListener(null, true, false));
756
805
  if (meanwhile == null) builder = builder.thumbnail(current);
757
806
  into(shown, builder, meanwhile);
758
807
  }
@@ -776,7 +825,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
776
825
  clearView(requestManager);
777
826
  RequestBuilder<Drawable> builder = request.clone()
778
827
  .error(current.clone())
779
- .listener(new FastImageRequestListener(null, null, true, false));
828
+ .listener(new FastImageRequestListener(null, true, false));
780
829
  if (meanwhile == null) builder = builder.thumbnail(current);
781
830
  into(request, builder, meanwhile);
782
831
  } else if (mLoadingRequest != null && !mLoadEnded) {
@@ -848,6 +897,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
848
897
  // defaultSource then, but not over a thumbnail: show it here, as iOS does.
849
898
  void onImageFailed(boolean hadThumbnail) {
850
899
  mLoadEnded = true;
900
+ endProgress();
851
901
  mShownRequest = null;
852
902
  if (!hadThumbnail) return;
853
903
  final int load = mLoadCount;
@@ -863,10 +913,9 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
863
913
  @SuppressLint("CheckResult")
864
914
  public void onAfterUpdate(
865
915
  @Nonnull FastImageViewManager manager,
866
- @Nullable RequestManager requestManager,
867
- @Nonnull Map<String, List<FastImageViewWithUrl>> viewsForUrlsMap) {
916
+ @Nullable RequestManager requestManager) {
917
+ if (mDropped) return;
868
918
  mManager = manager;
869
- mViewsForUrlsMap = viewsForUrlsMap;
870
919
  if (mBlurChanged && !mNeedsReload) {
871
920
  mBlurChanged = false;
872
921
  reblur();
@@ -887,7 +936,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
887
936
  if (!mWaitsForSize) return;
888
937
  mWaitsForSize = false;
889
938
  mWaitedForSize = true;
890
- onAfterUpdate(manager, requestManager, viewsForUrlsMap);
939
+ onAfterUpdate(manager, requestManager);
891
940
  }
892
941
  });
893
942
  return;
@@ -917,7 +966,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
917
966
  // Cancel existing requests.
918
967
  clearView(requestManager);
919
968
 
920
- untrackUrl(viewsForUrlsMap);
969
+ endProgress();
921
970
 
922
971
  // Clear the image.
923
972
  setImageDrawable(null);
@@ -942,38 +991,33 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
942
991
  // Cancel existing requests.
943
992
  clearView(requestManager);
944
993
 
945
- untrackUrl(viewsForUrlsMap);
994
+ endProgress();
946
995
  setImageDrawable(mDefaultSource);
947
996
  return;
948
997
  }
949
998
 
950
- // `imageSource` may be null and we still continue, if `defaultSource` is not null
951
- final GlideUrl glideUrl = imageSource == null ? null : imageSource.getGlideUrl();
952
-
953
- String key = glideUrl == null ? null : glideUrl.toStringUrl();
954
-
955
- // Loading a different url: stop tracking the old one.
956
- if (this.glideUrl != null && !this.glideUrl.toStringUrl().equals(key)) {
957
- untrackUrl(viewsForUrlsMap);
958
- }
999
+ // `imageSource` may be null and we still continue, if `defaultSource` is not null.
1000
+ // What Glide loads: a FastImageUrl for a remote image.
1001
+ Object model = imageSource == null ? null : imageSource.getSourceForLoad();
959
1002
 
960
1003
  // Before the view clears (see shownCopy).
961
1004
  Drawable meanwhile = shownRequest != null && imageSource != null ? shownCopy(shownRequest) : null;
962
1005
 
963
1006
  // Cancel existing request.
964
- this.glideUrl = glideUrl;
965
1007
  clearView(requestManager);
966
1008
 
967
- if (glideUrl != null) {
968
- FastImageOkHttpProgressGlideModule.expect(key, manager);
969
- List<FastImageViewWithUrl> viewsForKey = viewsForUrlsMap.get(key);
970
- if (viewsForKey != null && !viewsForKey.contains(this)) {
971
- viewsForKey.add(this);
972
- } else if (viewsForKey == null) {
973
- List<FastImageViewWithUrl> newViewsForKeys = new ArrayList<>(Collections.singletonList(this));
974
- viewsForUrlsMap.put(key, newViewsForKeys);
975
- }
1009
+ // The url this load downloads, for its progress: a remote image's
1010
+ // (without Glide, in a destroyed Activity, nothing loads).
1011
+ mLoadingUrl = requestManager != null && model instanceof FastImageUrl
1012
+ ? ((FastImageUrl) model).url
1013
+ : null;
1014
+ // A restart (reblur) is the same load: its progress doesn't go back,
1015
+ // and steps already on their way to it still count.
1016
+ if (!restarting) {
1017
+ mProgressSince = FastImageViewManager.progressStep();
1018
+ mProgressFraction = 0;
976
1019
  }
1020
+ updateProgressTracking();
977
1021
 
978
1022
  if (imageSource != null && !restarting) {
979
1023
  // This is an orphan even without a load/loadend when only loading a placeholder
@@ -982,7 +1026,6 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
982
1026
 
983
1027
  if (requestManager != null) {
984
1028
  // Records the image's own size when Glide decodes it, for onLoad.
985
- Object model = imageSource == null ? null : imageSource.getSourceForLoad();
986
1029
  RequestBuilder<Drawable> base =
987
1030
  requestManager
988
1031
  // This will make this work for remote and local images. e.g.
@@ -1019,31 +1062,13 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1019
1062
  builder = builder.thumbnail(fromCache(shownRequest));
1020
1063
  }
1021
1064
 
1022
- if (key != null)
1023
- builder.listener(new FastImageRequestListener(key, imageSource, thumbnail, true));
1065
+ if (imageSource != null)
1066
+ builder.listener(new FastImageRequestListener(imageSource, thumbnail, true));
1024
1067
 
1025
1068
  into(request, builder, meanwhile);
1026
1069
  }
1027
1070
  }
1028
1071
 
1029
- // Removes this view from the list of views for its current url (used to send
1030
- // progress events), which otherwise kept it, and its Activity, alive after
1031
- // its source changed (#384). The url's progress listener is only forgotten
1032
- // when no other view uses it.
1033
- void untrackUrl(@Nonnull Map<String, List<FastImageViewWithUrl>> viewsForUrlsMap) {
1034
- if (glideUrl == null) return;
1035
- String key = glideUrl.toStringUrl();
1036
- List<FastImageViewWithUrl> viewsForKey = viewsForUrlsMap.get(key);
1037
- if (viewsForKey != null) {
1038
- viewsForKey.remove(this);
1039
- if (viewsForKey.isEmpty()) viewsForUrlsMap.remove(key);
1040
- }
1041
- if (viewsForKey == null || viewsForKey.isEmpty()) {
1042
- FastImageOkHttpProgressGlideModule.forget(key);
1043
- }
1044
- glideUrl = null;
1045
- }
1046
-
1047
1072
  public void clearView(@Nullable RequestManager requestManager) {
1048
1073
  // Glide keeps the view's request under its own tag id, not getTag().
1049
1074
  // Without a request this does nothing.
package/dist/index.d.ts CHANGED
@@ -464,7 +464,9 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
464
464
  onLoad?(event: OnLoadEvent): void;
465
465
  /**
466
466
  * Called on an image fetching error, with a message describing it (e.g.
467
- * the HTTP status code).
467
+ * the HTTP status code). On iOS and Android, a download that gets nothing
468
+ * from the server for 15 seconds fails too (on Android, unless the app's
469
+ * OkHttp client has timeouts of its own).
468
470
  *
469
471
  * @example
470
472
  * ```jsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "9.1.0",
3
+ "version": "9.2.0",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",