@capgo/capacitor-downloader 8.3.21 → 8.5.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.
Files changed (22) hide show
  1. package/README.md +66 -10
  2. package/android/build.gradle +3 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/AndroidDownloadDestination.java +143 -0
  5. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/AndroidHttpEngineSelector.java +33 -0
  6. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/CapacitorDownloaderPlugin.java +357 -27
  7. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletedNotificationPresenter.java +91 -0
  8. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletedNotificationStore.java +138 -0
  9. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletedNotificationText.java +14 -0
  10. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletionNotifier.java +70 -0
  11. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/OkHttpDownloadExecutor.java +420 -0
  12. package/dist/docs.json +27 -3
  13. package/dist/esm/definitions.d.ts +51 -5
  14. package/dist/esm/definitions.js.map +1 -1
  15. package/ios/Sources/CapacitorDownloaderPlugin/CapacitorDownloaderPlugin.swift +86 -6
  16. package/ios/Sources/CapacitorDownloaderPlugin/DownloadCompletedNotificationPresenter.swift +38 -0
  17. package/ios/Sources/CapacitorDownloaderPlugin/DownloadCompletedNotificationStore.swift +74 -0
  18. package/ios/Sources/CapacitorDownloaderPlugin/DownloadCompletedNotificationText.swift +11 -0
  19. package/ios/Sources/CapacitorDownloaderPlugin/DownloadIdGenerationStore.swift +39 -0
  20. package/ios/Sources/CapacitorDownloaderPlugin/DownloadTaskGenerationStore.swift +47 -0
  21. package/ios/Tests/CapacitorDownloaderPluginTests/CapacitorDownloaderPluginTests.swift +121 -0
  22. package/package.json +1 -1
@@ -27,19 +27,25 @@ import java.util.concurrent.atomic.AtomicLong;
27
27
  @CapacitorPlugin(name = "CapacitorDownloader")
28
28
  public class CapacitorDownloaderPlugin extends Plugin {
29
29
 
30
- private final String pluginVersion = "8.3.21";
30
+ private final String pluginVersion = "8.5.0";
31
31
 
32
32
  private DownloadManager downloadManager;
33
33
  private final Map<String, Long> downloads = new ConcurrentHashMap<>();
34
+ private final Map<String, File> okHttpDestinationFiles = new ConcurrentHashMap<>();
35
+ private final Map<String, Long> downloadGenerations = new ConcurrentHashMap<>();
34
36
  private final AtomicLong pendingDownloadSequence = new AtomicLong(0);
35
37
  private ExecutorService downloadManagerExecutor;
38
+ private OkHttpDownloadExecutor okHttpDownloadExecutor;
36
39
  private final Handler handler = new Handler(Looper.getMainLooper());
37
40
  private BroadcastReceiver downloadReceiver;
41
+ private DownloadCompletedNotificationStore completedNotificationStore;
38
42
 
39
43
  @Override
40
44
  public void load() {
41
45
  downloadManager = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
42
46
  downloadManagerExecutor = Executors.newCachedThreadPool();
47
+ okHttpDownloadExecutor = new OkHttpDownloadExecutor(getContext());
48
+ completedNotificationStore = new DownloadCompletedNotificationStore(getContext());
43
49
  registerDownloadReceiver();
44
50
  }
45
51
 
@@ -72,11 +78,15 @@ public class CapacitorDownloaderPlugin extends Plugin {
72
78
  }
73
79
 
74
80
  private static boolean isPendingDownload(long downloadId) {
75
- return downloadId < 0;
81
+ return downloadId < 0 && downloadId != OkHttpDownloadExecutor.TRACKED_DOWNLOAD_ID;
76
82
  }
77
83
 
78
84
  private long reservePendingDownload() {
79
- return -pendingDownloadSequence.incrementAndGet();
85
+ long token;
86
+ do {
87
+ token = -pendingDownloadSequence.incrementAndGet();
88
+ } while (token == OkHttpDownloadExecutor.TRACKED_DOWNLOAD_ID);
89
+ return token;
80
90
  }
81
91
 
82
92
  private boolean isTrackedDownload(String id, long systemDownloadId) {
@@ -84,6 +94,44 @@ public class CapacitorDownloaderPlugin extends Plugin {
84
94
  return trackedDownloadId != null && trackedDownloadId.longValue() == systemDownloadId;
85
95
  }
86
96
 
97
+ private boolean isCurrentOkHttpDownload(String id) {
98
+ Long trackedDownloadId = downloads.get(id);
99
+ return trackedDownloadId != null && isOkHttpDownload(trackedDownloadId);
100
+ }
101
+
102
+ private long nextGeneration(String id) {
103
+ while (true) {
104
+ Long current = downloadGenerations.get(id);
105
+ long next = (current == null ? 0L : current.longValue()) + 1L;
106
+ if (current == null) {
107
+ if (downloadGenerations.putIfAbsent(id, next) == null) {
108
+ return next;
109
+ }
110
+ } else if (downloadGenerations.replace(id, current, next)) {
111
+ return next;
112
+ }
113
+ }
114
+ }
115
+
116
+ private Long generationFor(String id) {
117
+ return downloadGenerations.get(id);
118
+ }
119
+
120
+ private static boolean usesExtendedDestinationRoots(String androidHttpEngine) {
121
+ if (androidHttpEngine == null || androidHttpEngine.isEmpty()) {
122
+ return false;
123
+ }
124
+ if ("downloadManager".equalsIgnoreCase(androidHttpEngine) || "download_manager".equalsIgnoreCase(androidHttpEngine)) {
125
+ return false;
126
+ }
127
+ return true;
128
+ }
129
+
130
+ private boolean isCurrentOkHttpGeneration(String id, long expectedGeneration) {
131
+ Long current = generationFor(id);
132
+ return current != null && current.longValue() == expectedGeneration;
133
+ }
134
+
87
135
  @PluginMethod
88
136
  public void download(PluginCall call) {
89
137
  String id = call.getString("id");
@@ -95,41 +143,115 @@ public class CapacitorDownloaderPlugin extends Plugin {
95
143
  return;
96
144
  }
97
145
 
98
- DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url))
99
- .setNotificationVisibility(DownloadNotificationVisibility.resolve(call.getString("notification")))
100
- .setAllowedOverMetered(true)
101
- .setAllowedOverRoaming(true);
146
+ if (downloads.containsKey(id)) {
147
+ call.reject("Download already exists");
148
+ return;
149
+ }
102
150
 
103
- // Handle custom destination
104
- File destinationFile = new File(getContext().getExternalFilesDir(null), destination);
105
- Uri destinationUri = Uri.fromFile(destinationFile);
106
- request.setDestinationUri(destinationUri);
151
+ final boolean extendedDestinationRoots = usesExtendedDestinationRoots(call.getString("androidHttpEngine"));
107
152
 
108
- JSObject headers = call.getObject("headers");
109
- if (headers != null) {
110
- for (Iterator<String> it = headers.keys(); it.hasNext(); ) {
111
- String key = it.next();
112
- request.addRequestHeader(key, headers.getString(key));
113
- }
153
+ final AndroidDownloadDestination.ResolvedDestination resolvedDestination;
154
+ try {
155
+ resolvedDestination = AndroidDownloadDestination.resolve(getContext(), destination, extendedDestinationRoots);
156
+ } catch (IllegalArgumentException e) {
157
+ call.reject(e.getMessage(), e);
158
+ return;
159
+ }
160
+
161
+ final AndroidHttpEngineSelector.Engine httpEngine;
162
+ try {
163
+ httpEngine = AndroidHttpEngineSelector.resolve(call.getString("androidHttpEngine"), resolvedDestination.preferOkHttpInAutoMode);
164
+ } catch (IllegalArgumentException e) {
165
+ call.reject(e.getMessage(), e);
166
+ return;
114
167
  }
115
168
 
116
- String network = call.getString("network");
117
- if ("wifi-only".equals(network)) {
118
- request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
119
- } else {
120
- request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
169
+ if (
170
+ httpEngine == AndroidHttpEngineSelector.Engine.OKHTTP &&
171
+ extendedDestinationRoots &&
172
+ destination.trim().startsWith("public-downloads/")
173
+ ) {
174
+ call.reject("OkHttp cannot write to public-downloads/ paths. Use androidHttpEngine 'auto' or 'downloadManager'.");
175
+ return;
176
+ }
177
+
178
+ final String completionNotificationText = call.getString("downloadCompletedNotificationText");
179
+
180
+ if (httpEngine == AndroidHttpEngineSelector.Engine.OKHTTP) {
181
+ downloadWithOkHttp(
182
+ call,
183
+ id,
184
+ url,
185
+ resolvedDestination.file,
186
+ call.getObject("headers"),
187
+ call.getString("network"),
188
+ call.getString("notification"),
189
+ completionNotificationText
190
+ );
191
+ return;
192
+ }
193
+
194
+ downloadWithDownloadManager(
195
+ call,
196
+ id,
197
+ url,
198
+ resolvedDestination.file,
199
+ call.getObject("headers"),
200
+ call.getString("network"),
201
+ call.getString("notification"),
202
+ completionNotificationText
203
+ );
204
+ }
205
+
206
+ private void downloadWithDownloadManager(
207
+ PluginCall call,
208
+ String id,
209
+ String url,
210
+ File destinationFile,
211
+ JSObject headers,
212
+ String network,
213
+ String notification,
214
+ String completionNotificationText
215
+ ) {
216
+ final DownloadManager.Request request;
217
+ try {
218
+ request = new DownloadManager.Request(Uri.parse(url))
219
+ .setNotificationVisibility(DownloadNotificationVisibility.resolve(notification))
220
+ .setAllowedOverMetered(true)
221
+ .setAllowedOverRoaming(true);
222
+
223
+ Uri destinationUri = Uri.fromFile(destinationFile);
224
+ request.setDestinationUri(destinationUri);
225
+
226
+ if (headers != null) {
227
+ for (Iterator<String> it = headers.keys(); it.hasNext(); ) {
228
+ String key = it.next();
229
+ request.addRequestHeader(key, headers.getString(key));
230
+ }
231
+ }
232
+
233
+ if ("wifi-only".equals(network)) {
234
+ request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
235
+ } else {
236
+ request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
237
+ }
238
+ } catch (RuntimeException e) {
239
+ call.reject("Invalid download request", e);
240
+ return;
121
241
  }
122
242
 
123
- final String notification = call.getString("notification");
124
243
  final long pendingToken = reservePendingDownload();
125
244
  if (downloads.putIfAbsent(id, pendingToken) != null) {
126
245
  call.reject("Download already exists");
127
246
  return;
128
247
  }
248
+ final long downloadGeneration = nextGeneration(id);
249
+ completedNotificationStore.setText(id, completionNotificationText, downloadGeneration);
129
250
  runDownloadManagerWork(
130
251
  call,
131
252
  () -> {
132
253
  if (!isTrackedDownload(id, pendingToken)) {
254
+ completedNotificationStore.remove(id, downloadGeneration);
133
255
  call.reject("Download was cancelled");
134
256
  return;
135
257
  }
@@ -139,6 +261,7 @@ public class CapacitorDownloaderPlugin extends Plugin {
139
261
  downloadId = downloadManager.enqueue(request);
140
262
  } catch (SecurityException e) {
141
263
  downloads.remove(id, pendingToken);
264
+ completedNotificationStore.remove(id, downloadGeneration);
142
265
  if ("hidden".equals(notification)) {
143
266
  call.reject("Hidden downloads require android.permission.DOWNLOAD_WITHOUT_NOTIFICATION in the app manifest", e);
144
267
  } else {
@@ -147,6 +270,7 @@ public class CapacitorDownloaderPlugin extends Plugin {
147
270
  return;
148
271
  } catch (RuntimeException e) {
149
272
  downloads.remove(id, pendingToken);
273
+ completedNotificationStore.remove(id, downloadGeneration);
150
274
  call.reject("Download could not be enqueued", e);
151
275
  return;
152
276
  }
@@ -155,9 +279,11 @@ public class CapacitorDownloaderPlugin extends Plugin {
155
279
  try {
156
280
  downloadManager.remove(downloadId);
157
281
  } catch (RuntimeException e) {
282
+ completedNotificationStore.remove(id, downloadGeneration);
158
283
  call.reject("Download was cancelled", e);
159
284
  return;
160
285
  }
286
+ completedNotificationStore.remove(id, downloadGeneration);
161
287
  call.reject("Download was cancelled");
162
288
  return;
163
289
  }
@@ -167,13 +293,146 @@ public class CapacitorDownloaderPlugin extends Plugin {
167
293
  result.put("status", DownloadManager.STATUS_PENDING);
168
294
  call.resolve(result);
169
295
 
170
- // Start a periodic progress check
171
296
  startProgressCheck(id, downloadId);
172
297
  },
173
- () -> downloads.remove(id, pendingToken)
298
+ () -> {
299
+ downloads.remove(id, pendingToken);
300
+ completedNotificationStore.remove(id, downloadGeneration);
301
+ }
174
302
  );
175
303
  }
176
304
 
305
+ private void downloadWithOkHttp(
306
+ PluginCall call,
307
+ String id,
308
+ String url,
309
+ File destinationFile,
310
+ JSObject headers,
311
+ String network,
312
+ String notification,
313
+ String completionNotificationText
314
+ ) {
315
+ if (notification == null) {
316
+ notification = "completed";
317
+ }
318
+
319
+ final long pendingToken = reservePendingDownload();
320
+ if (downloads.putIfAbsent(id, pendingToken) != null) {
321
+ call.reject("Download already exists");
322
+ return;
323
+ }
324
+ okHttpDestinationFiles.put(id, destinationFile);
325
+ final long downloadGeneration = nextGeneration(id);
326
+ completedNotificationStore.setText(id, completionNotificationText, downloadGeneration);
327
+
328
+ final String notificationMode = notification;
329
+ final long storedGeneration = downloadGeneration;
330
+ runDownloadManagerWork(
331
+ call,
332
+ () -> {
333
+ if (!isTrackedDownload(id, pendingToken)) {
334
+ okHttpDestinationFiles.remove(id);
335
+ completedNotificationStore.remove(id, storedGeneration);
336
+ call.reject("Download was cancelled");
337
+ return;
338
+ }
339
+
340
+ if (!okHttpDownloadExecutor.isWifiSatisfied(network)) {
341
+ downloads.remove(id, pendingToken);
342
+ okHttpDestinationFiles.remove(id);
343
+ completedNotificationStore.remove(id, storedGeneration);
344
+ call.reject("Wi-Fi required for this download");
345
+ return;
346
+ }
347
+
348
+ if (!okHttpDownloadExecutor.beginTransfer(id)) {
349
+ downloads.remove(id, pendingToken);
350
+ okHttpDestinationFiles.remove(id);
351
+ completedNotificationStore.remove(id, storedGeneration);
352
+ call.reject("Download already exists");
353
+ return;
354
+ }
355
+
356
+ if (!downloads.replace(id, pendingToken, OkHttpDownloadExecutor.TRACKED_DOWNLOAD_ID)) {
357
+ okHttpDownloadExecutor.abandonTransfer(id);
358
+ okHttpDestinationFiles.remove(id);
359
+ completedNotificationStore.remove(id, storedGeneration);
360
+ call.reject("Download was cancelled");
361
+ return;
362
+ }
363
+
364
+ JSObject result = new JSObject();
365
+ result.put("id", id);
366
+ result.put("status", DownloadManager.STATUS_PENDING);
367
+ call.resolve(result);
368
+
369
+ try {
370
+ okHttpDownloadExecutor.start(
371
+ id,
372
+ url,
373
+ destinationFile,
374
+ headers,
375
+ network,
376
+ (downloadId, progress) ->
377
+ handler.post(() -> {
378
+ if (!isCurrentOkHttpDownload(downloadId) || !isCurrentOkHttpGeneration(downloadId, storedGeneration)) {
379
+ return;
380
+ }
381
+ JSObject progressData = new JSObject();
382
+ progressData.put("id", downloadId);
383
+ progressData.put("progress", progress);
384
+ notifyListeners("downloadProgress", progressData);
385
+ }),
386
+ (downloadId, success, cancelled, errorMessage) ->
387
+ handler.post(() -> {
388
+ if (!isCurrentOkHttpDownload(downloadId) || !isCurrentOkHttpGeneration(downloadId, storedGeneration)) {
389
+ return;
390
+ }
391
+ if (cancelled) {
392
+ return;
393
+ }
394
+ if (success) {
395
+ JSObject completedData = new JSObject();
396
+ completedData.put("id", downloadId);
397
+ notifyListeners("downloadCompleted", completedData);
398
+ DownloadCompletionNotifier.notifyIfRequested(getContext(), downloadId, notificationMode, true);
399
+ String completionText = completedNotificationStore.takeText(downloadId, storedGeneration);
400
+ if (completionText != null) {
401
+ DownloadCompletedNotificationPresenter.showIfAllowed(getContext(), downloadId, completionText);
402
+ }
403
+ } else {
404
+ okHttpDestinationFiles.remove(downloadId, destinationFile);
405
+ completedNotificationStore.remove(downloadId, storedGeneration);
406
+ JSObject failedData = new JSObject();
407
+ failedData.put("id", downloadId);
408
+ failedData.put("error", errorMessage != null ? errorMessage : "Download failed");
409
+ notifyListeners("downloadFailed", failedData);
410
+ }
411
+ })
412
+ );
413
+ } catch (RuntimeException e) {
414
+ okHttpDownloadExecutor.abandonTransfer(id);
415
+ downloads.remove(id);
416
+ okHttpDestinationFiles.remove(id);
417
+ completedNotificationStore.remove(id, storedGeneration);
418
+ JSObject failedData = new JSObject();
419
+ failedData.put("id", id);
420
+ failedData.put("error", e.getMessage());
421
+ notifyListeners("downloadFailed", failedData);
422
+ }
423
+ },
424
+ () -> {
425
+ downloads.remove(id, pendingToken);
426
+ okHttpDestinationFiles.remove(id);
427
+ completedNotificationStore.remove(id, storedGeneration);
428
+ }
429
+ );
430
+ }
431
+
432
+ private boolean isOkHttpDownload(long trackedDownloadId) {
433
+ return trackedDownloadId == OkHttpDownloadExecutor.TRACKED_DOWNLOAD_ID;
434
+ }
435
+
177
436
  private void startProgressCheck(final String id, final long downloadId) {
178
437
  runProgressQuery(id, true, downloadId);
179
438
  }
@@ -196,10 +455,12 @@ public class CapacitorDownloaderPlugin extends Plugin {
196
455
  return;
197
456
  }
198
457
 
458
+ final Long capturedGeneration = generationFor(id);
459
+
199
460
  try {
200
461
  executor.execute(() -> {
201
462
  ProgressSnapshot snapshot = queryProgressSnapshot(id, systemDownloadId);
202
- handler.post(() -> deliverProgressSnapshot(id, systemDownloadId, scheduleNext, snapshot));
463
+ handler.post(() -> deliverProgressSnapshot(id, systemDownloadId, scheduleNext, snapshot, capturedGeneration));
203
464
  });
204
465
  } catch (RejectedExecutionException ignored) {
205
466
  // Plugin is shutting down; stop polling quietly.
@@ -229,7 +490,13 @@ public class CapacitorDownloaderPlugin extends Plugin {
229
490
  }
230
491
  }
231
492
 
232
- private void deliverProgressSnapshot(String id, long systemDownloadId, boolean scheduleNext, ProgressSnapshot snapshot) {
493
+ private void deliverProgressSnapshot(
494
+ String id,
495
+ long systemDownloadId,
496
+ boolean scheduleNext,
497
+ ProgressSnapshot snapshot,
498
+ Long capturedGeneration
499
+ ) {
233
500
  if (snapshot == null || !snapshot.found || !isTrackedDownload(id, systemDownloadId)) {
234
501
  return;
235
502
  }
@@ -244,12 +511,20 @@ public class CapacitorDownloaderPlugin extends Plugin {
244
511
  JSObject completedData = new JSObject();
245
512
  completedData.put("id", snapshot.id);
246
513
  notifyListeners("downloadCompleted", completedData);
514
+ String completionText =
515
+ capturedGeneration != null ? completedNotificationStore.takeText(snapshot.id, capturedGeneration) : null;
516
+ if (completionText != null) {
517
+ DownloadCompletedNotificationPresenter.showIfAllowed(getContext(), snapshot.id, completionText);
518
+ }
247
519
  shouldContinue = false;
248
520
  } else if (snapshot.status == DownloadManager.STATUS_FAILED) {
249
521
  JSObject failedData = new JSObject();
250
522
  failedData.put("id", snapshot.id);
251
523
  failedData.put("error", "Download failed");
252
524
  notifyListeners("downloadFailed", failedData);
525
+ if (capturedGeneration != null) {
526
+ completedNotificationStore.remove(snapshot.id, capturedGeneration);
527
+ }
253
528
  shouldContinue = false;
254
529
  }
255
530
 
@@ -297,6 +572,7 @@ public class CapacitorDownloaderPlugin extends Plugin {
297
572
  return;
298
573
  }
299
574
 
575
+ final Long stoppedGeneration = generationFor(id);
300
576
  Long downloadId = downloads.remove(id);
301
577
  if (downloadId == null) {
302
578
  call.reject("Download not found");
@@ -304,17 +580,36 @@ public class CapacitorDownloaderPlugin extends Plugin {
304
580
  }
305
581
 
306
582
  if (isPendingDownload(downloadId)) {
583
+ okHttpDestinationFiles.remove(id);
584
+ if (stoppedGeneration != null) {
585
+ completedNotificationStore.remove(id, stoppedGeneration);
586
+ }
307
587
  call.resolve(new JSObject().put("removed", false));
308
588
  return;
309
589
  }
310
590
 
591
+ if (isOkHttpDownload(downloadId)) {
592
+ File destinationFile = okHttpDestinationFiles.remove(id);
593
+ boolean cancelled = okHttpDownloadExecutor.cancel(id, destinationFile);
594
+ okHttpDownloadExecutor.clearCompleted(id);
595
+ if (stoppedGeneration != null) {
596
+ completedNotificationStore.remove(id, stoppedGeneration);
597
+ }
598
+ call.resolve(new JSObject().put("removed", cancelled));
599
+ return;
600
+ }
601
+
311
602
  final long systemDownloadId = downloadId;
312
603
  final String stoppedId = id;
604
+ final long generation = stoppedGeneration != null ? stoppedGeneration.longValue() : 0L;
313
605
  runDownloadManagerWork(
314
606
  call,
315
607
  () -> {
316
608
  try {
317
609
  int removedDownloads = downloadManager.remove(systemDownloadId);
610
+ if (stoppedGeneration != null) {
611
+ completedNotificationStore.remove(stoppedId, generation);
612
+ }
318
613
  call.resolve(new JSObject().put("removed", removedDownloads > 0));
319
614
  } catch (RuntimeException e) {
320
615
  downloads.putIfAbsent(stoppedId, systemDownloadId);
@@ -339,6 +634,25 @@ public class CapacitorDownloaderPlugin extends Plugin {
339
634
  return;
340
635
  }
341
636
 
637
+ if (isOkHttpDownload(trackedDownloadId)) {
638
+ OkHttpDownloadExecutor.ActiveDownload active = okHttpDownloadExecutor.getActive(id);
639
+ JSObject result = new JSObject();
640
+ result.put("id", id);
641
+ if (active == null) {
642
+ call.reject("Download not found");
643
+ return;
644
+ } else {
645
+ result.put("status", mapOkHttpStateToDownloadManagerStatus(active.state));
646
+ result.put("bytesDownloaded", active.bytesWritten);
647
+ result.put("bytesTotal", active.bytesTotal);
648
+ if (active.state == OkHttpDownloadExecutor.State.ERROR && active.errorMessage != null) {
649
+ result.put("reasonText", active.errorMessage);
650
+ }
651
+ }
652
+ call.resolve(result);
653
+ return;
654
+ }
655
+
342
656
  final long downloadId = trackedDownloadId;
343
657
  runDownloadManagerWork(call, () -> {
344
658
  DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
@@ -404,6 +718,22 @@ public class CapacitorDownloaderPlugin extends Plugin {
404
718
  return result;
405
719
  }
406
720
 
721
+ private int mapOkHttpStateToDownloadManagerStatus(OkHttpDownloadExecutor.State state) {
722
+ switch (state) {
723
+ case PENDING:
724
+ return DownloadManager.STATUS_PENDING;
725
+ case RUNNING:
726
+ return DownloadManager.STATUS_RUNNING;
727
+ case DONE:
728
+ return DownloadManager.STATUS_SUCCESSFUL;
729
+ case ERROR:
730
+ case CANCELLED:
731
+ return DownloadManager.STATUS_FAILED;
732
+ default:
733
+ return DownloadManager.STATUS_PENDING;
734
+ }
735
+ }
736
+
407
737
  private String getReasonText(int status, int reason) {
408
738
  if (status == DownloadManager.STATUS_FAILED) {
409
739
  switch (reason) {
@@ -0,0 +1,91 @@
1
+ package ee.forgr.capacitor.plugin.downloader;
2
+
3
+ import android.Manifest;
4
+ import android.app.NotificationChannel;
5
+ import android.app.NotificationManager;
6
+ import android.app.PendingIntent;
7
+ import android.content.Context;
8
+ import android.content.Intent;
9
+ import android.content.pm.PackageManager;
10
+ import android.os.Build;
11
+ import androidx.core.app.NotificationCompat;
12
+ import androidx.core.app.NotificationManagerCompat;
13
+ import androidx.core.content.ContextCompat;
14
+
15
+ final class DownloadCompletedNotificationPresenter {
16
+
17
+ static final String CHANNEL_ID = "capacitor_downloader_completed";
18
+
19
+ private DownloadCompletedNotificationPresenter() {}
20
+
21
+ static boolean canPostNotifications(Context context) {
22
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
23
+ return NotificationManagerCompat.from(context).areNotificationsEnabled();
24
+ }
25
+ return ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED;
26
+ }
27
+
28
+ static void showIfAllowed(Context context, String downloadId, String bodyText) {
29
+ String body = DownloadCompletedNotificationText.normalize(bodyText);
30
+ if (body == null || downloadId == null) {
31
+ return;
32
+ }
33
+ if (!canPostNotifications(context)) {
34
+ return;
35
+ }
36
+
37
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
38
+ ensureChannel(context);
39
+
40
+ Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
41
+ if (launchIntent == null) {
42
+ return;
43
+ }
44
+ launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
45
+
46
+ int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
47
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
48
+ pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE;
49
+ }
50
+
51
+ PendingIntent contentIntent = PendingIntent.getActivity(context, downloadId.hashCode(), launchIntent, pendingIntentFlags);
52
+
53
+ CharSequence appLabel = context.getApplicationInfo().loadLabel(context.getPackageManager());
54
+
55
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
56
+ .setSmallIcon(android.R.drawable.stat_sys_download_done)
57
+ .setContentTitle(appLabel)
58
+ .setContentText(body)
59
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(body))
60
+ .setContentIntent(contentIntent)
61
+ .setAutoCancel(true)
62
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT);
63
+
64
+ try {
65
+ notificationManager.notify(notificationIdFor(downloadId), builder.build());
66
+ } catch (SecurityException ignored) {
67
+ // POST_NOTIFICATIONS denied at show time.
68
+ }
69
+ }
70
+
71
+ private static void ensureChannel(Context context) {
72
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
73
+ return;
74
+ }
75
+ NotificationManager manager = context.getSystemService(NotificationManager.class);
76
+ if (manager == null) {
77
+ return;
78
+ }
79
+ NotificationChannel existing = manager.getNotificationChannel(CHANNEL_ID);
80
+ if (existing != null) {
81
+ return;
82
+ }
83
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Download completed", NotificationManager.IMPORTANCE_DEFAULT);
84
+ channel.setDescription("Shown when a Capacitor Downloader task finishes");
85
+ manager.createNotificationChannel(channel);
86
+ }
87
+
88
+ static int notificationIdFor(String downloadId) {
89
+ return ("capacitor-downloader-completed-" + downloadId).hashCode();
90
+ }
91
+ }