@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.
@@ -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
  }
@@ -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.0";
30
+ private final String pluginVersion = "8.3.2";
27
31
 
28
32
  private DownloadManager downloadManager;
29
- private final Map<String, Long> downloads = new HashMap<>();
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
- long downloadId;
104
- try {
105
- downloadId = downloadManager.enqueue(request);
106
- } catch (SecurityException e) {
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
- downloads.put(id, downloadId);
115
-
116
- JSObject result = new JSObject();
117
- result.put("id", id);
118
- result.put("status", DownloadManager.STATUS_PENDING);
119
- call.resolve(result);
129
+ runDownloadManagerWork(
130
+ call,
131
+ () -> {
132
+ if (!isTrackedDownload(id, pendingToken)) {
133
+ call.reject("Download was cancelled");
134
+ return;
135
+ }
120
136
 
121
- // Start a periodic progress check
122
- startProgressCheck(id, downloadId);
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
- private void startProgressCheck(final String id, final long downloadId) {
126
- handler.post(
127
- new Runnable() {
128
- @Override
129
- public void run() {
130
- if (checkDownloadStatus(id)) {
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 boolean checkDownloadStatus(String id) {
139
- Long downloadId = downloads.get(id);
177
+ private void startProgressCheck(final String id, final long downloadId) {
178
+ runProgressQuery(id, true, downloadId);
179
+ }
140
180
 
141
- if (downloadId == null) {
142
- return false; // Download was removed, stop polling
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
- DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
146
- try (Cursor cursor = downloadManager.query(query)) {
147
- if (cursor.moveToFirst()) {
148
- int status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
149
- long bytesDownloaded = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
150
- long bytesTotal = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
151
-
152
- float progress = bytesTotal > 0 ? (float) bytesDownloaded / bytesTotal : 0f;
153
-
154
- JSObject progressData = new JSObject();
155
- progressData.put("id", id);
156
- progressData.put("progress", progress);
157
- notifyListeners("downloadProgress", progressData);
158
-
159
- if (status == DownloadManager.STATUS_SUCCESSFUL) {
160
- JSObject completedData = new JSObject();
161
- completedData.put("id", id);
162
- notifyListeners("downloadCompleted", completedData);
163
- return false; // Stop checking progress
164
- } else if (status == DownloadManager.STATUS_FAILED) {
165
- JSObject failedData = new JSObject();
166
- failedData.put("id", id);
167
- failedData.put("error", "Download failed");
168
- notifyListeners("downloadFailed", failedData);
169
- return false; // Stop checking progress
170
- }
171
- } else {
172
- return false; // Download no longer in DownloadManager, stop polling
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 || !downloads.containsKey(id)) {
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
- int removedDownloads = downloadManager.remove(downloads.get(id));
198
- downloads.remove(id);
199
- call.resolve(new JSObject().put("removed", removedDownloads > 0));
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 || !downloads.containsKey(id)) {
331
+ if (id == null) {
206
332
  call.reject("Download not found");
207
333
  return;
208
334
  }
209
335
 
210
- DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloads.get(id));
211
- try (Cursor cursor = downloadManager.query(query)) {
212
- if (cursor.moveToFirst()) {
213
- JSObject result = getDownloadStatus(cursor);
214
- call.resolve(result);
215
- } else {
216
- call.reject("Download not found");
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.0"
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] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-downloader",
3
- "version": "8.3.0",
3
+ "version": "8.3.2",
4
4
  "description": "Download file in background or foreground",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",