@capgo/capacitor-updater 7.9.1 → 7.11.6
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 +1 -1
- package/android/build.gradle +12 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +36 -17
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +21 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +259 -99
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +29 -24
- package/dist/docs.json +1 -1
- package/dist/esm/definitions.d.ts +1 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +28 -83
- package/ios/Plugin/CapgoUpdater.swift +20 -2
- package/ios/Plugin/Logger.swift +22 -1
- package/package.json +5 -2
|
@@ -16,6 +16,7 @@ import java.net.HttpURLConnection;
|
|
|
16
16
|
import java.net.URL;
|
|
17
17
|
import java.nio.channels.FileChannel;
|
|
18
18
|
import java.nio.file.Files;
|
|
19
|
+
import java.nio.file.StandardCopyOption;
|
|
19
20
|
import java.security.MessageDigest;
|
|
20
21
|
import java.util.ArrayList;
|
|
21
22
|
import java.util.Arrays;
|
|
@@ -33,6 +34,11 @@ import okhttp3.Protocol;
|
|
|
33
34
|
import okhttp3.Request;
|
|
34
35
|
import okhttp3.Response;
|
|
35
36
|
import okhttp3.ResponseBody;
|
|
37
|
+
import okio.Buffer;
|
|
38
|
+
import okio.BufferedSink;
|
|
39
|
+
import okio.BufferedSource;
|
|
40
|
+
import okio.Okio;
|
|
41
|
+
import okio.Source;
|
|
36
42
|
import org.brotli.dec.BrotliInputStream;
|
|
37
43
|
import org.json.JSONArray;
|
|
38
44
|
import org.json.JSONObject;
|
|
@@ -60,29 +66,44 @@ public class DownloadService extends Worker {
|
|
|
60
66
|
public static final String PLUGIN_VERSION = "plugin_version";
|
|
61
67
|
private static final String UPDATE_FILE = "update.dat";
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
// Shared OkHttpClient to prevent resource leaks
|
|
70
|
+
private static OkHttpClient sharedClient;
|
|
71
|
+
private static String currentAppId = "unknown";
|
|
72
|
+
private static String currentPluginVersion = "unknown";
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
String appId = getInputData().getString(APP_ID);
|
|
69
|
-
String pluginVersion = getInputData().getString(PLUGIN_VERSION);
|
|
70
|
-
|
|
71
|
-
// Build user agent with appId and plugin version
|
|
72
|
-
String userAgent =
|
|
73
|
-
"CapacitorUpdater/" + (pluginVersion != null ? pluginVersion : "unknown") + " (" + (appId != null ? appId : "unknown") + ")";
|
|
74
|
-
|
|
75
|
-
// Create OkHttpClient with custom user agent
|
|
76
|
-
this.client = new OkHttpClient.Builder()
|
|
74
|
+
// Initialize shared client with User-Agent interceptor
|
|
75
|
+
static {
|
|
76
|
+
sharedClient = new OkHttpClient.Builder()
|
|
77
77
|
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
|
|
78
78
|
.addInterceptor(chain -> {
|
|
79
79
|
Request originalRequest = chain.request();
|
|
80
|
+
String userAgent =
|
|
81
|
+
"CapacitorUpdater/" +
|
|
82
|
+
(currentPluginVersion != null ? currentPluginVersion : "unknown") +
|
|
83
|
+
" (" +
|
|
84
|
+
(currentAppId != null ? currentAppId : "unknown") +
|
|
85
|
+
")";
|
|
80
86
|
Request requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build();
|
|
81
87
|
return chain.proceed(requestWithUserAgent);
|
|
82
88
|
})
|
|
83
89
|
.build();
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
// Method to update User-Agent values
|
|
93
|
+
public static void updateUserAgent(String appId, String pluginVersion) {
|
|
94
|
+
currentAppId = appId != null ? appId : "unknown";
|
|
95
|
+
currentPluginVersion = pluginVersion != null ? pluginVersion : "unknown";
|
|
96
|
+
logger.debug("Updated User-Agent: CapacitorUpdater/" + currentPluginVersion + " (" + currentAppId + ")");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public DownloadService(@NonNull Context context, @NonNull WorkerParameters params) {
|
|
100
|
+
super(context, params);
|
|
101
|
+
// Use shared client - no need to create new instances
|
|
102
|
+
|
|
103
|
+
// Clean up old temporary files on service initialization
|
|
104
|
+
cleanupOldTempFiles(getApplicationContext().getCacheDir());
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
private void setProgress(int percent) {
|
|
87
108
|
Data progress = new Data.Builder().putInt(PERCENT, percent).build();
|
|
88
109
|
setProgressAsync(progress);
|
|
@@ -272,93 +293,156 @@ public class DownloadService extends Worker {
|
|
|
272
293
|
String checksum
|
|
273
294
|
) {
|
|
274
295
|
File target = new File(documentsDir, dest);
|
|
275
|
-
File infoFile = new File(documentsDir, UPDATE_FILE);
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
File infoFile = new File(documentsDir, UPDATE_FILE);
|
|
297
|
+
File tempFile = new File(documentsDir, "temp" + ".tmp");
|
|
298
|
+
|
|
299
|
+
// Check available disk space before starting
|
|
300
|
+
long availableSpace = target.getParentFile().getUsableSpace();
|
|
301
|
+
long estimatedSize = 50 * 1024 * 1024; // 50MB default estimate
|
|
302
|
+
if (availableSpace < estimatedSize * 2) {
|
|
303
|
+
throw new RuntimeException("insufficient_disk_space");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
HttpURLConnection httpConn = null;
|
|
307
|
+
InputStream inputStream = null;
|
|
308
|
+
FileOutputStream outputStream = null;
|
|
309
|
+
BufferedReader reader = null;
|
|
310
|
+
BufferedWriter writer = null;
|
|
311
|
+
|
|
278
312
|
try {
|
|
279
313
|
URL u = new URL(url);
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
httpConn = (HttpURLConnection) u.openConnection();
|
|
314
|
+
httpConn = (HttpURLConnection) u.openConnection();
|
|
283
315
|
|
|
284
|
-
|
|
285
|
-
|
|
316
|
+
// Set reasonable timeouts
|
|
317
|
+
httpConn.setConnectTimeout(30000); // 30 seconds
|
|
318
|
+
httpConn.setReadTimeout(60000); // 60 seconds
|
|
286
319
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
320
|
+
// Reading progress file (if exist)
|
|
321
|
+
long downloadedBytes = 0;
|
|
322
|
+
|
|
323
|
+
if (infoFile.exists() && tempFile.exists()) {
|
|
324
|
+
try {
|
|
325
|
+
reader = new BufferedReader(new FileReader(infoFile));
|
|
326
|
+
String updateVersion = reader.readLine();
|
|
327
|
+
if (updateVersion != null && !updateVersion.equals(version)) {
|
|
328
|
+
clearDownloadData(documentsDir);
|
|
329
|
+
} else {
|
|
330
|
+
downloadedBytes = tempFile.length();
|
|
331
|
+
}
|
|
332
|
+
} finally {
|
|
333
|
+
if (reader != null) {
|
|
334
|
+
try {
|
|
335
|
+
reader.close();
|
|
336
|
+
} catch (Exception ignored) {}
|
|
295
337
|
}
|
|
296
|
-
} else {
|
|
297
|
-
clearDownloadData(documentsDir);
|
|
298
338
|
}
|
|
339
|
+
} else {
|
|
340
|
+
clearDownloadData(documentsDir);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (downloadedBytes > 0) {
|
|
344
|
+
httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
|
|
345
|
+
}
|
|
299
346
|
|
|
300
|
-
|
|
301
|
-
|
|
347
|
+
int responseCode = httpConn.getResponseCode();
|
|
348
|
+
|
|
349
|
+
if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
|
|
350
|
+
long contentLength = httpConn.getContentLength() + downloadedBytes;
|
|
351
|
+
|
|
352
|
+
// Check if we have enough space for the actual file
|
|
353
|
+
if (contentLength > 0 && availableSpace < contentLength * 2) {
|
|
354
|
+
throw new RuntimeException("insufficient_disk_space");
|
|
302
355
|
}
|
|
303
356
|
|
|
304
|
-
|
|
357
|
+
try {
|
|
358
|
+
inputStream = httpConn.getInputStream();
|
|
359
|
+
outputStream = new FileOutputStream(tempFile, downloadedBytes > 0);
|
|
360
|
+
|
|
361
|
+
if (downloadedBytes == 0) {
|
|
362
|
+
writer = new BufferedWriter(new FileWriter(infoFile));
|
|
363
|
+
writer.write(String.valueOf(version));
|
|
364
|
+
writer.close();
|
|
365
|
+
writer = null;
|
|
366
|
+
}
|
|
305
367
|
|
|
306
|
-
|
|
307
|
-
|
|
368
|
+
byte[] buffer = new byte[8192]; // Larger buffer for better performance
|
|
369
|
+
int lastNotifiedPercent = 0;
|
|
370
|
+
int bytesRead;
|
|
308
371
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// Updating the info file
|
|
319
|
-
try (BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))) {
|
|
320
|
-
writer.write(String.valueOf(version));
|
|
372
|
+
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
373
|
+
outputStream.write(buffer, 0, bytesRead);
|
|
374
|
+
downloadedBytes += bytesRead;
|
|
375
|
+
|
|
376
|
+
// Flush every 1MB to ensure progress is saved
|
|
377
|
+
if (downloadedBytes % (1024 * 1024) == 0) {
|
|
378
|
+
outputStream.flush();
|
|
321
379
|
}
|
|
322
380
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
downloadedBytes += bytesRead;
|
|
329
|
-
// Saving progress (flushing every 100 Ko)
|
|
330
|
-
if (downloadedBytes % 102400 == 0) {
|
|
331
|
-
outputStream.flush();
|
|
332
|
-
}
|
|
333
|
-
// Computing percentage
|
|
334
|
-
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
335
|
-
while (lastNotifiedPercent + 10 <= percent) {
|
|
336
|
-
lastNotifiedPercent += 10;
|
|
337
|
-
// Artificial delay using CPU-bound calculation to take ~5 seconds
|
|
338
|
-
double result = 0;
|
|
339
|
-
setProgress(lastNotifiedPercent);
|
|
340
|
-
}
|
|
381
|
+
// Computing percentage
|
|
382
|
+
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
383
|
+
if (percent >= lastNotifiedPercent + 10) {
|
|
384
|
+
lastNotifiedPercent = (percent / 10) * 10;
|
|
385
|
+
setProgress(lastNotifiedPercent);
|
|
341
386
|
}
|
|
387
|
+
}
|
|
342
388
|
|
|
343
|
-
|
|
344
|
-
|
|
389
|
+
// Final flush
|
|
390
|
+
outputStream.flush();
|
|
391
|
+
outputStream.close();
|
|
392
|
+
outputStream = null;
|
|
345
393
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
394
|
+
inputStream.close();
|
|
395
|
+
inputStream = null;
|
|
396
|
+
|
|
397
|
+
// Rename the temp file with the final name (dest)
|
|
398
|
+
if (!tempFile.renameTo(new File(documentsDir, dest))) {
|
|
399
|
+
throw new RuntimeException("Failed to rename temp file to final destination");
|
|
349
400
|
}
|
|
350
|
-
} else {
|
|
351
401
|
infoFile.delete();
|
|
402
|
+
} catch (OutOfMemoryError e) {
|
|
403
|
+
logger.error("Out of memory during download: " + e.getMessage());
|
|
404
|
+
// Try to free some memory
|
|
405
|
+
System.gc();
|
|
406
|
+
throw new RuntimeException("low_mem_fail");
|
|
407
|
+
} finally {
|
|
408
|
+
// Ensure all resources are closed
|
|
409
|
+
if (outputStream != null) {
|
|
410
|
+
try {
|
|
411
|
+
outputStream.close();
|
|
412
|
+
} catch (Exception ignored) {}
|
|
413
|
+
}
|
|
414
|
+
if (inputStream != null) {
|
|
415
|
+
try {
|
|
416
|
+
inputStream.close();
|
|
417
|
+
} catch (Exception ignored) {}
|
|
418
|
+
}
|
|
419
|
+
if (writer != null) {
|
|
420
|
+
try {
|
|
421
|
+
writer.close();
|
|
422
|
+
} catch (Exception ignored) {}
|
|
423
|
+
}
|
|
352
424
|
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
425
|
+
} else {
|
|
426
|
+
infoFile.delete();
|
|
427
|
+
throw new RuntimeException("HTTP error: " + responseCode);
|
|
357
428
|
}
|
|
358
429
|
} catch (OutOfMemoryError e) {
|
|
430
|
+
logger.error("Critical memory error: " + e.getMessage());
|
|
431
|
+
System.gc(); // Suggest garbage collection
|
|
359
432
|
throw new RuntimeException("low_mem_fail");
|
|
433
|
+
} catch (SecurityException e) {
|
|
434
|
+
logger.error("Security error during download: " + e.getMessage());
|
|
435
|
+
throw new RuntimeException("security_error: " + e.getMessage());
|
|
360
436
|
} catch (Exception e) {
|
|
361
|
-
|
|
437
|
+
logger.error("Download error: " + e.getMessage());
|
|
438
|
+
throw new RuntimeException(e.getMessage());
|
|
439
|
+
} finally {
|
|
440
|
+
// Ensure connection is closed
|
|
441
|
+
if (httpConn != null) {
|
|
442
|
+
try {
|
|
443
|
+
httpConn.disconnect();
|
|
444
|
+
} catch (Exception ignored) {}
|
|
445
|
+
}
|
|
362
446
|
}
|
|
363
447
|
}
|
|
364
448
|
|
|
@@ -412,26 +496,20 @@ public class DownloadService extends Worker {
|
|
|
412
496
|
// Create a temporary file for the compressed data
|
|
413
497
|
File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".tmp");
|
|
414
498
|
|
|
415
|
-
try (Response response =
|
|
499
|
+
try (Response response = sharedClient.newCall(request).execute()) {
|
|
416
500
|
if (!response.isSuccessful()) {
|
|
417
501
|
throw new IOException("Unexpected response code: " + response.code());
|
|
418
502
|
}
|
|
419
503
|
|
|
420
|
-
// Download compressed file
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
byte[] buffer = new byte[8192];
|
|
427
|
-
int bytesRead;
|
|
428
|
-
try (InputStream inputStream = responseBody.byteStream()) {
|
|
429
|
-
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
430
|
-
compressedFos.write(buffer, 0, bytesRead);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
504
|
+
// Download compressed file atomically
|
|
505
|
+
ResponseBody responseBody = response.body();
|
|
506
|
+
if (responseBody == null) {
|
|
507
|
+
throw new IOException("Response body is null");
|
|
433
508
|
}
|
|
434
509
|
|
|
510
|
+
// Use OkIO for atomic write
|
|
511
|
+
writeFileAtomic(compressedFile, responseBody.byteStream(), null);
|
|
512
|
+
|
|
435
513
|
if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
436
514
|
logger.debug("Decrypting file " + targetFile.getName());
|
|
437
515
|
CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
|
|
@@ -439,13 +517,22 @@ public class DownloadService extends Worker {
|
|
|
439
517
|
|
|
440
518
|
// Only decompress if file has .br extension
|
|
441
519
|
if (isBrotli) {
|
|
442
|
-
// Use new decompression method
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
520
|
+
// Use new decompression method with atomic write
|
|
521
|
+
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
522
|
+
byte[] compressedData = new byte[(int) compressedFile.length()];
|
|
523
|
+
fis.read(compressedData);
|
|
524
|
+
byte[] decompressedData = decompressBrotli(compressedData, targetFile.getName());
|
|
525
|
+
|
|
526
|
+
// Write decompressed data atomically
|
|
527
|
+
try (java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(decompressedData)) {
|
|
528
|
+
writeFileAtomic(finalTargetFile, bais, null);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
446
531
|
} else {
|
|
447
|
-
// Just copy the file without decompression
|
|
448
|
-
|
|
532
|
+
// Just copy the file without decompression using atomic operation
|
|
533
|
+
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
534
|
+
writeFileAtomic(finalTargetFile, fis, null);
|
|
535
|
+
}
|
|
449
536
|
}
|
|
450
537
|
|
|
451
538
|
// Delete the compressed file
|
|
@@ -454,8 +541,10 @@ public class DownloadService extends Worker {
|
|
|
454
541
|
|
|
455
542
|
// Verify checksum
|
|
456
543
|
if (calculatedHash.equals(expectedHash)) {
|
|
457
|
-
// Only cache if checksum is correct
|
|
458
|
-
|
|
544
|
+
// Only cache if checksum is correct - use atomic copy
|
|
545
|
+
try (FileInputStream fis = new FileInputStream(finalTargetFile)) {
|
|
546
|
+
writeFileAtomic(cacheFile, fis, expectedHash);
|
|
547
|
+
}
|
|
459
548
|
} else {
|
|
460
549
|
finalTargetFile.delete();
|
|
461
550
|
throw new IOException(
|
|
@@ -561,4 +650,75 @@ public class DownloadService extends Worker {
|
|
|
561
650
|
throw e;
|
|
562
651
|
}
|
|
563
652
|
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Atomically write data to a file using OkIO
|
|
656
|
+
*/
|
|
657
|
+
private void writeFileAtomic(File targetFile, InputStream inputStream, String expectedChecksum) throws IOException {
|
|
658
|
+
File tempFile = new File(targetFile.getParent(), targetFile.getName() + ".tmp");
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
// Write to temp file first using OkIO
|
|
662
|
+
try (BufferedSink sink = Okio.buffer(Okio.sink(tempFile)); BufferedSource source = Okio.buffer(Okio.source(inputStream))) {
|
|
663
|
+
sink.writeAll(source);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Verify checksum if provided
|
|
667
|
+
if (expectedChecksum != null && !expectedChecksum.isEmpty()) {
|
|
668
|
+
String actualChecksum = calculateFileChecksum(tempFile);
|
|
669
|
+
if (!expectedChecksum.equalsIgnoreCase(actualChecksum)) {
|
|
670
|
+
tempFile.delete();
|
|
671
|
+
throw new IOException("Checksum verification failed");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Atomic rename (on same filesystem)
|
|
676
|
+
Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
|
677
|
+
} catch (Exception e) {
|
|
678
|
+
// Clean up temp file on error
|
|
679
|
+
if (tempFile.exists()) {
|
|
680
|
+
tempFile.delete();
|
|
681
|
+
}
|
|
682
|
+
throw new IOException("Failed to write file atomically: " + e.getMessage(), e);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Calculate MD5 checksum of a file
|
|
688
|
+
*/
|
|
689
|
+
private String calculateFileChecksum(File file) throws IOException {
|
|
690
|
+
try (FileInputStream fis = new FileInputStream(file)) {
|
|
691
|
+
MessageDigest md = MessageDigest.getInstance("MD5");
|
|
692
|
+
byte[] buffer = new byte[8192];
|
|
693
|
+
int bytesRead;
|
|
694
|
+
while ((bytesRead = fis.read(buffer)) != -1) {
|
|
695
|
+
md.update(buffer, 0, bytesRead);
|
|
696
|
+
}
|
|
697
|
+
byte[] digest = md.digest();
|
|
698
|
+
StringBuilder sb = new StringBuilder();
|
|
699
|
+
for (byte b : digest) {
|
|
700
|
+
sb.append(String.format("%02x", b));
|
|
701
|
+
}
|
|
702
|
+
return sb.toString();
|
|
703
|
+
} catch (Exception e) {
|
|
704
|
+
throw new IOException("Failed to calculate checksum: " + e.getMessage(), e);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Clean up old temporary files
|
|
710
|
+
*/
|
|
711
|
+
private void cleanupOldTempFiles(File directory) {
|
|
712
|
+
if (directory == null || !directory.exists()) return;
|
|
713
|
+
|
|
714
|
+
File[] tempFiles = directory.listFiles((dir, name) -> name.endsWith(".tmp"));
|
|
715
|
+
if (tempFiles != null) {
|
|
716
|
+
long oneHourAgo = System.currentTimeMillis() - 3600000;
|
|
717
|
+
for (File tempFile : tempFiles) {
|
|
718
|
+
if (tempFile.lastModified() < oneHourAgo) {
|
|
719
|
+
tempFile.delete();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
564
724
|
}
|
|
@@ -5,12 +5,11 @@ import androidx.work.BackoffPolicy;
|
|
|
5
5
|
import androidx.work.Configuration;
|
|
6
6
|
import androidx.work.Constraints;
|
|
7
7
|
import androidx.work.Data;
|
|
8
|
+
import androidx.work.ExistingWorkPolicy;
|
|
8
9
|
import androidx.work.NetworkType;
|
|
9
10
|
import androidx.work.OneTimeWorkRequest;
|
|
10
11
|
import androidx.work.WorkManager;
|
|
11
12
|
import androidx.work.WorkRequest;
|
|
12
|
-
import java.util.HashSet;
|
|
13
|
-
import java.util.Set;
|
|
14
13
|
import java.util.concurrent.TimeUnit;
|
|
15
14
|
|
|
16
15
|
public class DownloadWorkerManager {
|
|
@@ -22,8 +21,6 @@ public class DownloadWorkerManager {
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
private static volatile boolean isInitialized = false;
|
|
25
|
-
private static final Set<String> activeVersions = new HashSet<>();
|
|
26
|
-
private static final Object activeVersionsLock = new Object();
|
|
27
24
|
|
|
28
25
|
private static synchronized void initializeIfNeeded(Context context) {
|
|
29
26
|
if (!isInitialized) {
|
|
@@ -37,9 +34,17 @@ public class DownloadWorkerManager {
|
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
public static boolean isVersionDownloading(String version) {
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
public static boolean isVersionDownloading(Context context, String version) {
|
|
38
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
39
|
+
try {
|
|
40
|
+
return WorkManager.getInstance(context)
|
|
41
|
+
.getWorkInfosByTag(version)
|
|
42
|
+
.get()
|
|
43
|
+
.stream()
|
|
44
|
+
.anyMatch(workInfo -> !workInfo.getState().isFinished());
|
|
45
|
+
} catch (Exception e) {
|
|
46
|
+
logger.error("Error checking download status: " + e.getMessage());
|
|
47
|
+
return false;
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -60,14 +65,8 @@ public class DownloadWorkerManager {
|
|
|
60
65
|
) {
|
|
61
66
|
initializeIfNeeded(context.getApplicationContext());
|
|
62
67
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
if (activeVersions.contains(version)) {
|
|
66
|
-
logger.info("Version " + version + " is already downloading");
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
activeVersions.add(version);
|
|
70
|
-
}
|
|
68
|
+
// Use unique work name for this bundle to prevent duplicates
|
|
69
|
+
String uniqueWorkName = "bundle_" + id + "_" + version;
|
|
71
70
|
|
|
72
71
|
// Create input data
|
|
73
72
|
Data inputData = new Data.Builder()
|
|
@@ -100,7 +99,7 @@ public class DownloadWorkerManager {
|
|
|
100
99
|
.setConstraints(constraints)
|
|
101
100
|
.setInputData(inputData)
|
|
102
101
|
.addTag(id)
|
|
103
|
-
.addTag(version)
|
|
102
|
+
.addTag(version)
|
|
104
103
|
.addTag("capacitor_updater_download");
|
|
105
104
|
|
|
106
105
|
// More aggressive retry policy for emulators
|
|
@@ -112,23 +111,29 @@ public class DownloadWorkerManager {
|
|
|
112
111
|
|
|
113
112
|
OneTimeWorkRequest workRequest = workRequestBuilder.build();
|
|
114
113
|
|
|
115
|
-
//
|
|
116
|
-
WorkManager.getInstance(context)
|
|
114
|
+
// Use beginUniqueWork to prevent duplicate downloads
|
|
115
|
+
WorkManager.getInstance(context)
|
|
116
|
+
.beginUniqueWork(
|
|
117
|
+
uniqueWorkName,
|
|
118
|
+
ExistingWorkPolicy.KEEP, // Don't start if already running
|
|
119
|
+
workRequest
|
|
120
|
+
)
|
|
121
|
+
.enqueue();
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
public static void cancelVersionDownload(Context context, String version) {
|
|
120
125
|
initializeIfNeeded(context.getApplicationContext());
|
|
121
126
|
WorkManager.getInstance(context).cancelAllWorkByTag(version);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public static void cancelBundleDownload(Context context, String id, String version) {
|
|
130
|
+
String uniqueWorkName = "bundle_" + id + "_" + version;
|
|
131
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
132
|
+
WorkManager.getInstance(context).cancelUniqueWork(uniqueWorkName);
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
public static void cancelAllDownloads(Context context) {
|
|
128
136
|
initializeIfNeeded(context.getApplicationContext());
|
|
129
137
|
WorkManager.getInstance(context).cancelAllWorkByTag("capacitor_updater_download");
|
|
130
|
-
synchronized (activeVersionsLock) {
|
|
131
|
-
activeVersions.clear();
|
|
132
|
-
}
|
|
133
138
|
}
|
|
134
139
|
}
|
package/dist/docs.json
CHANGED
|
@@ -2512,7 +2512,7 @@
|
|
|
2512
2512
|
"name": "since"
|
|
2513
2513
|
}
|
|
2514
2514
|
],
|
|
2515
|
-
"docs": "Set the default channel for the app in the config. Case sensitive.\nThis will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud.",
|
|
2515
|
+
"docs": "Set the default channel for the app in the config. Case sensitive.\nThis will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud.\nThis requires the channel to allow devices to self dissociate/associate in the channel settings. https://capgo.app/docs/public-api/channels/#channel-configuration-options",
|
|
2516
2516
|
"complexTypes": [],
|
|
2517
2517
|
"type": "string | undefined"
|
|
2518
2518
|
},
|
|
@@ -209,6 +209,7 @@ declare module '@capacitor/cli' {
|
|
|
209
209
|
/**
|
|
210
210
|
* Set the default channel for the app in the config. Case sensitive.
|
|
211
211
|
* This will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud.
|
|
212
|
+
* This requires the channel to allow devices to self dissociate/associate in the channel settings. https://capgo.app/docs/public-api/channels/#channel-configuration-options
|
|
212
213
|
*
|
|
213
214
|
*
|
|
214
215
|
* @default undefined
|