@capgo/capacitor-downloader 8.3.0 → 8.3.2
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/android/build.gradle
CHANGED
|
@@ -40,6 +40,11 @@ android {
|
|
|
40
40
|
sourceCompatibility JavaVersion.VERSION_21
|
|
41
41
|
targetCompatibility JavaVersion.VERSION_21
|
|
42
42
|
}
|
|
43
|
+
testOptions {
|
|
44
|
+
unitTests {
|
|
45
|
+
returnDefaultValues = true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
repositories {
|
|
@@ -53,6 +58,7 @@ dependencies {
|
|
|
53
58
|
annotationProcessor project(':capacitor-android')
|
|
54
59
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
55
60
|
testImplementation "junit:junit:$junitVersion"
|
|
61
|
+
testImplementation "org.mockito:mockito-core:5.23.0"
|
|
56
62
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
57
63
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
58
64
|
}
|
package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/CapacitorDownloaderPlugin.java
CHANGED
|
@@ -16,23 +16,30 @@ import com.getcapacitor.PluginCall;
|
|
|
16
16
|
import com.getcapacitor.PluginMethod;
|
|
17
17
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
18
18
|
import java.io.File;
|
|
19
|
-
import java.util.HashMap;
|
|
20
19
|
import java.util.Iterator;
|
|
21
20
|
import java.util.Map;
|
|
21
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
22
|
+
import java.util.concurrent.ExecutorService;
|
|
23
|
+
import java.util.concurrent.Executors;
|
|
24
|
+
import java.util.concurrent.RejectedExecutionException;
|
|
25
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
22
26
|
|
|
23
27
|
@CapacitorPlugin(name = "CapacitorDownloader")
|
|
24
28
|
public class CapacitorDownloaderPlugin extends Plugin {
|
|
25
29
|
|
|
26
|
-
private final String pluginVersion = "8.3.
|
|
30
|
+
private final String pluginVersion = "8.3.2";
|
|
27
31
|
|
|
28
32
|
private DownloadManager downloadManager;
|
|
29
|
-
private final Map<String, Long> downloads = new
|
|
33
|
+
private final Map<String, Long> downloads = new ConcurrentHashMap<>();
|
|
34
|
+
private final AtomicLong pendingDownloadSequence = new AtomicLong(0);
|
|
35
|
+
private ExecutorService downloadManagerExecutor;
|
|
30
36
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
|
31
37
|
private BroadcastReceiver downloadReceiver;
|
|
32
38
|
|
|
33
39
|
@Override
|
|
34
40
|
public void load() {
|
|
35
41
|
downloadManager = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
|
|
42
|
+
downloadManagerExecutor = Executors.newCachedThreadPool();
|
|
36
43
|
registerDownloadReceiver();
|
|
37
44
|
}
|
|
38
45
|
|
|
@@ -57,13 +64,26 @@ public class CapacitorDownloaderPlugin extends Plugin {
|
|
|
57
64
|
|
|
58
65
|
private String getDownloadIdByValue(long value) {
|
|
59
66
|
for (Map.Entry<String, Long> entry : downloads.entrySet()) {
|
|
60
|
-
if (entry.getValue() == value) {
|
|
67
|
+
if (entry.getValue() == value && value > 0) {
|
|
61
68
|
return entry.getKey();
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
return null;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
private static boolean isPendingDownload(long downloadId) {
|
|
75
|
+
return downloadId < 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private long reservePendingDownload() {
|
|
79
|
+
return -pendingDownloadSequence.incrementAndGet();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private boolean isTrackedDownload(String id, long systemDownloadId) {
|
|
83
|
+
Long trackedDownloadId = downloads.get(id);
|
|
84
|
+
return trackedDownloadId != null && trackedDownloadId.longValue() == systemDownloadId;
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
@PluginMethod
|
|
68
88
|
public void download(PluginCall call) {
|
|
69
89
|
String id = call.getString("id");
|
|
@@ -100,79 +120,161 @@ public class CapacitorDownloaderPlugin extends Plugin {
|
|
|
100
120
|
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
|
|
101
121
|
}
|
|
102
122
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if ("hidden".equals(call.getString("notification"))) {
|
|
108
|
-
call.reject("Hidden downloads require android.permission.DOWNLOAD_WITHOUT_NOTIFICATION in the app manifest", e);
|
|
109
|
-
} else {
|
|
110
|
-
call.reject("Download could not be enqueued due to missing permission", e);
|
|
111
|
-
}
|
|
123
|
+
final String notification = call.getString("notification");
|
|
124
|
+
final long pendingToken = reservePendingDownload();
|
|
125
|
+
if (downloads.putIfAbsent(id, pendingToken) != null) {
|
|
126
|
+
call.reject("Download already exists");
|
|
112
127
|
return;
|
|
113
128
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
runDownloadManagerWork(
|
|
130
|
+
call,
|
|
131
|
+
() -> {
|
|
132
|
+
if (!isTrackedDownload(id, pendingToken)) {
|
|
133
|
+
call.reject("Download was cancelled");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
137
|
+
long downloadId;
|
|
138
|
+
try {
|
|
139
|
+
downloadId = downloadManager.enqueue(request);
|
|
140
|
+
} catch (SecurityException e) {
|
|
141
|
+
downloads.remove(id, pendingToken);
|
|
142
|
+
if ("hidden".equals(notification)) {
|
|
143
|
+
call.reject("Hidden downloads require android.permission.DOWNLOAD_WITHOUT_NOTIFICATION in the app manifest", e);
|
|
144
|
+
} else {
|
|
145
|
+
call.reject("Download could not be enqueued due to missing permission", e);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
} catch (RuntimeException e) {
|
|
149
|
+
downloads.remove(id, pendingToken);
|
|
150
|
+
call.reject("Download could not be enqueued", e);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
124
153
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
handler.postDelayed(this, 1000); // Check every second
|
|
154
|
+
if (!downloads.replace(id, pendingToken, downloadId)) {
|
|
155
|
+
try {
|
|
156
|
+
downloadManager.remove(downloadId);
|
|
157
|
+
} catch (RuntimeException e) {
|
|
158
|
+
call.reject("Download was cancelled", e);
|
|
159
|
+
return;
|
|
132
160
|
}
|
|
161
|
+
call.reject("Download was cancelled");
|
|
162
|
+
return;
|
|
133
163
|
}
|
|
134
|
-
|
|
164
|
+
|
|
165
|
+
JSObject result = new JSObject();
|
|
166
|
+
result.put("id", id);
|
|
167
|
+
result.put("status", DownloadManager.STATUS_PENDING);
|
|
168
|
+
call.resolve(result);
|
|
169
|
+
|
|
170
|
+
// Start a periodic progress check
|
|
171
|
+
startProgressCheck(id, downloadId);
|
|
172
|
+
},
|
|
173
|
+
() -> downloads.remove(id, pendingToken)
|
|
135
174
|
);
|
|
136
175
|
}
|
|
137
176
|
|
|
138
|
-
private
|
|
139
|
-
|
|
177
|
+
private void startProgressCheck(final String id, final long downloadId) {
|
|
178
|
+
runProgressQuery(id, true, downloadId);
|
|
179
|
+
}
|
|
140
180
|
|
|
141
|
-
|
|
142
|
-
|
|
181
|
+
private void checkDownloadStatus(String id) {
|
|
182
|
+
Long trackedDownloadId = downloads.get(id);
|
|
183
|
+
if (trackedDownloadId == null || isPendingDownload(trackedDownloadId)) {
|
|
184
|
+
return;
|
|
143
185
|
}
|
|
186
|
+
runProgressQuery(id, false, trackedDownloadId);
|
|
187
|
+
}
|
|
144
188
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
private void runProgressQuery(final String id, final boolean scheduleNext, final long systemDownloadId) {
|
|
190
|
+
if (!isTrackedDownload(id, systemDownloadId)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
ExecutorService executor = downloadManagerExecutor;
|
|
195
|
+
if (executor == null) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
executor.execute(() -> {
|
|
201
|
+
ProgressSnapshot snapshot = queryProgressSnapshot(id, systemDownloadId);
|
|
202
|
+
handler.post(() -> deliverProgressSnapshot(id, systemDownloadId, scheduleNext, snapshot));
|
|
203
|
+
});
|
|
204
|
+
} catch (RejectedExecutionException ignored) {
|
|
205
|
+
// Plugin is shutting down; stop polling quietly.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private ProgressSnapshot queryProgressSnapshot(String id, long systemDownloadId) {
|
|
210
|
+
if (!isTrackedDownload(id, systemDownloadId)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
DownloadManager.Query query = new DownloadManager.Query().setFilterById(systemDownloadId);
|
|
215
|
+
Cursor cursor = downloadManager.query(query);
|
|
216
|
+
if (cursor == null) {
|
|
217
|
+
return ProgressSnapshot.notFound(id);
|
|
218
|
+
}
|
|
219
|
+
try (cursor) {
|
|
220
|
+
if (!cursor.moveToFirst()) {
|
|
221
|
+
return ProgressSnapshot.notFound(id);
|
|
173
222
|
}
|
|
223
|
+
|
|
224
|
+
int status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
|
|
225
|
+
long bytesDownloaded = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
|
|
226
|
+
long bytesTotal = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
|
|
227
|
+
float progress = bytesTotal > 0 ? (float) bytesDownloaded / bytesTotal : 0f;
|
|
228
|
+
return new ProgressSnapshot(id, progress, status, true);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private void deliverProgressSnapshot(String id, long systemDownloadId, boolean scheduleNext, ProgressSnapshot snapshot) {
|
|
233
|
+
if (snapshot == null || !snapshot.found || !isTrackedDownload(id, systemDownloadId)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
JSObject progressData = new JSObject();
|
|
238
|
+
progressData.put("id", snapshot.id);
|
|
239
|
+
progressData.put("progress", snapshot.progress);
|
|
240
|
+
notifyListeners("downloadProgress", progressData);
|
|
241
|
+
|
|
242
|
+
boolean shouldContinue = true;
|
|
243
|
+
if (snapshot.status == DownloadManager.STATUS_SUCCESSFUL) {
|
|
244
|
+
JSObject completedData = new JSObject();
|
|
245
|
+
completedData.put("id", snapshot.id);
|
|
246
|
+
notifyListeners("downloadCompleted", completedData);
|
|
247
|
+
shouldContinue = false;
|
|
248
|
+
} else if (snapshot.status == DownloadManager.STATUS_FAILED) {
|
|
249
|
+
JSObject failedData = new JSObject();
|
|
250
|
+
failedData.put("id", snapshot.id);
|
|
251
|
+
failedData.put("error", "Download failed");
|
|
252
|
+
notifyListeners("downloadFailed", failedData);
|
|
253
|
+
shouldContinue = false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (scheduleNext && shouldContinue) {
|
|
257
|
+
handler.postDelayed(() -> runProgressQuery(id, true, systemDownloadId), 1000);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private static final class ProgressSnapshot {
|
|
262
|
+
|
|
263
|
+
private final String id;
|
|
264
|
+
private final float progress;
|
|
265
|
+
private final int status;
|
|
266
|
+
private final boolean found;
|
|
267
|
+
|
|
268
|
+
private ProgressSnapshot(String id, float progress, int status, boolean found) {
|
|
269
|
+
this.id = id;
|
|
270
|
+
this.progress = progress;
|
|
271
|
+
this.status = status;
|
|
272
|
+
this.found = found;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private static ProgressSnapshot notFound(String id) {
|
|
276
|
+
return new ProgressSnapshot(id, 0f, -1, false);
|
|
174
277
|
}
|
|
175
|
-
return true; // Continue checking progress
|
|
176
278
|
}
|
|
177
279
|
|
|
178
280
|
@PluginMethod
|
|
@@ -190,31 +292,96 @@ public class CapacitorDownloaderPlugin extends Plugin {
|
|
|
190
292
|
@PluginMethod
|
|
191
293
|
public void stop(PluginCall call) {
|
|
192
294
|
String id = call.getString("id");
|
|
193
|
-
if (id == null
|
|
295
|
+
if (id == null) {
|
|
296
|
+
call.reject("Download not found");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
Long downloadId = downloads.remove(id);
|
|
301
|
+
if (downloadId == null) {
|
|
194
302
|
call.reject("Download not found");
|
|
195
303
|
return;
|
|
196
304
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
305
|
+
|
|
306
|
+
if (isPendingDownload(downloadId)) {
|
|
307
|
+
call.resolve(new JSObject().put("removed", false));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
final long systemDownloadId = downloadId;
|
|
312
|
+
final String stoppedId = id;
|
|
313
|
+
runDownloadManagerWork(
|
|
314
|
+
call,
|
|
315
|
+
() -> {
|
|
316
|
+
try {
|
|
317
|
+
int removedDownloads = downloadManager.remove(systemDownloadId);
|
|
318
|
+
call.resolve(new JSObject().put("removed", removedDownloads > 0));
|
|
319
|
+
} catch (RuntimeException e) {
|
|
320
|
+
downloads.putIfAbsent(stoppedId, systemDownloadId);
|
|
321
|
+
call.reject("Download could not be removed", e);
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
() -> downloads.putIfAbsent(stoppedId, systemDownloadId)
|
|
325
|
+
);
|
|
200
326
|
}
|
|
201
327
|
|
|
202
328
|
@PluginMethod
|
|
203
329
|
public void checkStatus(PluginCall call) {
|
|
204
330
|
String id = call.getString("id");
|
|
205
|
-
if (id == null
|
|
331
|
+
if (id == null) {
|
|
206
332
|
call.reject("Download not found");
|
|
207
333
|
return;
|
|
208
334
|
}
|
|
209
335
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
336
|
+
Long trackedDownloadId = downloads.get(id);
|
|
337
|
+
if (trackedDownloadId == null || isPendingDownload(trackedDownloadId)) {
|
|
338
|
+
call.reject("Download not found");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
final long downloadId = trackedDownloadId;
|
|
343
|
+
runDownloadManagerWork(call, () -> {
|
|
344
|
+
DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
|
|
345
|
+
try {
|
|
346
|
+
Cursor cursor = downloadManager.query(query);
|
|
347
|
+
if (cursor == null) {
|
|
348
|
+
call.reject("Download not found");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
try (cursor) {
|
|
352
|
+
if (cursor.moveToFirst()) {
|
|
353
|
+
call.resolve(getDownloadStatus(cursor));
|
|
354
|
+
} else {
|
|
355
|
+
call.reject("Download not found");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch (RuntimeException e) {
|
|
359
|
+
call.reject("Download status could not be read", e);
|
|
217
360
|
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private void runDownloadManagerWork(PluginCall call, Runnable work) {
|
|
365
|
+
runDownloadManagerWork(call, work, null);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private void runDownloadManagerWork(PluginCall call, Runnable work, Runnable onFailure) {
|
|
369
|
+
ExecutorService executor = downloadManagerExecutor;
|
|
370
|
+
if (executor == null) {
|
|
371
|
+
if (onFailure != null) {
|
|
372
|
+
onFailure.run();
|
|
373
|
+
}
|
|
374
|
+
call.reject("Download manager is not available");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
executor.execute(work);
|
|
380
|
+
} catch (RejectedExecutionException e) {
|
|
381
|
+
if (onFailure != null) {
|
|
382
|
+
onFailure.run();
|
|
383
|
+
}
|
|
384
|
+
call.reject("Download manager is shutting down", e);
|
|
218
385
|
}
|
|
219
386
|
}
|
|
220
387
|
|
|
@@ -284,6 +451,10 @@ public class CapacitorDownloaderPlugin extends Plugin {
|
|
|
284
451
|
if (downloadReceiver != null) {
|
|
285
452
|
getContext().unregisterReceiver(downloadReceiver);
|
|
286
453
|
}
|
|
454
|
+
if (downloadManagerExecutor != null) {
|
|
455
|
+
downloadManagerExecutor.shutdown();
|
|
456
|
+
downloadManagerExecutor = null;
|
|
457
|
+
}
|
|
287
458
|
}
|
|
288
459
|
|
|
289
460
|
@PluginMethod
|
|
@@ -8,7 +8,7 @@ import Capacitor
|
|
|
8
8
|
|
|
9
9
|
@objc(CapacitorDownloaderPlugin)
|
|
10
10
|
public class CapacitorDownloaderPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
-
private let pluginVersion: String = "8.3.
|
|
11
|
+
private let pluginVersion: String = "8.3.2"
|
|
12
12
|
public let identifier = "CapacitorDownloaderPlugin"
|
|
13
13
|
public let jsName = "CapacitorDownloader"
|
|
14
14
|
public let pluginMethods: [CAPPluginMethod] = [
|