@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.
- package/README.md +66 -10
- package/android/build.gradle +3 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/AndroidDownloadDestination.java +143 -0
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/AndroidHttpEngineSelector.java +33 -0
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/CapacitorDownloaderPlugin.java +357 -27
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletedNotificationPresenter.java +91 -0
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletedNotificationStore.java +138 -0
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletedNotificationText.java +14 -0
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletionNotifier.java +70 -0
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/OkHttpDownloadExecutor.java +420 -0
- package/dist/docs.json +27 -3
- package/dist/esm/definitions.d.ts +51 -5
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorDownloaderPlugin/CapacitorDownloaderPlugin.swift +86 -6
- package/ios/Sources/CapacitorDownloaderPlugin/DownloadCompletedNotificationPresenter.swift +38 -0
- package/ios/Sources/CapacitorDownloaderPlugin/DownloadCompletedNotificationStore.swift +74 -0
- package/ios/Sources/CapacitorDownloaderPlugin/DownloadCompletedNotificationText.swift +11 -0
- package/ios/Sources/CapacitorDownloaderPlugin/DownloadIdGenerationStore.swift +39 -0
- package/ios/Sources/CapacitorDownloaderPlugin/DownloadTaskGenerationStore.swift +47 -0
- package/ios/Tests/CapacitorDownloaderPluginTests/CapacitorDownloaderPluginTests.swift +121 -0
- package/package.json +1 -1
package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/CapacitorDownloaderPlugin.java
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
if (downloads.containsKey(id)) {
|
|
147
|
+
call.reject("Download already exists");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
102
150
|
|
|
103
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
() ->
|
|
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(
|
|
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
|
+
}
|