react-native-fast-image 9.0.3 → 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.
- package/README.md +14 -6
- package/android/build.gradle +12 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageGlide.java +7 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +23 -156
- package/android/src/main/java/com/dylanvann/fastimage/FastImageProgressListener.java +4 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +1 -4
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSharedDownloads.java +948 -196
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +128 -28
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +44 -22
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +85 -60
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.flow +4 -2
- package/dist/index.cjs.web.js +2 -2
- package/dist/index.cjs.web.js.flow +4 -2
- package/dist/index.d.ts +33 -7
- package/dist/index.js +2 -2
- package/dist/index.js.flow +4 -2
- package/dist/index.web.js +2 -2
- package/dist/index.web.js.flow +4 -2
- package/ios/FastImage/FFFastImageViewManager.m +2 -2
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
}
|
|
140
|
+
static {
|
|
141
|
+
reading.allowCoreThreadTimeOut(true);
|
|
142
|
+
}
|
|
74
143
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
144
|
+
private static final class Finished {
|
|
145
|
+
final File file;
|
|
146
|
+
final long at;
|
|
78
147
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
148
|
+
Finished(File file, long at) {
|
|
149
|
+
this.file = file;
|
|
150
|
+
this.at = at;
|
|
82
151
|
}
|
|
83
152
|
}
|
|
84
153
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
158
|
+
private FastImageSharedDownloads() {
|
|
159
|
+
}
|
|
96
160
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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).
|
|
186
299
|
@Nullable
|
|
187
|
-
|
|
188
|
-
//
|
|
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(
|
|
193
|
-
this.
|
|
194
|
-
this.
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
328
|
+
Download.join(this, callback);
|
|
247
329
|
}
|
|
248
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);
|
|
340
|
+
}
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
waitingFor.remove(waiting);
|
|
270
|
-
waiting.drop();
|
|
355
|
+
Download joined;
|
|
356
|
+
synchronized (downloads) {
|
|
357
|
+
joined = download;
|
|
271
358
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
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,678 @@ final class FastImageSharedDownloads {
|
|
|
281
367
|
return InputStream.class;
|
|
282
368
|
}
|
|
283
369
|
|
|
284
|
-
//
|
|
285
|
-
//
|
|
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.
|
|
286
374
|
@NonNull
|
|
287
375
|
@Override
|
|
288
376
|
public DataSource getDataSource() {
|
|
289
|
-
return
|
|
377
|
+
return fromFile ? DataSource.DATA_DISK_CACHE : DataSource.REMOTE;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// A request waiting for a download to end.
|
|
382
|
+
private static final class Waiter {
|
|
383
|
+
final Fetcher fetcher;
|
|
384
|
+
final DataFetcher.DataCallback<? super InputStream> callback;
|
|
385
|
+
// Its url's request (with its headers).
|
|
386
|
+
final Request request;
|
|
387
|
+
|
|
388
|
+
Waiter(Fetcher fetcher, DataFetcher.DataCallback<? super InputStream> callback, Request request) {
|
|
389
|
+
this.fetcher = fetcher;
|
|
390
|
+
this.callback = callback;
|
|
391
|
+
this.request = request;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Where a download keeps its response's bytes: written in order on its
|
|
396
|
+
// reading thread, then read by the requests, on theirs.
|
|
397
|
+
private interface Store {
|
|
398
|
+
void write(long at, byte[] buffer, int count) throws IOException;
|
|
399
|
+
|
|
400
|
+
// Reads up to `count` of the bytes, from `at`.
|
|
401
|
+
int read(long at, byte[] buffer, int offset, int count) throws IOException;
|
|
402
|
+
|
|
403
|
+
void release();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private static final class MemoryStore implements Store {
|
|
407
|
+
private final byte[] bytes;
|
|
408
|
+
|
|
409
|
+
MemoryStore(int length) {
|
|
410
|
+
bytes = new byte[length];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
@Override
|
|
414
|
+
public void write(long at, byte[] buffer, int count) {
|
|
415
|
+
System.arraycopy(buffer, 0, bytes, (int) at, count);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@Override
|
|
419
|
+
public int read(long at, byte[] buffer, int offset, int count) {
|
|
420
|
+
System.arraycopy(bytes, (int) at, buffer, offset, count);
|
|
421
|
+
return count;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
@Override
|
|
425
|
+
public void release() {
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// A temporary file, removed from its folder once open: its space is freed
|
|
430
|
+
// once it's closed, or the app ends, so nothing is left behind. Read and
|
|
431
|
+
// written with a RandomAccessFile, which (unlike a FileChannel) isn't
|
|
432
|
+
// closed for every thread when one reading it is interrupted.
|
|
433
|
+
private static final class FileStore implements Store {
|
|
434
|
+
private final RandomAccessFile file;
|
|
435
|
+
|
|
436
|
+
FileStore() throws IOException {
|
|
437
|
+
File folder = directory;
|
|
438
|
+
if (folder == null) throw new IOException("No folder for downloads");
|
|
439
|
+
if (!folder.isDirectory() && !folder.mkdirs() && !folder.isDirectory()) {
|
|
440
|
+
throw new IOException("Can't make " + folder);
|
|
441
|
+
}
|
|
442
|
+
File created = File.createTempFile("download", null, folder);
|
|
443
|
+
try {
|
|
444
|
+
file = new RandomAccessFile(created, "rw");
|
|
445
|
+
} finally {
|
|
446
|
+
//noinspection ResultOfMethodCallIgnored
|
|
447
|
+
created.delete();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
@Override
|
|
452
|
+
public synchronized void write(long at, byte[] buffer, int count) throws IOException {
|
|
453
|
+
file.seek(at);
|
|
454
|
+
file.write(buffer, 0, count);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
@Override
|
|
458
|
+
public synchronized int read(long at, byte[] buffer, int offset, int count) throws IOException {
|
|
459
|
+
file.seek(at);
|
|
460
|
+
int read = file.read(buffer, offset, count);
|
|
461
|
+
if (read == -1) throw new EOFException("The download's file ended early");
|
|
462
|
+
return read;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Not waiting for a read in progress (it fails): only the request
|
|
466
|
+
// whose stream was closed (it was cancelled) can still be reading.
|
|
467
|
+
@Override
|
|
468
|
+
public void release() {
|
|
469
|
+
try {
|
|
470
|
+
file.close();
|
|
471
|
+
} catch (IOException e) {
|
|
472
|
+
// Closed as far as it could.
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// The bytes in memory, for a response whose temporary file can't be used
|
|
478
|
+
// (see FALLBACK_MAX), growing as they arrive, with the memory reserved
|
|
479
|
+
// first.
|
|
480
|
+
private static final class GrowingStore implements Store {
|
|
481
|
+
private final Download download;
|
|
482
|
+
// Why the file couldn't be used.
|
|
483
|
+
private final IOException cause;
|
|
484
|
+
private byte[] bytes;
|
|
485
|
+
|
|
486
|
+
GrowingStore(Download download, byte[] bytes, IOException cause) {
|
|
487
|
+
this.download = download;
|
|
488
|
+
this.bytes = bytes;
|
|
489
|
+
this.cause = cause;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@Override
|
|
493
|
+
public void write(long at, byte[] buffer, int count) throws IOException {
|
|
494
|
+
long end = at + count;
|
|
495
|
+
if (end > bytes.length) {
|
|
496
|
+
if (end > FALLBACK_MAX) {
|
|
497
|
+
throw new IOException("The download is too large to keep in memory, and its temporary file couldn't be written", cause);
|
|
498
|
+
}
|
|
499
|
+
int length = (int) Math.min(FALLBACK_MAX, Math.max(end, 2L * bytes.length));
|
|
500
|
+
download.reserve(length - bytes.length, cause);
|
|
501
|
+
bytes = Arrays.copyOf(bytes, length);
|
|
502
|
+
}
|
|
503
|
+
System.arraycopy(buffer, 0, bytes, (int) at, count);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
@Override
|
|
507
|
+
public int read(long at, byte[] buffer, int offset, int count) {
|
|
508
|
+
System.arraycopy(bytes, (int) at, buffer, offset, count);
|
|
509
|
+
return count;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
@Override
|
|
513
|
+
public void release() {
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private static final class Download implements Callback {
|
|
518
|
+
private final String key;
|
|
519
|
+
// Guarded by downloads. The request it downloads (its url and
|
|
520
|
+
// headers): until it starts, the latest url a request for its key
|
|
521
|
+
// asked for (see join()).
|
|
522
|
+
private GlideUrl url;
|
|
523
|
+
private OkHttpClient client;
|
|
524
|
+
private Request request;
|
|
525
|
+
// Guarded by downloads. The urls requested for its key so far: its own
|
|
526
|
+
// once it starts, and those of the downloads it's a retry of (see
|
|
527
|
+
// fail()).
|
|
528
|
+
private final List<GlideUrl> tried = new ArrayList<>();
|
|
529
|
+
// Guarded by downloads. In the queue (most urgent first, then in
|
|
530
|
+
// order), or running.
|
|
531
|
+
private Priority priority;
|
|
532
|
+
private final long order;
|
|
533
|
+
private boolean queued = false;
|
|
534
|
+
private boolean running = false;
|
|
535
|
+
// Guarded by this. The call, once started, and whether the download
|
|
536
|
+
// was cancelled (before it started, it doesn't).
|
|
537
|
+
@Nullable
|
|
538
|
+
private Call call;
|
|
539
|
+
private boolean cancelled = false;
|
|
540
|
+
// Guarded by this. Requests waiting for it to end, and once it has,
|
|
541
|
+
// the streams open on its bytes.
|
|
542
|
+
private final List<Waiter> waiting = new ArrayList<>();
|
|
543
|
+
private int readers = 0;
|
|
544
|
+
// Guarded by this. Where the bytes are (null before the response is
|
|
545
|
+
// read, and once dropped), and how many there are.
|
|
546
|
+
@Nullable
|
|
547
|
+
private Store store;
|
|
548
|
+
private long size = 0;
|
|
549
|
+
// All of the response has been read, or the download failed.
|
|
550
|
+
private boolean ended = false;
|
|
551
|
+
// Guarded by downloads. The memory the bytes hold, and whether it sends
|
|
552
|
+
// no more progress: it ended, or was cancelled (it can still read
|
|
553
|
+
// bytes it had received).
|
|
554
|
+
private long reserved = 0;
|
|
555
|
+
private boolean silent = false;
|
|
556
|
+
|
|
557
|
+
// With downloads locked.
|
|
558
|
+
private Download(String key, GlideUrl url, OkHttpClient client, Request request, Priority priority) {
|
|
559
|
+
this.key = key;
|
|
560
|
+
this.url = url;
|
|
561
|
+
this.client = client;
|
|
562
|
+
this.request = request;
|
|
563
|
+
this.priority = priority;
|
|
564
|
+
this.order = orders++;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Joins the fetcher's key's download, or queues one of its url.
|
|
568
|
+
static void join(Fetcher fetcher, DataFetcher.DataCallback<? super InputStream> callback) {
|
|
569
|
+
// Its own request, which it sends if it starts the download.
|
|
570
|
+
Request request;
|
|
571
|
+
try {
|
|
572
|
+
request = request(fetcher.url);
|
|
573
|
+
} catch (IOException e) {
|
|
574
|
+
callback.onLoadFailed(e);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
Download download;
|
|
578
|
+
boolean queuedOne = false;
|
|
579
|
+
InputStream ready = null;
|
|
580
|
+
synchronized (downloads) {
|
|
581
|
+
// Cancelled before it got here.
|
|
582
|
+
if (fetcher.cancelled) return;
|
|
583
|
+
download = downloads.get(fetcher.key);
|
|
584
|
+
// A `web` image follows its HTTP cache: it only joins a
|
|
585
|
+
// download in progress, not the bytes of one that has ended.
|
|
586
|
+
if (download != null && fetcher.url instanceof FastImageWebGlideUrl && download.hasEnded()) {
|
|
587
|
+
download = null;
|
|
588
|
+
}
|
|
589
|
+
if (download == null) {
|
|
590
|
+
download = new Download(fetcher.key, fetcher.url, fetcher.client, request, fetcher.priority);
|
|
591
|
+
downloads.put(fetcher.key, download);
|
|
592
|
+
download.queued = true;
|
|
593
|
+
queue.add(download);
|
|
594
|
+
queuedOne = true;
|
|
595
|
+
} else if (download.queued) {
|
|
596
|
+
// A more urgent request moves it up (until it leaves).
|
|
597
|
+
if (fetcher.priority.compareTo(download.priority) < 0) download.reprioritize(fetcher.priority);
|
|
598
|
+
// Not started yet: another url for its key (a cacheKey's)
|
|
599
|
+
// is the latest one, e.g. a signed url with a fresh token,
|
|
600
|
+
// so it's the one requested (they're the same image).
|
|
601
|
+
if (!sameRequest(download.url, fetcher.url)) {
|
|
602
|
+
download.url = fetcher.url;
|
|
603
|
+
download.client = fetcher.client;
|
|
604
|
+
download.request = request;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
fetcher.download = download;
|
|
608
|
+
synchronized (download) {
|
|
609
|
+
// An ended download in the map has its bytes (a failed one
|
|
610
|
+
// leaves the map).
|
|
611
|
+
if (download.ended) {
|
|
612
|
+
download.readers++;
|
|
613
|
+
ready = download.stream();
|
|
614
|
+
} else {
|
|
615
|
+
download.waiting.add(new Waiter(fetcher, callback, request));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (ready != null) fetcher.deliver(ready, callback);
|
|
620
|
+
if (queuedOne) startQueued();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Sends the request (off the main thread, and outside the locks:
|
|
624
|
+
// creating the call runs the app's OkHttp EventListener).
|
|
625
|
+
private void start(Request request) {
|
|
626
|
+
Call created = client.newCall(request);
|
|
627
|
+
boolean go;
|
|
628
|
+
synchronized (this) {
|
|
629
|
+
go = !cancelled;
|
|
630
|
+
if (go) call = created;
|
|
631
|
+
}
|
|
632
|
+
if (go) {
|
|
633
|
+
created.enqueue(this);
|
|
634
|
+
} else {
|
|
635
|
+
stopped();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Cancels the call, or keeps it from starting. Outside the locks.
|
|
640
|
+
private void cancel() {
|
|
641
|
+
Call started;
|
|
642
|
+
synchronized (this) {
|
|
643
|
+
cancelled = true;
|
|
644
|
+
started = call;
|
|
645
|
+
}
|
|
646
|
+
if (started != null) started.cancel();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// It no longer runs: the next download waiting can start.
|
|
650
|
+
private void stopped() {
|
|
651
|
+
synchronized (downloads) {
|
|
652
|
+
if (!running) return;
|
|
653
|
+
running = false;
|
|
654
|
+
FastImageSharedDownloads.running--;
|
|
655
|
+
}
|
|
656
|
+
startQueued();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private synchronized boolean hasEnded() {
|
|
660
|
+
return ended;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// With downloads locked.
|
|
664
|
+
private boolean wasTried(GlideUrl other) {
|
|
665
|
+
for (GlideUrl requested : tried) {
|
|
666
|
+
if (sameRequest(requested, other)) return true;
|
|
667
|
+
}
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// With downloads locked, while it's queued.
|
|
672
|
+
private void reprioritize(Priority urgency) {
|
|
673
|
+
queue.remove(this);
|
|
674
|
+
priority = urgency;
|
|
675
|
+
queue.add(this);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// With this locked, once it has ended: a stream over all of its bytes.
|
|
679
|
+
private SharedStream stream() {
|
|
680
|
+
return new SharedStream(this, store, size);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// With downloads locked: new requests start another download.
|
|
684
|
+
private void unmap() {
|
|
685
|
+
if (downloads.get(key) == this) downloads.remove(key);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// With downloads and this locked: drops the bytes, and gives their
|
|
689
|
+
// memory back. Returns the store, to release outside the locks (closing
|
|
690
|
+
// a file can wait for the disk; this can be the main thread).
|
|
691
|
+
@Nullable
|
|
692
|
+
private Store drop() {
|
|
693
|
+
held -= reserved;
|
|
694
|
+
reserved = 0;
|
|
695
|
+
Store dropped = store;
|
|
696
|
+
store = null;
|
|
697
|
+
return dropped;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private static void release(@Nullable Store store) {
|
|
701
|
+
if (store == null) return;
|
|
702
|
+
store.release();
|
|
703
|
+
open.decrementAndGet();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// With this locked: the requests waiting for it to end.
|
|
707
|
+
private List<Waiter> takeWaiting() {
|
|
708
|
+
List<Waiter> taken = new ArrayList<>(waiting);
|
|
709
|
+
waiting.clear();
|
|
710
|
+
return taken;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// A request was cancelled (on the main thread). With no one left, the
|
|
714
|
+
// download is too (or, waiting to start, leaves the queue); waiting to
|
|
715
|
+
// start, it's as urgent as the most urgent request left, and of the
|
|
716
|
+
// latest url left (the url of a request that left isn't asked for).
|
|
717
|
+
void leave(Fetcher fetcher) {
|
|
718
|
+
boolean cancel = false;
|
|
719
|
+
Priority urgency = null;
|
|
720
|
+
Waiter latest = null;
|
|
721
|
+
synchronized (downloads) {
|
|
722
|
+
synchronized (this) {
|
|
723
|
+
for (int i = waiting.size() - 1; i >= 0; i--) {
|
|
724
|
+
if (waiting.get(i).fetcher == fetcher) waiting.remove(i);
|
|
725
|
+
}
|
|
726
|
+
if (!ended && waiting.isEmpty()) {
|
|
727
|
+
unmap();
|
|
728
|
+
silent = true;
|
|
729
|
+
cancel = true;
|
|
730
|
+
}
|
|
731
|
+
for (Waiter waiter : waiting) {
|
|
732
|
+
Priority asked = waiter.fetcher.priority;
|
|
733
|
+
if (urgency == null || asked.compareTo(urgency) < 0) urgency = asked;
|
|
734
|
+
latest = waiter;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (cancel && queued) {
|
|
738
|
+
queue.remove(this);
|
|
739
|
+
queued = false;
|
|
740
|
+
} else if (queued && urgency != null) {
|
|
741
|
+
if (urgency != priority) reprioritize(urgency);
|
|
742
|
+
if (!sameRequest(url, latest.fetcher.url)) {
|
|
743
|
+
url = latest.fetcher.url;
|
|
744
|
+
client = latest.fetcher.client;
|
|
745
|
+
request = latest.request;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (cancel) cancel();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// A stream on the bytes was closed: once every one has, the bytes go.
|
|
753
|
+
void readerClosed() {
|
|
754
|
+
Store dropped = null;
|
|
755
|
+
synchronized (downloads) {
|
|
756
|
+
synchronized (this) {
|
|
757
|
+
readers--;
|
|
758
|
+
if (readers > 0) return;
|
|
759
|
+
unmap();
|
|
760
|
+
dropped = drop();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
release(dropped);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
@Override
|
|
767
|
+
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
768
|
+
fail(e);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// The download failed: the requests waiting for it fail with the
|
|
772
|
+
// error, but those that asked for another url (a cacheKey's, e.g. a
|
|
773
|
+
// signed url with a fresh token) wait for a retry, of the latest of
|
|
774
|
+
// those urls (they're the same image), if it hasn't been tried for the
|
|
775
|
+
// key yet: this url can fail for reasons of its own, like an expired
|
|
776
|
+
// token. So a newer url that joins a retry gets a retry of its own,
|
|
777
|
+
// and an older one doesn't. As on iOS, where each url downloads on its
|
|
778
|
+
// own, an image gets onError when its own url fails.
|
|
779
|
+
private void fail(IOException e) {
|
|
780
|
+
List<Waiter> failed;
|
|
781
|
+
Store dropped;
|
|
782
|
+
Download again = null;
|
|
783
|
+
synchronized (downloads) {
|
|
784
|
+
synchronized (this) {
|
|
785
|
+
unmap();
|
|
786
|
+
silent = true;
|
|
787
|
+
ended = true;
|
|
788
|
+
failed = takeWaiting();
|
|
789
|
+
dropped = drop();
|
|
790
|
+
}
|
|
791
|
+
Waiter latest = null;
|
|
792
|
+
Priority urgency = null;
|
|
793
|
+
for (Waiter waiter : failed) {
|
|
794
|
+
if (waiter.fetcher.cancelled || sameRequest(waiter.fetcher.url, url)) continue;
|
|
795
|
+
latest = waiter;
|
|
796
|
+
Priority asked = waiter.fetcher.priority;
|
|
797
|
+
if (urgency == null || asked.compareTo(urgency) < 0) urgency = asked;
|
|
798
|
+
}
|
|
799
|
+
if (latest != null && !wasTried(latest.fetcher.url)) {
|
|
800
|
+
again = new Download(key, latest.fetcher.url, latest.fetcher.client, latest.request, urgency);
|
|
801
|
+
again.tried.addAll(tried);
|
|
802
|
+
List<Waiter> failing = new ArrayList<>();
|
|
803
|
+
synchronized (again) {
|
|
804
|
+
for (Waiter waiter : failed) {
|
|
805
|
+
if (waiter.fetcher.cancelled) continue;
|
|
806
|
+
if (sameRequest(waiter.fetcher.url, url)) {
|
|
807
|
+
failing.add(waiter);
|
|
808
|
+
} else {
|
|
809
|
+
waiter.fetcher.download = again;
|
|
810
|
+
again.waiting.add(waiter);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
failed = failing;
|
|
815
|
+
// Not yet in the map, so no one else has it. The requests
|
|
816
|
+
// for the other urls can have left meanwhile (a cancel
|
|
817
|
+
// marks its request before it takes the lock): no retry.
|
|
818
|
+
if (again.waiting.isEmpty()) {
|
|
819
|
+
again = null;
|
|
820
|
+
} else {
|
|
821
|
+
downloads.put(key, again);
|
|
822
|
+
again.queued = true;
|
|
823
|
+
queue.add(again);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
release(dropped);
|
|
828
|
+
stopped();
|
|
829
|
+
if (again != null) startQueued();
|
|
830
|
+
for (Waiter waiter : failed) waiter.callback.onLoadFailed(e);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
@Override
|
|
834
|
+
public void onResponse(@NonNull Call call, @NonNull Response response) {
|
|
835
|
+
final ResponseBody body = response.body();
|
|
836
|
+
if (!response.isSuccessful() || body == null) {
|
|
837
|
+
if (body != null) body.close();
|
|
838
|
+
fail(new HttpException(response.message(), response.code()));
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
final long length = body.contentLength();
|
|
842
|
+
synchronized (downloads) {
|
|
843
|
+
// No progress without a length.
|
|
844
|
+
if (length <= 0) silent = true;
|
|
845
|
+
}
|
|
846
|
+
reading.execute(new Runnable() {
|
|
847
|
+
@Override
|
|
848
|
+
public void run() {
|
|
849
|
+
readInto(body, length);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Sends a progress step, unless the download has stopped: checked and
|
|
855
|
+
// sent with downloads locked, which a cancel takes to stop it, so a new
|
|
856
|
+
// load of the key never gets a cancelled download's steps. Without an
|
|
857
|
+
// image with onProgress, nothing is locked.
|
|
858
|
+
private void report(Progress progress, long loaded, long total) {
|
|
859
|
+
if (!progress.step(loaded, total) || !FastImageViewManager.wantsProgress()) return;
|
|
860
|
+
synchronized (downloads) {
|
|
861
|
+
if (!silent) FastImageViewManager.onDownloadProgress(key, Math.min(loaded, total), total);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Memory for a response of known length up to MEMORY_MAX, if the
|
|
866
|
+
// budget has room for it (and there's the memory), otherwise a file.
|
|
867
|
+
private Store newStore(long length) throws IOException {
|
|
868
|
+
if (length >= 0 && length <= MEMORY_MAX) {
|
|
869
|
+
boolean fits;
|
|
870
|
+
synchronized (downloads) {
|
|
871
|
+
fits = held + length <= MEMORY_BUDGET;
|
|
872
|
+
if (fits) {
|
|
873
|
+
held += length;
|
|
874
|
+
reserved = length;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (fits) {
|
|
878
|
+
try {
|
|
879
|
+
MemoryStore created = new MemoryStore((int) length);
|
|
880
|
+
open.incrementAndGet();
|
|
881
|
+
return created;
|
|
882
|
+
} catch (OutOfMemoryError e) {
|
|
883
|
+
synchronized (downloads) {
|
|
884
|
+
held -= reserved;
|
|
885
|
+
reserved = 0;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
FileStore created = new FileStore();
|
|
892
|
+
open.incrementAndGet();
|
|
893
|
+
return created;
|
|
894
|
+
} catch (IOException e) {
|
|
895
|
+
return inMemory(null, 0, length, e);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Memory for `more` of its bytes, from MEMORY_BUDGET (see
|
|
900
|
+
// GrowingStore), given back when they're dropped; without room, the
|
|
901
|
+
// download fails with why its file couldn't be used.
|
|
902
|
+
private void reserve(long more, IOException cause) throws IOException {
|
|
903
|
+
synchronized (downloads) {
|
|
904
|
+
if (held + more > MEMORY_BUDGET) {
|
|
905
|
+
throw new IOException("No memory left for downloads, and the temporary file couldn't be written", cause);
|
|
906
|
+
}
|
|
907
|
+
held += more;
|
|
908
|
+
reserved += more;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Its temporary file couldn't be made, or written (`failed`): the
|
|
913
|
+
// bytes go to memory instead (see FALLBACK_MAX), with the `read` of
|
|
914
|
+
// them already in `file`.
|
|
915
|
+
private Store inMemory(@Nullable Store file, long read, long length, IOException failed) throws IOException {
|
|
916
|
+
if (length > FALLBACK_MAX) throw failed;
|
|
917
|
+
int capacity = (int) (length >= 0 ? length : Math.max(read, 64 * 1024));
|
|
918
|
+
reserve(capacity, failed);
|
|
919
|
+
byte[] bytes = new byte[capacity];
|
|
920
|
+
if (file != null) {
|
|
921
|
+
for (long at = 0; at < read; ) at += file.read(at, bytes, (int) at, (int) (read - at));
|
|
922
|
+
}
|
|
923
|
+
GrowingStore memory = new GrowingStore(this, bytes, failed);
|
|
924
|
+
open.incrementAndGet();
|
|
925
|
+
synchronized (this) {
|
|
926
|
+
store = memory;
|
|
927
|
+
}
|
|
928
|
+
release(file);
|
|
929
|
+
return memory;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Reads the response into its store, on a thread of `reading`, then
|
|
933
|
+
// gives the requests waiting a stream each.
|
|
934
|
+
private void readInto(ResponseBody body, long length) {
|
|
935
|
+
Progress progress = new Progress();
|
|
936
|
+
long read = 0;
|
|
937
|
+
try (BufferedSource source = body.source()) {
|
|
938
|
+
Store kept = newStore(length);
|
|
939
|
+
synchronized (this) {
|
|
940
|
+
store = kept;
|
|
941
|
+
}
|
|
942
|
+
byte[] buffer = new byte[8192];
|
|
943
|
+
while (length < 0 || read < length) {
|
|
944
|
+
int count = source.read(buffer, 0, (int) (length < 0 ? buffer.length : Math.min(buffer.length, length - read)));
|
|
945
|
+
if (count == -1) {
|
|
946
|
+
if (length < 0) break;
|
|
947
|
+
throw new EOFException("The response ended early");
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
kept.write(read, buffer, count);
|
|
951
|
+
} catch (IOException e) {
|
|
952
|
+
// The temporary file can't be written (e.g. a full
|
|
953
|
+
// disk): the bytes go to memory.
|
|
954
|
+
if (!(kept instanceof FileStore)) throw e;
|
|
955
|
+
kept = inMemory(kept, read, length, e);
|
|
956
|
+
kept.write(read, buffer, count);
|
|
957
|
+
}
|
|
958
|
+
read += count;
|
|
959
|
+
report(progress, read, length);
|
|
960
|
+
}
|
|
961
|
+
} catch (IOException e) {
|
|
962
|
+
fail(e);
|
|
963
|
+
return;
|
|
964
|
+
} catch (RuntimeException | OutOfMemoryError e) {
|
|
965
|
+
// E.g. no memory for OkHttp's buffers: the requests fail, and
|
|
966
|
+
// the next ones start another download.
|
|
967
|
+
fail(new IOException(e));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
List<Waiter> waiters;
|
|
971
|
+
List<SharedStream> streams = new ArrayList<>();
|
|
972
|
+
Store dropped = null;
|
|
973
|
+
synchronized (downloads) {
|
|
974
|
+
synchronized (this) {
|
|
975
|
+
size = read;
|
|
976
|
+
ended = true;
|
|
977
|
+
silent = true;
|
|
978
|
+
waiters = takeWaiting();
|
|
979
|
+
readers += waiters.size();
|
|
980
|
+
for (int i = 0; i < waiters.size(); i++) streams.add(stream());
|
|
981
|
+
// Every request left as it ended.
|
|
982
|
+
if (readers == 0) {
|
|
983
|
+
unmap();
|
|
984
|
+
dropped = drop();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
release(dropped);
|
|
989
|
+
stopped();
|
|
990
|
+
for (int i = 0; i < waiters.size(); i++) {
|
|
991
|
+
waiters.get(i).fetcher.deliver(streams.get(i), waiters.get(i).callback);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// A request's stream on a download's bytes, all of them there. Closing it
|
|
997
|
+
// holds no connection, so it can be done on the main thread.
|
|
998
|
+
private static final class SharedStream extends InputStream {
|
|
999
|
+
private final Download download;
|
|
1000
|
+
@Nullable
|
|
1001
|
+
private final Store store;
|
|
1002
|
+
private final long size;
|
|
1003
|
+
private final byte[] one = new byte[1];
|
|
1004
|
+
// Only used by the thread reading it.
|
|
1005
|
+
private long position = 0;
|
|
1006
|
+
private volatile boolean closed = false;
|
|
1007
|
+
|
|
1008
|
+
SharedStream(Download download, @Nullable Store store, long size) {
|
|
1009
|
+
this.download = download;
|
|
1010
|
+
this.store = store;
|
|
1011
|
+
this.size = size;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
@Override
|
|
1015
|
+
public int read() throws IOException {
|
|
1016
|
+
int count = read(one, 0, 1);
|
|
1017
|
+
return count == -1 ? -1 : one[0] & 0xff;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
@Override
|
|
1021
|
+
public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
|
|
1022
|
+
if (length == 0) return 0;
|
|
1023
|
+
if (closed) throw new IOException("Canceled");
|
|
1024
|
+
if (position >= size || store == null) return -1;
|
|
1025
|
+
int read = store.read(position, buffer, offset, (int) Math.min(length, size - position));
|
|
1026
|
+
position += read;
|
|
1027
|
+
return read;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
@Override
|
|
1031
|
+
public int available() {
|
|
1032
|
+
return closed ? 0 : (int) Math.min(Integer.MAX_VALUE, size - position);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
@Override
|
|
1036
|
+
public void close() {
|
|
1037
|
+
synchronized (this) {
|
|
1038
|
+
if (closed) return;
|
|
1039
|
+
closed = true;
|
|
1040
|
+
}
|
|
1041
|
+
download.readerClosed();
|
|
290
1042
|
}
|
|
291
1043
|
}
|
|
292
1044
|
}
|