react-native-fast-image 8.29.0 → 8.30.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.
@@ -0,0 +1,141 @@
1
+ package com.dylanvann.fastimage;
2
+
3
+ import android.graphics.Rect;
4
+ import android.graphics.drawable.Drawable;
5
+
6
+ import androidx.annotation.NonNull;
7
+ import androidx.annotation.Nullable;
8
+
9
+ import com.bumptech.glide.load.engine.Resource;
10
+ import com.github.penfeizhou.animation.apng.APNGDrawable;
11
+ import com.github.penfeizhou.animation.apng.decode.APNGDecoder;
12
+ import com.github.penfeizhou.animation.loader.ByteBufferLoader;
13
+ import com.github.penfeizhou.animation.loader.Loader;
14
+
15
+ import java.nio.ByteBuffer;
16
+
17
+ // APNGs with APNG4Android (FastImageApng, which only uses this when it's
18
+ // there). Glide keeps the image's data in its memory cache, and each view
19
+ // gets a drawable of its own over it, so views don't share playback: making
20
+ // one only reads the image's header, and it decodes its frames (at a sample
21
+ // size from the size it's drawn at) on APNG4Android's threads as it plays.
22
+ final class FastImageApngRenderer {
23
+ private FastImageApngRenderer() {
24
+ }
25
+
26
+ // Throws NoClassDefFoundError without APNG4Android.
27
+ static boolean available() {
28
+ return APNGDrawable.class.getName() != null;
29
+ }
30
+
31
+ private static Loader loader(ByteBuffer data) {
32
+ return new ByteBufferLoader() {
33
+ @Override
34
+ public ByteBuffer getByteBuffer() {
35
+ ByteBuffer buffer = data.duplicate();
36
+ buffer.position(0);
37
+ return buffer;
38
+ }
39
+ };
40
+ }
41
+
42
+ // Null if APNG4Android can't read it (Glide then decodes it as a PNG).
43
+ @Nullable
44
+ static Resource<Drawable> decode(@NonNull ByteBuffer data, int plays) {
45
+ // Reads the header, waiting for APNG4Android's thread.
46
+ Rect bounds = new APNGDecoder(loader(data), null).getBounds();
47
+ if (bounds.isEmpty()) return null;
48
+ return new ApngResource(data, plays, bounds);
49
+ }
50
+
51
+ private static final class ApngResource implements Resource<Drawable> {
52
+ private final ByteBuffer data;
53
+ private final int plays;
54
+ private final Rect bounds;
55
+
56
+ ApngResource(ByteBuffer data, int plays, Rect bounds) {
57
+ this.data = data;
58
+ this.plays = plays;
59
+ this.bounds = bounds;
60
+ }
61
+
62
+ @NonNull
63
+ @Override
64
+ public Class<Drawable> getResourceClass() {
65
+ return Drawable.class;
66
+ }
67
+
68
+ @NonNull
69
+ @Override
70
+ public Drawable get() {
71
+ return new ApngDrawable(loader(data), plays);
72
+ }
73
+
74
+ // The data, and a frame at full size (each view's drawable has its
75
+ // own, at the size it's drawn at).
76
+ @Override
77
+ public int getSize() {
78
+ return data.limit() + bounds.width() * bounds.height() * 4;
79
+ }
80
+
81
+ @Override
82
+ public void recycle() {
83
+ }
84
+ }
85
+
86
+ private static final class ApngDrawable extends APNGDrawable implements FastImageAnimatable {
87
+ // How many times the file says it plays (0: forever).
88
+ private final int plays;
89
+ private volatile boolean paused = false;
90
+
91
+ ApngDrawable(Loader loader, int plays) {
92
+ super(loader);
93
+ this.plays = plays;
94
+ }
95
+
96
+ @Override
97
+ public void setLoopCount(int loopCount, boolean restart) {
98
+ // Plays (0: forever), as APNG4Android counts them.
99
+ setLoopLimit(loopCount == -1 ? plays : loopCount);
100
+ if (restart) {
101
+ reset();
102
+ start();
103
+ }
104
+ }
105
+
106
+ @Override
107
+ public void setPaused(boolean paused) {
108
+ this.paused = paused;
109
+ // It starts and stops itself as it's shown and hidden, but not
110
+ // while paused.
111
+ setAutoPlay(!paused);
112
+ if (paused) {
113
+ pause();
114
+ } else {
115
+ resume();
116
+ }
117
+ }
118
+
119
+ // From its first frame. The target starts it right after
120
+ // setVisible(true) has (auto play): APNG4Android's start() stops and
121
+ // starts a running decoder, and a stop made while the decoder is
122
+ // still starting can win over that start, leaving it stopped on its
123
+ // first frame. A running one is reset to its first frame instead.
124
+ @Override
125
+ public void start() {
126
+ if (isRunning()) {
127
+ getFrameSeqDecoder().reset();
128
+ return;
129
+ }
130
+ super.start();
131
+ }
132
+
133
+ // On its decoder thread, once starting has drawn the first frame:
134
+ // starting clears a pause made before it got there, so pause again.
135
+ @Override
136
+ public void onStart() {
137
+ super.onStart();
138
+ if (paused) getFrameSeqDecoder().pause();
139
+ }
140
+ }
141
+ }
@@ -121,6 +121,8 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
121
121
  OkHttpClient client = builder.build();
122
122
  registry.replace(GlideUrl.class, InputStream.class, new UrlLoaderFactory(client));
123
123
  FastImageSvg.register(registry, glide.getBitmapPool());
124
+ FastImageAnimated.register(context, glide, registry);
125
+ FastImageApng.register(registry);
124
126
 
125
127
  // `cache: 'web'` skips Glide's caches and relies on HTTP caching, so
126
128
  // those urls get a client with an HTTP cache (#280): one of their own,
@@ -156,7 +158,10 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
156
158
  return new ModelLoader<GlideUrl, InputStream>() {
157
159
  @Override
158
160
  public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height, @NonNull Options options) {
159
- return loader.buildLoadData(model, width, height, options);
161
+ LoadData<InputStream> data = loader.buildLoadData(model, width, height, options);
162
+ // A view loading an image that's being preloaded waits
163
+ // for the preload's download.
164
+ return data == null ? null : FastImageSharedDownloads.share(data, model, options);
160
165
  }
161
166
 
162
167
  @Override
@@ -0,0 +1,292 @@
1
+ package com.dylanvann.fastimage;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import androidx.annotation.Nullable;
5
+
6
+ import com.bumptech.glide.Priority;
7
+ import com.bumptech.glide.load.DataSource;
8
+ import com.bumptech.glide.load.Option;
9
+ import com.bumptech.glide.load.Options;
10
+ import com.bumptech.glide.load.data.DataFetcher;
11
+ import com.bumptech.glide.load.model.GlideUrl;
12
+ import com.bumptech.glide.load.model.ModelLoader;
13
+
14
+ import java.io.File;
15
+ import java.io.FileInputStream;
16
+ import java.io.IOException;
17
+ import java.io.InputStream;
18
+ import java.util.ArrayList;
19
+ import java.util.List;
20
+ import java.util.Map;
21
+ import java.util.concurrent.ConcurrentHashMap;
22
+ import java.util.concurrent.Executors;
23
+ import java.util.concurrent.ScheduledExecutorService;
24
+ import java.util.concurrent.ThreadFactory;
25
+ import java.util.concurrent.TimeUnit;
26
+ import java.util.concurrent.atomic.AtomicBoolean;
27
+
28
+ // Lets a view load an image that a preload is downloading from the preload's
29
+ // file, instead of downloading it again. Glide only shares a download between
30
+ // requests for the same size (and a preload loads the original size), so a
31
+ // view mounted while its image was being preloaded downloaded it a second
32
+ // time. A preload's download registers here once it has started (not while
33
+ // it's queued); a view's download of the same url then waits for the
34
+ // preload's file and reads it, and downloads as usual if the preload fails.
35
+ // The wait holds no thread: the view's fetch returns, and goes on when the
36
+ // preload has finished, as an OkHttp download does. A finished download stays
37
+ // for a while: a view's request that looked in the disk cache before the
38
+ // preload stored the file, and gets to downloading after, reads it too.
39
+ final class FastImageSharedDownloads {
40
+ // Set on a preload's request (see FastImageViewModule.loadFile).
41
+ static final Option<Boolean> PRELOAD = Option.memory("com.dylanvann.fastimage.Preload", false);
42
+
43
+ // The longest a view waits for a preload's download, before downloading
44
+ // the image itself.
45
+ private static final long WAIT_MS = 30_000;
46
+ // How long a finished download's file is used.
47
+ private static final long KEEP_MS = 60_000;
48
+
49
+ // Goes on with waiting views (opening the file, or starting their own
50
+ // download), off the main thread, where preloads finish.
51
+ private static final ScheduledExecutorService executor =
52
+ Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
53
+ @Override
54
+ public Thread newThread(@NonNull Runnable runnable) {
55
+ Thread thread = new Thread(runnable, "FastImageSharedDownloads");
56
+ thread.setDaemon(true);
57
+ return thread;
58
+ }
59
+ });
60
+
61
+ // A view's fetch waiting for a download. It goes on once: with the file,
62
+ // or without it (the preload failed, or took too long).
63
+ private static final class Waiter {
64
+ private final Fetcher fetcher;
65
+ private final Priority priority;
66
+ private final DataFetcher.DataCallback<? super InputStream> callback;
67
+ private final AtomicBoolean went = new AtomicBoolean();
68
+
69
+ Waiter(Fetcher fetcher, Priority priority, DataFetcher.DataCallback<? super InputStream> callback) {
70
+ this.fetcher = fetcher;
71
+ this.priority = priority;
72
+ this.callback = callback;
73
+ }
74
+
75
+ void go(@Nullable File file) {
76
+ if (went.compareAndSet(false, true)) fetcher.resume(file, priority, callback);
77
+ }
78
+
79
+ // Cancelled: never goes on.
80
+ void drop() {
81
+ went.set(true);
82
+ }
83
+ }
84
+
85
+ private static final class Download {
86
+ // Guarded by this.
87
+ private boolean done;
88
+ private final List<Waiter> waiters = new ArrayList<>();
89
+ @Nullable
90
+ volatile File file;
91
+ volatile long finishedAt;
92
+
93
+ synchronized boolean isDone() {
94
+ return done;
95
+ }
96
+
97
+ boolean isStale(long now) {
98
+ return isDone() && now - finishedAt > KEEP_MS;
99
+ }
100
+
101
+ void await(final Waiter waiter) {
102
+ synchronized (this) {
103
+ if (!done) {
104
+ waiters.add(waiter);
105
+ executor.schedule(new Runnable() {
106
+ @Override
107
+ public void run() {
108
+ remove(waiter);
109
+ waiter.go(null);
110
+ }
111
+ }, WAIT_MS, TimeUnit.MILLISECONDS);
112
+ return;
113
+ }
114
+ }
115
+ goOn(waiter, file);
116
+ }
117
+
118
+ synchronized void remove(Waiter waiter) {
119
+ waiters.remove(waiter);
120
+ }
121
+
122
+ // Returns false if it had finished already.
123
+ boolean finish(@Nullable File file, long now) {
124
+ List<Waiter> waiting;
125
+ synchronized (this) {
126
+ if (done) return false;
127
+ done = true;
128
+ this.file = file;
129
+ finishedAt = now;
130
+ waiting = new ArrayList<>(waiters);
131
+ waiters.clear();
132
+ }
133
+ for (Waiter waiter : waiting) goOn(waiter, file);
134
+ return true;
135
+ }
136
+
137
+ private static void goOn(final Waiter waiter, @Nullable final File file) {
138
+ executor.execute(new Runnable() {
139
+ @Override
140
+ public void run() {
141
+ waiter.go(file);
142
+ }
143
+ });
144
+ }
145
+ }
146
+
147
+ // Preloads' downloads in progress, or finished with their file, by the
148
+ // url's cache key.
149
+ private static final ConcurrentHashMap<String, Download> downloads = new ConcurrentHashMap<>();
150
+
151
+ private FastImageSharedDownloads() {
152
+ }
153
+
154
+ // A preload's download of the image with this cache key has ended: with
155
+ // its file in Glide's disk cache, or null if it failed or was cancelled.
156
+ // Views waiting for it go on.
157
+ static void finished(String key, @Nullable File file) {
158
+ long now = System.currentTimeMillis();
159
+ for (Map.Entry<String, Download> entry : downloads.entrySet()) {
160
+ if (entry.getValue().isStale(now)) downloads.remove(entry.getKey(), entry.getValue());
161
+ }
162
+ Download download = downloads.get(key);
163
+ if (download == null) return;
164
+ if (download.finish(file, now) && file == null) downloads.remove(key, download);
165
+ }
166
+
167
+ // The url loader's load data, with a fetcher that shares preloads'
168
+ // downloads.
169
+ static ModelLoader.LoadData<InputStream> share(
170
+ ModelLoader.LoadData<InputStream> data, GlideUrl model, Options options) {
171
+ boolean preload = Boolean.TRUE.equals(options.get(PRELOAD));
172
+ return new ModelLoader.LoadData<>(
173
+ data.sourceKey,
174
+ data.alternateKeys,
175
+ new Fetcher(data.fetcher, model.getCacheKey(), preload));
176
+ }
177
+
178
+ private static final class Fetcher implements DataFetcher<InputStream> {
179
+ private final DataFetcher<InputStream> fetcher;
180
+ private final String key;
181
+ private final boolean preload;
182
+ private volatile boolean cancelled;
183
+ // While it waits for a preload's download.
184
+ @Nullable
185
+ private volatile Download download;
186
+ @Nullable
187
+ private volatile Waiter waiter;
188
+ // The preload's file, when the image came from it.
189
+ @Nullable
190
+ private volatile InputStream stream;
191
+
192
+ Fetcher(DataFetcher<InputStream> fetcher, String key, boolean preload) {
193
+ this.fetcher = fetcher;
194
+ this.key = key;
195
+ this.preload = preload;
196
+ }
197
+
198
+ @Override
199
+ public void loadData(@NonNull Priority priority, @NonNull final DataCallback<? super InputStream> callback) {
200
+ if (preload) {
201
+ // A second preload of the url (with other options) keeps the
202
+ // first's entry while it's downloading; one after it finished
203
+ // downloads again (the file left the disk cache).
204
+ Download current = downloads.get(key);
205
+ if (current == null || current.isDone()) downloads.put(key, new Download());
206
+ fetcher.loadData(priority, new DataCallback<InputStream>() {
207
+ @Override
208
+ public void onDataReady(@Nullable InputStream data) {
209
+ callback.onDataReady(data);
210
+ }
211
+
212
+ @Override
213
+ public void onLoadFailed(@NonNull Exception e) {
214
+ finished(key, null);
215
+ callback.onLoadFailed(e);
216
+ }
217
+ });
218
+ return;
219
+ }
220
+ Download current = downloads.get(key);
221
+ if (current == null || current.isStale(System.currentTimeMillis())) {
222
+ fetcher.loadData(priority, callback);
223
+ return;
224
+ }
225
+ Waiter waiting = new Waiter(this, priority, callback);
226
+ download = current;
227
+ waiter = waiting;
228
+ current.await(waiting);
229
+ }
230
+
231
+ // Goes on after waiting: reads the preload's file, or downloads the
232
+ // image if there's none.
233
+ void resume(@Nullable File file, Priority priority, DataCallback<? super InputStream> callback) {
234
+ download = null;
235
+ waiter = null;
236
+ if (cancelled) return;
237
+ if (file != null) {
238
+ try {
239
+ stream = new FileInputStream(file);
240
+ callback.onDataReady(stream);
241
+ return;
242
+ } catch (IOException e) {
243
+ // Gone from the disk cache meanwhile: download it.
244
+ }
245
+ }
246
+ fetcher.loadData(priority, callback);
247
+ }
248
+
249
+ @Override
250
+ public void cleanup() {
251
+ InputStream opened = stream;
252
+ if (opened != null) {
253
+ try {
254
+ opened.close();
255
+ } catch (IOException e) {
256
+ // Closed as far as it could.
257
+ }
258
+ stream = null;
259
+ }
260
+ fetcher.cleanup();
261
+ }
262
+
263
+ @Override
264
+ public void cancel() {
265
+ cancelled = true;
266
+ Download waitingFor = download;
267
+ Waiter waiting = waiter;
268
+ if (waitingFor != null && waiting != null) {
269
+ waitingFor.remove(waiting);
270
+ waiting.drop();
271
+ }
272
+ // The preload's download stops: views waiting for it download
273
+ // the image themselves.
274
+ if (preload) finished(key, null);
275
+ fetcher.cancel();
276
+ }
277
+
278
+ @NonNull
279
+ @Override
280
+ public Class<InputStream> getDataClass() {
281
+ return InputStream.class;
282
+ }
283
+
284
+ // From the preload's file: as from Glide's disk cache, which it is in,
285
+ // so Glide doesn't store it there again.
286
+ @NonNull
287
+ @Override
288
+ public DataSource getDataSource() {
289
+ return stream != null ? DataSource.DATA_DISK_CACHE : fetcher.getDataSource();
290
+ }
291
+ }
292
+ }
@@ -169,7 +169,7 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
169
169
  pendingPreloads.add(new Runnable() {
170
170
  @Override
171
171
  public void run() {
172
- loadFile(context, imageSource.getSourceForLoad(), preloadOptions, new FileCallback() {
172
+ loadFile(context, imageSource.getSourceForLoad(), preloadOptions, true, new FileCallback() {
173
173
  @Override
174
174
  public void run(@Nullable final File file, @Nullable String error) {
175
175
  if (file == null) {
@@ -196,7 +196,8 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
196
196
  });
197
197
  continue;
198
198
  }
199
- pendingPreloads.add(new Runnable() {
199
+ // Decodes it into the memory cache.
200
+ final Runnable decode = new Runnable() {
200
201
  @Override
201
202
  public void run() {
202
203
  Glide
@@ -222,6 +223,22 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
222
223
  })
223
224
  .preload();
224
225
  }
226
+ };
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.
230
+ final boolean download = !imageSource.isWebCache() && imageSource.isRemote();
231
+ pendingPreloads.add(!download ? decode : new Runnable() {
232
+ @Override
233
+ public void run() {
234
+ loadFile(context, imageSource.getSourceForLoad(), preloadOptions, true, new FileCallback() {
235
+ @Override
236
+ public void run(@Nullable File file, @Nullable String error) {
237
+ if (file == null) done.run(failure(error));
238
+ else decode.run();
239
+ }
240
+ });
241
+ }
225
242
  });
226
243
  }
227
244
  if (count == 0) {
@@ -250,21 +267,26 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
250
267
 
251
268
  // Downloads the image into Glide's disk cache without decoding it (if it
252
269
  // isn't there), then calls back with its file or the error, on the UI
253
- // thread.
254
- private static void loadFile(Context context, Object model, RequestOptions options, final FileCallback callback) {
270
+ // thread. For a preload (`shared`), views that load the image meanwhile
271
+ // wait for the download and read its file (FastImageSharedDownloads).
272
+ private static void loadFile(Context context, Object model, RequestOptions options, boolean shared, final FileCallback callback) {
273
+ final String key = shared && model instanceof GlideUrl ? ((GlideUrl) model).getCacheKey() : null;
255
274
  Glide.with(context)
256
275
  .asFile()
257
276
  .load(model)
258
277
  .apply(options)
278
+ .set(FastImageSharedDownloads.PRELOAD, shared)
259
279
  .listener(new RequestListener<File>() {
260
280
  @Override
261
281
  public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
282
+ if (key != null) FastImageSharedDownloads.finished(key, null);
262
283
  callback.run(null, FastImageRequestListener.errorMessage(e));
263
284
  return false;
264
285
  }
265
286
 
266
287
  @Override
267
288
  public boolean onResourceReady(File file, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
289
+ if (key != null) FastImageSharedDownloads.finished(key, file);
268
290
  callback.run(file, null);
269
291
  return false;
270
292
  }
@@ -400,7 +422,7 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
400
422
  return new Runnable() {
401
423
  @Override
402
424
  public void run() {
403
- loadFile(context, imageSource.getSourceForLoad(), options, new FileCallback() {
425
+ loadFile(context, imageSource.getSourceForLoad(), options, false, new FileCallback() {
404
426
  @Override
405
427
  public void run(@Nullable File file, @Nullable String error) {
406
428
  finishDownload(promise, file != null ? pathResult(file) : failure(error));