react-native-fast-image 9.1.0 → 9.2.1

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.
@@ -1,278 +1,364 @@
1
1
  package com.dylanvann.fastimage;
2
2
 
3
+ import android.os.Process;
4
+
3
5
  import androidx.annotation.NonNull;
4
6
  import androidx.annotation.Nullable;
5
7
 
6
8
  import com.bumptech.glide.Priority;
7
9
  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.HttpException;
10
11
  import com.bumptech.glide.load.data.DataFetcher;
11
12
  import com.bumptech.glide.load.model.GlideUrl;
12
13
  import com.bumptech.glide.load.model.ModelLoader;
13
14
 
15
+ import java.io.EOFException;
14
16
  import java.io.File;
15
17
  import java.io.FileInputStream;
16
18
  import java.io.IOException;
17
19
  import java.io.InputStream;
20
+ import java.io.RandomAccessFile;
18
21
  import java.util.ArrayList;
22
+ import java.util.Arrays;
23
+ import java.util.Comparator;
24
+ import java.util.HashMap;
19
25
  import java.util.List;
20
26
  import java.util.Map;
27
+ import java.util.PriorityQueue;
28
+ import java.util.TreeMap;
21
29
  import java.util.concurrent.ConcurrentHashMap;
22
- import java.util.concurrent.Executors;
23
- import java.util.concurrent.ScheduledExecutorService;
30
+ import java.util.concurrent.LinkedBlockingQueue;
24
31
  import java.util.concurrent.ThreadFactory;
32
+ import java.util.concurrent.ThreadPoolExecutor;
25
33
  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);
34
+ import java.util.concurrent.atomic.AtomicInteger;
35
+
36
+ import okhttp3.Call;
37
+ import okhttp3.Callback;
38
+ import okhttp3.OkHttpClient;
39
+ import okhttp3.Request;
40
+ import okhttp3.Response;
41
+ import okhttp3.ResponseBody;
42
+ import okio.BufferedSource;
42
43
 
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.
44
+ // One download of an image at a time, shared by every request for it, as
45
+ // SDWebImage's downloader shares one per url (and Fresco one per encoded
46
+ // image). Glide only shares a load between requests for the same size and
47
+ // options, so views showing an image at different sizes, or one mounted while
48
+ // a preload downloads it, downloaded it again. Downloads are keyed by key(),
49
+ // which progress is sent by too (FastImageViewManager.onDownloadProgress).
50
+ //
51
+ // A download reads its response on a thread of its own, and keeps the bytes:
52
+ // in memory if the response is small (a known length up to MEMORY_MAX, while
53
+ // MEMORY_BUDGET has room), otherwise in a temporary file (Glide streams a
54
+ // download to its disk cache, and decodes at the view's size, so a large image
55
+ // never needs its bytes in the heap; the file keeps it that way; if the file
56
+ // can't be made or written, e.g. on a full disk, they're kept in memory up to
57
+ // FALLBACK_MAX). The requests
58
+ // for it wait without holding a thread, and once it has ended, each gets a
59
+ // stream over all of the bytes, as SDWebImage and Fresco decode a download
60
+ // once it has arrived. Glide's threads (4) never wait for the network, so a
61
+ // request that comes while many images download still gets to the queue below
62
+ // at once, and other images aren't held up behind slow downloads. The download
63
+ // is cancelled once every request for it has gone, and its bytes are dropped
64
+ // once every request has read what it needed.
65
+ //
66
+ // At most MAX_RUNNING downloads run at once, as SDWebImage's downloader limits
67
+ // them; the others wait to send their request, most urgent first (Glide's
68
+ // priority: views before preloads). A running download's response is read as
69
+ // soon as it arrives (not on OkHttp's dispatcher, which React Native's
70
+ // networking shares): a response left unread on an HTTP/2 connection would
71
+ // hold back the others on it. A download that stalls fails like any other,
72
+ // with the timeouts of the clients (FastImageOkHttpProgressGlideModule), which
73
+ // only count once it has started.
74
+ final class FastImageSharedDownloads {
75
+ // The longest response kept in memory, and the most bytes held in memory
76
+ // for downloads at once: encoded images (not decoded bitmaps, which
77
+ // Glide's memory cache holds), each only until every request has read it.
78
+ // Others are kept in a temporary file.
79
+ private static final long MEMORY_MAX = 1024 * 1024;
80
+ private static final long MEMORY_BUDGET = Runtime.getRuntime().maxMemory() / 16;
81
+ // The longest response kept in memory, still within MEMORY_BUDGET, when
82
+ // its temporary file can't be made or written (e.g. the device's storage
83
+ // is full): Glide's own fetcher decoded a download from its network
84
+ // stream, which it buffers up to 5 MB, when it couldn't write its disk
85
+ // cache.
86
+ private static final int FALLBACK_MAX = 5 * 1024 * 1024;
87
+ // The most downloads running at once (see above).
88
+ private static final int MAX_RUNNING = 16;
89
+ // How long a preload's finished file is used (see finished()).
47
90
  private static final long KEEP_MS = 60_000;
91
+ // Progress is sent when a download has read another 0.5% (and at its
92
+ // start and end).
93
+ private static final long PROGRESS_STEPS = 200;
48
94
 
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() {
95
+ // The download new requests for a key join: in progress, or ended and
96
+ // still being read. Guards the downloads' sharing, queue and progress
97
+ // (taken before a download's own lock, and FastImageViewManager's progress
98
+ // lock).
99
+ private static final Map<String, Download> downloads = new HashMap<>();
100
+ // Guarded by downloads: downloads waiting to start, most urgent first, then
101
+ // in the order they were asked for; how many are running; and the bytes
102
+ // held in memory.
103
+ private static final PriorityQueue<Download> queue = new PriorityQueue<>(11, new Comparator<Download>() {
104
+ @Override
105
+ public int compare(Download a, Download b) {
106
+ int byPriority = a.priority.compareTo(b.priority);
107
+ return byPriority != 0 ? byPriority : Long.compare(a.order, b.order);
108
+ }
109
+ });
110
+ private static long orders = 0;
111
+ private static int running = 0;
112
+ private static long held = 0;
113
+ // Stores not yet released (see busy()).
114
+ private static final AtomicInteger open = new AtomicInteger();
115
+
116
+ // Where temporary files go (FastImageOkHttpProgressGlideModule sets it).
117
+ @Nullable
118
+ private static volatile File directory;
119
+
120
+ // Reads the running downloads' responses, one thread each, at the priority
121
+ // of Glide's own threads (below the UI's), where downloads were read before.
122
+ private static final ThreadPoolExecutor reading = new ThreadPoolExecutor(
123
+ MAX_RUNNING, MAX_RUNNING, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
124
+ new ThreadFactory() {
53
125
  @Override
54
- public Thread newThread(@NonNull Runnable runnable) {
55
- Thread thread = new Thread(runnable, "FastImageSharedDownloads");
126
+ public Thread newThread(@NonNull final Runnable runnable) {
127
+ Thread thread = new Thread(new Runnable() {
128
+ @Override
129
+ public void run() {
130
+ Process.setThreadPriority(
131
+ Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
132
+ runnable.run();
133
+ }
134
+ }, "FastImageDownload");
56
135
  thread.setDaemon(true);
57
136
  return thread;
58
137
  }
59
138
  });
60
139
 
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();
140
+ static {
141
+ reading.allowCoreThreadTimeOut(true);
142
+ }
68
143
 
69
- Waiter(Fetcher fetcher, Priority priority, DataFetcher.DataCallback<? super InputStream> callback) {
70
- this.fetcher = fetcher;
71
- this.priority = priority;
72
- this.callback = callback;
73
- }
144
+ private static final class Finished {
145
+ final File file;
146
+ final long at;
74
147
 
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);
148
+ Finished(File file, long at) {
149
+ this.file = file;
150
+ this.at = at;
82
151
  }
83
152
  }
84
153
 
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;
154
+ // Preloads' files in Glide's disk cache, by key, for a while after they
155
+ // finished.
156
+ private static final ConcurrentHashMap<String, Finished> finished = new ConcurrentHashMap<>();
92
157
 
93
- synchronized boolean isDone() {
94
- return done;
95
- }
158
+ private FastImageSharedDownloads() {
159
+ }
96
160
 
97
- boolean isStale(long now) {
98
- return isDone() && now - finishedAt > KEEP_MS;
161
+ // For tests: what downloads still hold, or null once none is left (none
162
+ // waiting or running), their memory has been given back, and every store
163
+ // released.
164
+ @Nullable
165
+ static String busy() {
166
+ synchronized (downloads) {
167
+ int stores = open.get();
168
+ if (downloads.isEmpty() && queue.isEmpty() && running == 0 && held == 0 && stores == 0) return null;
169
+ return downloads.size() + " downloads, " + queue.size() + " waiting, " + running + " running, "
170
+ + held + " bytes in memory, " + stores + " stores open";
99
171
  }
172
+ }
100
173
 
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
- }
174
+ // Also removes files left in it (one is unlinked once open, but the app
175
+ // can end in between).
176
+ static void setDirectory(final File cacheDirectory) {
177
+ directory = cacheDirectory;
178
+ reading.execute(new Runnable() {
179
+ @Override
180
+ public void run() {
181
+ File[] left = cacheDirectory.listFiles();
182
+ if (left == null) return;
183
+ //noinspection ResultOfMethodCallIgnored
184
+ for (File file : left) file.delete();
114
185
  }
115
- goOn(waiter, file);
116
- }
186
+ });
187
+ }
188
+
189
+ // The key downloads are shared, and their progress sent, by. A url with
190
+ // the source's cacheKey: the key (Glide caches it, and shares its loads,
191
+ // by the key too). Another url: the url and its headers (Glide's loads
192
+ // compare both). `web` images apart: they load with their own client, and
193
+ // its HTTP cache.
194
+ static String key(GlideUrl url) {
195
+ if (url instanceof FastImageKeyedGlideUrl) return "key " + url.getCacheKey();
196
+ String key = url.getCacheKey() + " " + new TreeMap<>(url.getHeaders());
197
+ return (url instanceof FastImageWebGlideUrl ? "web " : "url ") + key;
198
+ }
117
199
 
118
- synchronized void remove(Waiter waiter) {
119
- waiters.remove(waiter);
200
+ // A preload's download ended (FastImageViewModule.loadFile), with its file
201
+ // in Glide's disk cache, or null if it failed. A request that looked in
202
+ // the disk cache before the preload stored the file, and gets to
203
+ // downloading after, reads the file.
204
+ static void finished(String key, @Nullable File file) {
205
+ long now = System.currentTimeMillis();
206
+ for (Map.Entry<String, Finished> entry : finished.entrySet()) {
207
+ if (now - entry.getValue().at > KEEP_MS) finished.remove(entry.getKey(), entry.getValue());
120
208
  }
209
+ if (file != null) finished.put(key, new Finished(file, now));
210
+ }
121
211
 
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;
212
+ @Nullable
213
+ private static File finishedFile(String key) {
214
+ Finished file = finished.get(key);
215
+ if (file == null) return null;
216
+ if (System.currentTimeMillis() - file.at > KEEP_MS) {
217
+ finished.remove(key, file);
218
+ return null;
135
219
  }
220
+ return file.file;
221
+ }
136
222
 
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
- });
223
+ // A remote image's load data (FastImageOkHttpProgressGlideModule): its
224
+ // download with the client, shared with the other requests for its key.
225
+ static ModelLoader.LoadData<InputStream> loadData(GlideUrl url, OkHttpClient client) {
226
+ return new ModelLoader.LoadData<>(url, new Fetcher(url, client));
227
+ }
228
+
229
+ // As Glide's OkHttp fetcher requests it. A url or header OkHttp can't
230
+ // send is an IOException, so its load fails (OkHttp throws an
231
+ // IllegalArgumentException, which would crash the thread it's on).
232
+ static Request request(GlideUrl url) throws IOException {
233
+ try {
234
+ Request.Builder builder = new Request.Builder().url(url.toStringUrl());
235
+ for (Map.Entry<String, String> header : url.getHeaders().entrySet()) {
236
+ builder.addHeader(header.getKey(), header.getValue());
237
+ }
238
+ return builder.build();
239
+ } catch (IllegalArgumentException e) {
240
+ throw new IOException("Invalid request: " + e.getMessage(), e);
144
241
  }
145
242
  }
146
243
 
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<>();
244
+ private static boolean sameRequest(GlideUrl a, GlideUrl b) {
245
+ return a.toStringUrl().equals(b.toStringUrl()) && a.getHeaders().equals(b.getHeaders());
246
+ }
150
247
 
151
- private FastImageSharedDownloads() {
248
+ private static void closeQuietly(InputStream stream) {
249
+ try {
250
+ stream.close();
251
+ } catch (IOException e) {
252
+ // Closed as far as it could.
253
+ }
152
254
  }
153
255
 
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());
256
+ // Starts the downloads waiting that can run now.
257
+ private static void startQueued() {
258
+ List<Download> starting = new ArrayList<>();
259
+ List<Request> requests = new ArrayList<>();
260
+ synchronized (downloads) {
261
+ while (running < MAX_RUNNING && !queue.isEmpty()) {
262
+ Download download = queue.poll();
263
+ download.queued = false;
264
+ download.running = true;
265
+ running++;
266
+ starting.add(download);
267
+ requests.add(download.request);
268
+ download.tried.add(download.url);
269
+ }
161
270
  }
162
- Download download = downloads.get(key);
163
- if (download == null) return;
164
- if (download.finish(file, now) && file == null) downloads.remove(key, download);
271
+ for (int i = 0; i < starting.size(); i++) starting.get(i).start(requests.get(i));
165
272
  }
166
273
 
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));
274
+ // A download's progress steps (see PROGRESS_STEPS).
275
+ private static final class Progress {
276
+ private long lastStep = -1;
277
+
278
+ // Whether `loaded` is another step. Without a Content-Length the total
279
+ // is unknown (-1), and a fraction can't be worked out from it.
280
+ boolean step(long loaded, long total) {
281
+ if (total <= 0) return false;
282
+ long step = Math.min(loaded, total) * PROGRESS_STEPS / total;
283
+ if (step == lastStep) return false;
284
+ lastStep = step;
285
+ return true;
286
+ }
176
287
  }
177
288
 
289
+ // A request's fetch: joins its key's download.
178
290
  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.
291
+ final GlideUrl url;
292
+ final OkHttpClient client;
293
+ // Set when it loads (Glide also makes fetchers it doesn't use, e.g.
294
+ // for an image in its disk cache).
295
+ String key;
296
+ Priority priority;
297
+ volatile boolean cancelled;
298
+ // The download it waits for, or reads (guarded by downloads).
184
299
  @Nullable
185
- private volatile Download download;
186
- @Nullable
187
- private volatile Waiter waiter;
188
- // The preload's file, when the image came from it.
300
+ Download download;
301
+ // What it gave Glide.
189
302
  @Nullable
190
303
  private volatile InputStream stream;
304
+ private volatile boolean fromFile;
191
305
 
192
- Fetcher(DataFetcher<InputStream> fetcher, String key, boolean preload) {
193
- this.fetcher = fetcher;
194
- this.key = key;
195
- this.preload = preload;
306
+ Fetcher(GlideUrl url, OkHttpClient client) {
307
+ this.url = url;
308
+ this.client = client;
196
309
  }
197
310
 
198
311
  @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;
312
+ public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
313
+ this.key = key(url);
314
+ this.priority = priority;
315
+ // `web` images follow their HTTP cache, not a preload's file.
316
+ File file = url instanceof FastImageWebGlideUrl ? null : finishedFile(key);
237
317
  if (file != null) {
238
318
  try {
239
- stream = new FileInputStream(file);
240
- callback.onDataReady(stream);
319
+ InputStream opened = new FileInputStream(file);
320
+ fromFile = true;
321
+ stream = opened;
322
+ callback.onDataReady(opened);
241
323
  return;
242
324
  } catch (IOException e) {
243
325
  // Gone from the disk cache meanwhile: download it.
244
326
  }
245
327
  }
246
- fetcher.loadData(priority, callback);
328
+ Download.join(this, callback);
329
+ }
330
+
331
+ // From the download, outside its locks, off the main thread.
332
+ void deliver(InputStream data, DataCallback<? super InputStream> callback) {
333
+ stream = data;
334
+ // Cancelled meanwhile: Glide won't read or close it.
335
+ if (cancelled) {
336
+ closeQuietly(data);
337
+ return;
338
+ }
339
+ callback.onDataReady(data);
247
340
  }
248
341
 
342
+ // On one of Glide's threads.
249
343
  @Override
250
344
  public void cleanup() {
251
345
  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();
346
+ if (opened != null) closeQuietly(opened);
261
347
  }
262
348
 
349
+ // On the main thread. The request leaves its download, which is
350
+ // cancelled once no request is left, and its stream on the bytes (if
351
+ // it has one) is closed.
263
352
  @Override
264
353
  public void cancel() {
265
354
  cancelled = true;
266
- Download waitingFor = download;
267
- Waiter waiting = waiter;
268
- if (waitingFor != null && waiting != null) {
269
- waitingFor.remove(waiting);
270
- waiting.drop();
355
+ Download joined;
356
+ synchronized (downloads) {
357
+ joined = download;
271
358
  }
272
- // The preload's download stops: views waiting for it download
273
- // the image themselves.
274
- if (preload) finished(key, null);
275
- fetcher.cancel();
359
+ if (joined != null) joined.leave(this);
360
+ InputStream opened = stream;
361
+ if (opened instanceof SharedStream) closeQuietly(opened);
276
362
  }
277
363
 
278
364
  @NonNull
@@ -281,12 +367,691 @@ final class FastImageSharedDownloads {
281
367
  return InputStream.class;
282
368
  }
283
369
 
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.
370
+ // A preload's file is from Glide's disk cache. A download is remote
371
+ // for every request reading it: Glide stores it in its disk cache once
372
+ // (a request that finds it there already reads it from there), and
373
+ // fades the image in as for any download. A `web` image's response
374
+ // from its HTTP cache, without the network, is from a disk cache too
375
+ // (OkHttp's; Glide doesn't store `web` images in its own).
286
376
  @NonNull
287
377
  @Override
288
378
  public DataSource getDataSource() {
289
- return stream != null ? DataSource.DATA_DISK_CACHE : fetcher.getDataSource();
379
+ if (fromFile) return DataSource.DATA_DISK_CACHE;
380
+ Download joined;
381
+ synchronized (downloads) {
382
+ joined = download;
383
+ }
384
+ return joined != null && joined.fromHttpCache ? DataSource.DATA_DISK_CACHE : DataSource.REMOTE;
385
+ }
386
+ }
387
+
388
+ // A request waiting for a download to end.
389
+ private static final class Waiter {
390
+ final Fetcher fetcher;
391
+ final DataFetcher.DataCallback<? super InputStream> callback;
392
+ // Its url's request (with its headers).
393
+ final Request request;
394
+
395
+ Waiter(Fetcher fetcher, DataFetcher.DataCallback<? super InputStream> callback, Request request) {
396
+ this.fetcher = fetcher;
397
+ this.callback = callback;
398
+ this.request = request;
399
+ }
400
+ }
401
+
402
+ // Where a download keeps its response's bytes: written in order on its
403
+ // reading thread, then read by the requests, on theirs.
404
+ private interface Store {
405
+ void write(long at, byte[] buffer, int count) throws IOException;
406
+
407
+ // Reads up to `count` of the bytes, from `at`.
408
+ int read(long at, byte[] buffer, int offset, int count) throws IOException;
409
+
410
+ void release();
411
+ }
412
+
413
+ private static final class MemoryStore implements Store {
414
+ private final byte[] bytes;
415
+
416
+ MemoryStore(int length) {
417
+ bytes = new byte[length];
418
+ }
419
+
420
+ @Override
421
+ public void write(long at, byte[] buffer, int count) {
422
+ System.arraycopy(buffer, 0, bytes, (int) at, count);
423
+ }
424
+
425
+ @Override
426
+ public int read(long at, byte[] buffer, int offset, int count) {
427
+ System.arraycopy(bytes, (int) at, buffer, offset, count);
428
+ return count;
429
+ }
430
+
431
+ @Override
432
+ public void release() {
433
+ }
434
+ }
435
+
436
+ // A temporary file, removed from its folder once open: its space is freed
437
+ // once it's closed, or the app ends, so nothing is left behind. Read and
438
+ // written with a RandomAccessFile, which (unlike a FileChannel) isn't
439
+ // closed for every thread when one reading it is interrupted.
440
+ private static final class FileStore implements Store {
441
+ private final RandomAccessFile file;
442
+
443
+ FileStore() throws IOException {
444
+ File folder = directory;
445
+ if (folder == null) throw new IOException("No folder for downloads");
446
+ if (!folder.isDirectory() && !folder.mkdirs() && !folder.isDirectory()) {
447
+ throw new IOException("Can't make " + folder);
448
+ }
449
+ File created = File.createTempFile("download", null, folder);
450
+ try {
451
+ file = new RandomAccessFile(created, "rw");
452
+ } finally {
453
+ //noinspection ResultOfMethodCallIgnored
454
+ created.delete();
455
+ }
456
+ }
457
+
458
+ @Override
459
+ public synchronized void write(long at, byte[] buffer, int count) throws IOException {
460
+ file.seek(at);
461
+ file.write(buffer, 0, count);
462
+ }
463
+
464
+ @Override
465
+ public synchronized int read(long at, byte[] buffer, int offset, int count) throws IOException {
466
+ file.seek(at);
467
+ int read = file.read(buffer, offset, count);
468
+ if (read == -1) throw new EOFException("The download's file ended early");
469
+ return read;
470
+ }
471
+
472
+ // Not waiting for a read in progress (it fails): only the request
473
+ // whose stream was closed (it was cancelled) can still be reading.
474
+ @Override
475
+ public void release() {
476
+ try {
477
+ file.close();
478
+ } catch (IOException e) {
479
+ // Closed as far as it could.
480
+ }
481
+ }
482
+ }
483
+
484
+ // The bytes in memory, for a response whose temporary file can't be used
485
+ // (see FALLBACK_MAX), growing as they arrive, with the memory reserved
486
+ // first.
487
+ private static final class GrowingStore implements Store {
488
+ private final Download download;
489
+ // Why the file couldn't be used.
490
+ private final IOException cause;
491
+ private byte[] bytes;
492
+
493
+ GrowingStore(Download download, byte[] bytes, IOException cause) {
494
+ this.download = download;
495
+ this.bytes = bytes;
496
+ this.cause = cause;
497
+ }
498
+
499
+ @Override
500
+ public void write(long at, byte[] buffer, int count) throws IOException {
501
+ long end = at + count;
502
+ if (end > bytes.length) {
503
+ if (end > FALLBACK_MAX) {
504
+ throw new IOException("The download is too large to keep in memory, and its temporary file couldn't be written", cause);
505
+ }
506
+ int length = (int) Math.min(FALLBACK_MAX, Math.max(end, 2L * bytes.length));
507
+ download.reserve(length - bytes.length, cause);
508
+ bytes = Arrays.copyOf(bytes, length);
509
+ }
510
+ System.arraycopy(buffer, 0, bytes, (int) at, count);
511
+ }
512
+
513
+ @Override
514
+ public int read(long at, byte[] buffer, int offset, int count) {
515
+ System.arraycopy(bytes, (int) at, buffer, offset, count);
516
+ return count;
517
+ }
518
+
519
+ @Override
520
+ public void release() {
521
+ }
522
+ }
523
+
524
+ private static final class Download implements Callback {
525
+ private final String key;
526
+ // Guarded by downloads. The request it downloads (its url and
527
+ // headers): until it starts, the latest url a request for its key
528
+ // asked for (see join()).
529
+ private GlideUrl url;
530
+ private OkHttpClient client;
531
+ private Request request;
532
+ // Guarded by downloads. The urls requested for its key so far: its own
533
+ // once it starts, and those of the downloads it's a retry of (see
534
+ // fail()).
535
+ private final List<GlideUrl> tried = new ArrayList<>();
536
+ // Guarded by downloads. In the queue (most urgent first, then in
537
+ // order), or running.
538
+ private Priority priority;
539
+ private final long order;
540
+ private boolean queued = false;
541
+ private boolean running = false;
542
+ // Guarded by this. The call, once started, and whether the download
543
+ // was cancelled (before it started, it doesn't).
544
+ @Nullable
545
+ private Call call;
546
+ private boolean cancelled = false;
547
+ // Guarded by this. Requests waiting for it to end, and once it has,
548
+ // the streams open on its bytes.
549
+ private final List<Waiter> waiting = new ArrayList<>();
550
+ private int readers = 0;
551
+ // Guarded by this. Where the bytes are (null before the response is
552
+ // read, and once dropped), and how many there are.
553
+ @Nullable
554
+ private Store store;
555
+ private long size = 0;
556
+ // All of the response has been read, or the download failed.
557
+ private boolean ended = false;
558
+ // Guarded by downloads. The memory the bytes hold, and whether it sends
559
+ // no more progress: it ended, or was cancelled (it can still read
560
+ // bytes it had received).
561
+ private long reserved = 0;
562
+ private boolean silent = false;
563
+ // Whether the response came from the HTTP cache of `web` images
564
+ // without the network (set before its bytes are read).
565
+ private volatile boolean fromHttpCache = false;
566
+
567
+ // With downloads locked.
568
+ private Download(String key, GlideUrl url, OkHttpClient client, Request request, Priority priority) {
569
+ this.key = key;
570
+ this.url = url;
571
+ this.client = client;
572
+ this.request = request;
573
+ this.priority = priority;
574
+ this.order = orders++;
575
+ }
576
+
577
+ // Joins the fetcher's key's download, or queues one of its url.
578
+ static void join(Fetcher fetcher, DataFetcher.DataCallback<? super InputStream> callback) {
579
+ // Its own request, which it sends if it starts the download.
580
+ Request request;
581
+ try {
582
+ request = request(fetcher.url);
583
+ } catch (IOException e) {
584
+ callback.onLoadFailed(e);
585
+ return;
586
+ }
587
+ Download download;
588
+ boolean queuedOne = false;
589
+ InputStream ready = null;
590
+ synchronized (downloads) {
591
+ // Cancelled before it got here.
592
+ if (fetcher.cancelled) return;
593
+ download = downloads.get(fetcher.key);
594
+ // A `web` image follows its HTTP cache: it only joins a
595
+ // download in progress, not the bytes of one that has ended.
596
+ if (download != null && fetcher.url instanceof FastImageWebGlideUrl && download.hasEnded()) {
597
+ download = null;
598
+ }
599
+ if (download == null) {
600
+ download = new Download(fetcher.key, fetcher.url, fetcher.client, request, fetcher.priority);
601
+ downloads.put(fetcher.key, download);
602
+ download.queued = true;
603
+ queue.add(download);
604
+ queuedOne = true;
605
+ } else if (download.queued) {
606
+ // A more urgent request moves it up (until it leaves).
607
+ if (fetcher.priority.compareTo(download.priority) < 0) download.reprioritize(fetcher.priority);
608
+ // Not started yet: another url for its key (a cacheKey's)
609
+ // is the latest one, e.g. a signed url with a fresh token,
610
+ // so it's the one requested (they're the same image).
611
+ if (!sameRequest(download.url, fetcher.url)) {
612
+ download.url = fetcher.url;
613
+ download.client = fetcher.client;
614
+ download.request = request;
615
+ }
616
+ }
617
+ fetcher.download = download;
618
+ synchronized (download) {
619
+ // An ended download in the map has its bytes (a failed one
620
+ // leaves the map).
621
+ if (download.ended) {
622
+ download.readers++;
623
+ ready = download.stream();
624
+ } else {
625
+ download.waiting.add(new Waiter(fetcher, callback, request));
626
+ }
627
+ }
628
+ }
629
+ if (ready != null) fetcher.deliver(ready, callback);
630
+ if (queuedOne) startQueued();
631
+ }
632
+
633
+ // Sends the request (off the main thread, and outside the locks:
634
+ // creating the call runs the app's OkHttp EventListener).
635
+ private void start(Request request) {
636
+ Call created = client.newCall(request);
637
+ boolean go;
638
+ synchronized (this) {
639
+ go = !cancelled;
640
+ if (go) call = created;
641
+ }
642
+ if (go) {
643
+ created.enqueue(this);
644
+ } else {
645
+ stopped();
646
+ }
647
+ }
648
+
649
+ // Cancels the call, or keeps it from starting. Outside the locks.
650
+ private void cancel() {
651
+ Call started;
652
+ synchronized (this) {
653
+ cancelled = true;
654
+ started = call;
655
+ }
656
+ if (started != null) started.cancel();
657
+ }
658
+
659
+ // It no longer runs: the next download waiting can start.
660
+ private void stopped() {
661
+ synchronized (downloads) {
662
+ if (!running) return;
663
+ running = false;
664
+ FastImageSharedDownloads.running--;
665
+ }
666
+ startQueued();
667
+ }
668
+
669
+ private synchronized boolean hasEnded() {
670
+ return ended;
671
+ }
672
+
673
+ // With downloads locked.
674
+ private boolean wasTried(GlideUrl other) {
675
+ for (GlideUrl requested : tried) {
676
+ if (sameRequest(requested, other)) return true;
677
+ }
678
+ return false;
679
+ }
680
+
681
+ // With downloads locked, while it's queued.
682
+ private void reprioritize(Priority urgency) {
683
+ queue.remove(this);
684
+ priority = urgency;
685
+ queue.add(this);
686
+ }
687
+
688
+ // With this locked, once it has ended: a stream over all of its bytes.
689
+ private SharedStream stream() {
690
+ return new SharedStream(this, store, size);
691
+ }
692
+
693
+ // With downloads locked: new requests start another download.
694
+ private void unmap() {
695
+ if (downloads.get(key) == this) downloads.remove(key);
696
+ }
697
+
698
+ // With downloads and this locked: drops the bytes, and gives their
699
+ // memory back. Returns the store, to release outside the locks (closing
700
+ // a file can wait for the disk; this can be the main thread).
701
+ @Nullable
702
+ private Store drop() {
703
+ held -= reserved;
704
+ reserved = 0;
705
+ Store dropped = store;
706
+ store = null;
707
+ return dropped;
708
+ }
709
+
710
+ private static void release(@Nullable Store store) {
711
+ if (store == null) return;
712
+ store.release();
713
+ open.decrementAndGet();
714
+ }
715
+
716
+ // With this locked: the requests waiting for it to end.
717
+ private List<Waiter> takeWaiting() {
718
+ List<Waiter> taken = new ArrayList<>(waiting);
719
+ waiting.clear();
720
+ return taken;
721
+ }
722
+
723
+ // A request was cancelled (on the main thread). With no one left, the
724
+ // download is too (or, waiting to start, leaves the queue); waiting to
725
+ // start, it's as urgent as the most urgent request left, and of the
726
+ // latest url left (the url of a request that left isn't asked for).
727
+ void leave(Fetcher fetcher) {
728
+ boolean cancel = false;
729
+ Priority urgency = null;
730
+ Waiter latest = null;
731
+ synchronized (downloads) {
732
+ synchronized (this) {
733
+ for (int i = waiting.size() - 1; i >= 0; i--) {
734
+ if (waiting.get(i).fetcher == fetcher) waiting.remove(i);
735
+ }
736
+ if (!ended && waiting.isEmpty()) {
737
+ unmap();
738
+ silent = true;
739
+ cancel = true;
740
+ }
741
+ for (Waiter waiter : waiting) {
742
+ Priority asked = waiter.fetcher.priority;
743
+ if (urgency == null || asked.compareTo(urgency) < 0) urgency = asked;
744
+ latest = waiter;
745
+ }
746
+ }
747
+ if (cancel && queued) {
748
+ queue.remove(this);
749
+ queued = false;
750
+ } else if (queued && urgency != null) {
751
+ if (urgency != priority) reprioritize(urgency);
752
+ if (!sameRequest(url, latest.fetcher.url)) {
753
+ url = latest.fetcher.url;
754
+ client = latest.fetcher.client;
755
+ request = latest.request;
756
+ }
757
+ }
758
+ }
759
+ if (cancel) cancel();
760
+ }
761
+
762
+ // A stream on the bytes was closed: once every one has, the bytes go.
763
+ void readerClosed() {
764
+ Store dropped = null;
765
+ synchronized (downloads) {
766
+ synchronized (this) {
767
+ readers--;
768
+ if (readers > 0) return;
769
+ unmap();
770
+ dropped = drop();
771
+ }
772
+ }
773
+ release(dropped);
774
+ }
775
+
776
+ @Override
777
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
778
+ fail(e);
779
+ }
780
+
781
+ // The download failed: the requests waiting for it fail with the
782
+ // error, but those that asked for another url (a cacheKey's, e.g. a
783
+ // signed url with a fresh token) wait for a retry, of the latest of
784
+ // those urls (they're the same image), if it hasn't been tried for the
785
+ // key yet: this url can fail for reasons of its own, like an expired
786
+ // token. So a newer url that joins a retry gets a retry of its own,
787
+ // and an older one doesn't. As on iOS, where each url downloads on its
788
+ // own, an image gets onError when its own url fails.
789
+ private void fail(IOException e) {
790
+ List<Waiter> failed;
791
+ Store dropped;
792
+ Download again = null;
793
+ synchronized (downloads) {
794
+ synchronized (this) {
795
+ unmap();
796
+ silent = true;
797
+ ended = true;
798
+ failed = takeWaiting();
799
+ dropped = drop();
800
+ }
801
+ Waiter latest = null;
802
+ Priority urgency = null;
803
+ for (Waiter waiter : failed) {
804
+ if (waiter.fetcher.cancelled || sameRequest(waiter.fetcher.url, url)) continue;
805
+ latest = waiter;
806
+ Priority asked = waiter.fetcher.priority;
807
+ if (urgency == null || asked.compareTo(urgency) < 0) urgency = asked;
808
+ }
809
+ if (latest != null && !wasTried(latest.fetcher.url)) {
810
+ again = new Download(key, latest.fetcher.url, latest.fetcher.client, latest.request, urgency);
811
+ again.tried.addAll(tried);
812
+ List<Waiter> failing = new ArrayList<>();
813
+ synchronized (again) {
814
+ for (Waiter waiter : failed) {
815
+ if (waiter.fetcher.cancelled) continue;
816
+ if (sameRequest(waiter.fetcher.url, url)) {
817
+ failing.add(waiter);
818
+ } else {
819
+ waiter.fetcher.download = again;
820
+ again.waiting.add(waiter);
821
+ }
822
+ }
823
+ }
824
+ failed = failing;
825
+ // Not yet in the map, so no one else has it. The requests
826
+ // for the other urls can have left meanwhile (a cancel
827
+ // marks its request before it takes the lock): no retry.
828
+ if (again.waiting.isEmpty()) {
829
+ again = null;
830
+ } else {
831
+ downloads.put(key, again);
832
+ again.queued = true;
833
+ queue.add(again);
834
+ }
835
+ }
836
+ }
837
+ release(dropped);
838
+ stopped();
839
+ if (again != null) startQueued();
840
+ for (Waiter waiter : failed) waiter.callback.onLoadFailed(e);
841
+ }
842
+
843
+ @Override
844
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
845
+ final ResponseBody body = response.body();
846
+ if (!response.isSuccessful() || body == null) {
847
+ if (body != null) body.close();
848
+ fail(new HttpException(response.message(), response.code()));
849
+ return;
850
+ }
851
+ // OkHttp has no network response for one it served from the
852
+ // cache without checking with the server.
853
+ fromHttpCache = response.networkResponse() == null && response.cacheResponse() != null;
854
+ final long length = body.contentLength();
855
+ synchronized (downloads) {
856
+ // No progress without a length.
857
+ if (length <= 0) silent = true;
858
+ }
859
+ reading.execute(new Runnable() {
860
+ @Override
861
+ public void run() {
862
+ readInto(body, length);
863
+ }
864
+ });
865
+ }
866
+
867
+ // Sends a progress step, unless the download has stopped: checked and
868
+ // sent with downloads locked, which a cancel takes to stop it, so a new
869
+ // load of the key never gets a cancelled download's steps. Without an
870
+ // image with onProgress, nothing is locked.
871
+ private void report(Progress progress, long loaded, long total) {
872
+ if (!progress.step(loaded, total) || !FastImageViewManager.wantsProgress()) return;
873
+ synchronized (downloads) {
874
+ if (!silent) FastImageViewManager.onDownloadProgress(key, Math.min(loaded, total), total);
875
+ }
876
+ }
877
+
878
+ // Memory for a response of known length up to MEMORY_MAX, if the
879
+ // budget has room for it (and there's the memory), otherwise a file.
880
+ private Store newStore(long length) throws IOException {
881
+ if (length >= 0 && length <= MEMORY_MAX) {
882
+ boolean fits;
883
+ synchronized (downloads) {
884
+ fits = held + length <= MEMORY_BUDGET;
885
+ if (fits) {
886
+ held += length;
887
+ reserved = length;
888
+ }
889
+ }
890
+ if (fits) {
891
+ try {
892
+ MemoryStore created = new MemoryStore((int) length);
893
+ open.incrementAndGet();
894
+ return created;
895
+ } catch (OutOfMemoryError e) {
896
+ synchronized (downloads) {
897
+ held -= reserved;
898
+ reserved = 0;
899
+ }
900
+ }
901
+ }
902
+ }
903
+ try {
904
+ FileStore created = new FileStore();
905
+ open.incrementAndGet();
906
+ return created;
907
+ } catch (IOException e) {
908
+ return inMemory(null, 0, length, e);
909
+ }
910
+ }
911
+
912
+ // Memory for `more` of its bytes, from MEMORY_BUDGET (see
913
+ // GrowingStore), given back when they're dropped; without room, the
914
+ // download fails with why its file couldn't be used.
915
+ private void reserve(long more, IOException cause) throws IOException {
916
+ synchronized (downloads) {
917
+ if (held + more > MEMORY_BUDGET) {
918
+ throw new IOException("No memory left for downloads, and the temporary file couldn't be written", cause);
919
+ }
920
+ held += more;
921
+ reserved += more;
922
+ }
923
+ }
924
+
925
+ // Its temporary file couldn't be made, or written (`failed`): the
926
+ // bytes go to memory instead (see FALLBACK_MAX), with the `read` of
927
+ // them already in `file`.
928
+ private Store inMemory(@Nullable Store file, long read, long length, IOException failed) throws IOException {
929
+ if (length > FALLBACK_MAX) throw failed;
930
+ int capacity = (int) (length >= 0 ? length : Math.max(read, 64 * 1024));
931
+ reserve(capacity, failed);
932
+ byte[] bytes = new byte[capacity];
933
+ if (file != null) {
934
+ for (long at = 0; at < read; ) at += file.read(at, bytes, (int) at, (int) (read - at));
935
+ }
936
+ GrowingStore memory = new GrowingStore(this, bytes, failed);
937
+ open.incrementAndGet();
938
+ synchronized (this) {
939
+ store = memory;
940
+ }
941
+ release(file);
942
+ return memory;
943
+ }
944
+
945
+ // Reads the response into its store, on a thread of `reading`, then
946
+ // gives the requests waiting a stream each.
947
+ private void readInto(ResponseBody body, long length) {
948
+ Progress progress = new Progress();
949
+ long read = 0;
950
+ try (BufferedSource source = body.source()) {
951
+ Store kept = newStore(length);
952
+ synchronized (this) {
953
+ store = kept;
954
+ }
955
+ byte[] buffer = new byte[8192];
956
+ while (length < 0 || read < length) {
957
+ int count = source.read(buffer, 0, (int) (length < 0 ? buffer.length : Math.min(buffer.length, length - read)));
958
+ if (count == -1) {
959
+ if (length < 0) break;
960
+ throw new EOFException("The response ended early");
961
+ }
962
+ try {
963
+ kept.write(read, buffer, count);
964
+ } catch (IOException e) {
965
+ // The temporary file can't be written (e.g. a full
966
+ // disk): the bytes go to memory.
967
+ if (!(kept instanceof FileStore)) throw e;
968
+ kept = inMemory(kept, read, length, e);
969
+ kept.write(read, buffer, count);
970
+ }
971
+ read += count;
972
+ report(progress, read, length);
973
+ }
974
+ } catch (IOException e) {
975
+ fail(e);
976
+ return;
977
+ } catch (RuntimeException | OutOfMemoryError e) {
978
+ // E.g. no memory for OkHttp's buffers: the requests fail, and
979
+ // the next ones start another download.
980
+ fail(new IOException(e));
981
+ return;
982
+ }
983
+ List<Waiter> waiters;
984
+ List<SharedStream> streams = new ArrayList<>();
985
+ Store dropped = null;
986
+ synchronized (downloads) {
987
+ synchronized (this) {
988
+ size = read;
989
+ ended = true;
990
+ silent = true;
991
+ waiters = takeWaiting();
992
+ readers += waiters.size();
993
+ for (int i = 0; i < waiters.size(); i++) streams.add(stream());
994
+ // Every request left as it ended.
995
+ if (readers == 0) {
996
+ unmap();
997
+ dropped = drop();
998
+ }
999
+ }
1000
+ }
1001
+ release(dropped);
1002
+ stopped();
1003
+ for (int i = 0; i < waiters.size(); i++) {
1004
+ waiters.get(i).fetcher.deliver(streams.get(i), waiters.get(i).callback);
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ // A request's stream on a download's bytes, all of them there. Closing it
1010
+ // holds no connection, so it can be done on the main thread.
1011
+ private static final class SharedStream extends InputStream {
1012
+ private final Download download;
1013
+ @Nullable
1014
+ private final Store store;
1015
+ private final long size;
1016
+ private final byte[] one = new byte[1];
1017
+ // Only used by the thread reading it.
1018
+ private long position = 0;
1019
+ private volatile boolean closed = false;
1020
+
1021
+ SharedStream(Download download, @Nullable Store store, long size) {
1022
+ this.download = download;
1023
+ this.store = store;
1024
+ this.size = size;
1025
+ }
1026
+
1027
+ @Override
1028
+ public int read() throws IOException {
1029
+ int count = read(one, 0, 1);
1030
+ return count == -1 ? -1 : one[0] & 0xff;
1031
+ }
1032
+
1033
+ @Override
1034
+ public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
1035
+ if (length == 0) return 0;
1036
+ if (closed) throw new IOException("Canceled");
1037
+ if (position >= size || store == null) return -1;
1038
+ int read = store.read(position, buffer, offset, (int) Math.min(length, size - position));
1039
+ position += read;
1040
+ return read;
1041
+ }
1042
+
1043
+ @Override
1044
+ public int available() {
1045
+ return closed ? 0 : (int) Math.min(Integer.MAX_VALUE, size - position);
1046
+ }
1047
+
1048
+ @Override
1049
+ public void close() {
1050
+ synchronized (this) {
1051
+ if (closed) return;
1052
+ closed = true;
1053
+ }
1054
+ download.readerClosed();
290
1055
  }
291
1056
  }
292
1057
  }