@capgo/capacitor-updater 8.51.21 → 8.51.23
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/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +18 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +135 -35
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +51 -18
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +103 -28
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +218 -94
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +177 -32
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +9 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +4 -1
- package/package.json +1 -1
|
@@ -363,6 +363,38 @@ public class DownloadService extends Worker {
|
|
|
363
363
|
return seenTargets.add(targetFile.getCanonicalPath());
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
private static final class ManifestDownloadTask {
|
|
367
|
+
|
|
368
|
+
final String fileName;
|
|
369
|
+
final String finalFileHash;
|
|
370
|
+
final String downloadUrl;
|
|
371
|
+
final boolean isBrotli;
|
|
372
|
+
final File targetFile;
|
|
373
|
+
final File builtinFile;
|
|
374
|
+
final File cacheFile;
|
|
375
|
+
final File legacyCacheFile;
|
|
376
|
+
|
|
377
|
+
ManifestDownloadTask(
|
|
378
|
+
final String fileName,
|
|
379
|
+
final String finalFileHash,
|
|
380
|
+
final String downloadUrl,
|
|
381
|
+
final boolean isBrotli,
|
|
382
|
+
final File targetFile,
|
|
383
|
+
final File builtinFile,
|
|
384
|
+
final File cacheFile,
|
|
385
|
+
final File legacyCacheFile
|
|
386
|
+
) {
|
|
387
|
+
this.fileName = fileName;
|
|
388
|
+
this.finalFileHash = finalFileHash;
|
|
389
|
+
this.downloadUrl = downloadUrl;
|
|
390
|
+
this.isBrotli = isBrotli;
|
|
391
|
+
this.targetFile = targetFile;
|
|
392
|
+
this.builtinFile = builtinFile;
|
|
393
|
+
this.cacheFile = cacheFile;
|
|
394
|
+
this.legacyCacheFile = legacyCacheFile;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
366
398
|
static boolean tryCopyBuiltinAsset(final AssetManager assets, final String fileName, final File dest, final String expectedHash) {
|
|
367
399
|
if (assets == null || fileName == null || dest == null) {
|
|
368
400
|
return false;
|
|
@@ -424,7 +456,7 @@ public class DownloadService extends Worker {
|
|
|
424
456
|
return createFailureResult("Manifest is null");
|
|
425
457
|
}
|
|
426
458
|
} else {
|
|
427
|
-
handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum);
|
|
459
|
+
handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum, publicKey);
|
|
428
460
|
return createSuccessResult(dest, version, sessionKey, checksum, false);
|
|
429
461
|
}
|
|
430
462
|
} catch (DownloadRetryException e) {
|
|
@@ -515,6 +547,12 @@ public class DownloadService extends Worker {
|
|
|
515
547
|
try {
|
|
516
548
|
logger.debug("handleManifestDownload");
|
|
517
549
|
|
|
550
|
+
if (publicKey != null && !publicKey.isEmpty() && !CryptoCipher.isValidSessionKey(sessionKey)) {
|
|
551
|
+
logger.error("Public key present but no valid session key provided");
|
|
552
|
+
sendStatsAsync("session_key_required", version);
|
|
553
|
+
throw new IOException("Session key required when public key is present");
|
|
554
|
+
}
|
|
555
|
+
|
|
518
556
|
// Send stats for manifest download start
|
|
519
557
|
sendStatsAsync("download_manifest_start", version);
|
|
520
558
|
|
|
@@ -535,9 +573,7 @@ public class DownloadService extends Worker {
|
|
|
535
573
|
int totalFiles = manifest.length();
|
|
536
574
|
final AtomicLong completedFiles = new AtomicLong(0);
|
|
537
575
|
final AtomicBoolean hasError = new AtomicBoolean(false);
|
|
538
|
-
|
|
539
|
-
ExecutorService executor = Executors.newFixedThreadPool(Math.min(MANIFEST_MAX_CONCURRENT_FILES, Math.max(1, totalFiles)));
|
|
540
|
-
List<Future<?>> futures = new ArrayList<>();
|
|
576
|
+
final List<ManifestDownloadTask> tasks = new ArrayList<>();
|
|
541
577
|
final Set<String> seenTargets = new HashSet<>();
|
|
542
578
|
|
|
543
579
|
for (int i = 0; i < totalFiles; i++) {
|
|
@@ -552,7 +588,13 @@ public class DownloadService extends Worker {
|
|
|
552
588
|
continue;
|
|
553
589
|
}
|
|
554
590
|
|
|
555
|
-
if (publicKey != null && !publicKey.isEmpty()
|
|
591
|
+
if (publicKey != null && !publicKey.isEmpty()) {
|
|
592
|
+
if (!CryptoCipher.isValidSessionKey(sessionKey)) {
|
|
593
|
+
logger.error("Public key present but no valid session key provided");
|
|
594
|
+
sendStatsAsync("session_key_required", version);
|
|
595
|
+
hasError.set(true);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
556
598
|
try {
|
|
557
599
|
fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
|
|
558
600
|
} catch (Exception e) {
|
|
@@ -592,35 +634,56 @@ public class DownloadService extends Worker {
|
|
|
592
634
|
final File legacyCacheFile =
|
|
593
635
|
isBrotli && cacheFile != null ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
|
|
594
636
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
637
|
+
tasks.add(
|
|
638
|
+
new ManifestDownloadTask(
|
|
639
|
+
fileName,
|
|
640
|
+
finalFileHash,
|
|
641
|
+
downloadUrl,
|
|
642
|
+
isBrotli,
|
|
643
|
+
targetFile,
|
|
644
|
+
builtinFile,
|
|
645
|
+
cacheFile,
|
|
646
|
+
legacyCacheFile
|
|
647
|
+
)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (hasError.get()) {
|
|
652
|
+
throw new IOException("Manifest contains invalid or duplicate file paths");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
for (final ManifestDownloadTask task : tasks) {
|
|
656
|
+
if (!Objects.requireNonNull(task.targetFile.getParentFile()).exists() && !task.targetFile.getParentFile().mkdirs()) {
|
|
657
|
+
logger.error("Failed to create parent directory for: " + task.targetFile.getAbsolutePath());
|
|
658
|
+
throw new IOException("Failed to create parent directory for: " + task.targetFile.getAbsolutePath());
|
|
600
659
|
}
|
|
660
|
+
}
|
|
601
661
|
|
|
602
|
-
|
|
662
|
+
ExecutorService executor = Executors.newFixedThreadPool(Math.min(MANIFEST_MAX_CONCURRENT_FILES, Math.max(1, totalFiles)));
|
|
663
|
+
List<Future<?>> futures = new ArrayList<>();
|
|
664
|
+
|
|
665
|
+
for (final ManifestDownloadTask task : tasks) {
|
|
603
666
|
Future<?> future = executor.submit(() -> {
|
|
604
667
|
try {
|
|
605
|
-
if (tryCopyBuiltinAsset(assets, fileName, targetFile, finalFileHash)) {
|
|
606
|
-
logger.debug("using builtin asset " + fileName);
|
|
607
|
-
} else if (tryCopyBuiltinFile(builtinFile, targetFile, finalFileHash)) {
|
|
608
|
-
logger.debug("using builtin file " + fileName);
|
|
668
|
+
if (tryCopyBuiltinAsset(assets, task.fileName, task.targetFile, task.finalFileHash)) {
|
|
669
|
+
logger.debug("using builtin asset " + task.fileName);
|
|
670
|
+
} else if (tryCopyBuiltinFile(task.builtinFile, task.targetFile, task.finalFileHash)) {
|
|
671
|
+
logger.debug("using builtin file " + task.fileName);
|
|
609
672
|
} else if (
|
|
610
|
-
tryCopyFromCache(cacheFile, targetFile, finalFileHash) ||
|
|
611
|
-
(legacyCacheFile != null && tryCopyFromCache(legacyCacheFile, targetFile, finalFileHash))
|
|
673
|
+
tryCopyFromCache(task.cacheFile, task.targetFile, task.finalFileHash) ||
|
|
674
|
+
(task.legacyCacheFile != null && tryCopyFromCache(task.legacyCacheFile, task.targetFile, task.finalFileHash))
|
|
612
675
|
) {
|
|
613
|
-
logger.debug("already cached " + fileName);
|
|
676
|
+
logger.debug("already cached " + task.fileName);
|
|
614
677
|
} else {
|
|
615
678
|
downloadAndVerify(
|
|
616
|
-
downloadUrl,
|
|
617
|
-
targetFile,
|
|
618
|
-
cacheFile,
|
|
619
|
-
finalFileHash,
|
|
679
|
+
task.downloadUrl,
|
|
680
|
+
task.targetFile,
|
|
681
|
+
task.cacheFile,
|
|
682
|
+
task.finalFileHash,
|
|
620
683
|
sessionKey,
|
|
621
684
|
publicKey,
|
|
622
|
-
|
|
623
|
-
fileName
|
|
685
|
+
task.isBrotli,
|
|
686
|
+
task.fileName
|
|
624
687
|
);
|
|
625
688
|
}
|
|
626
689
|
|
|
@@ -628,8 +691,8 @@ public class DownloadService extends Worker {
|
|
|
628
691
|
int percent = calcTotalPercent(completed, totalFiles);
|
|
629
692
|
setProgress(percent);
|
|
630
693
|
} catch (Exception e) {
|
|
631
|
-
logger.error("Error processing file: " + fileName + " " + e.getMessage());
|
|
632
|
-
sendStatsAsync("download_manifest_file_fail", version + ":" + fileName);
|
|
694
|
+
logger.error("Error processing file: " + task.fileName + " " + e.getMessage());
|
|
695
|
+
sendStatsAsync("download_manifest_file_fail", version + ":" + task.fileName);
|
|
633
696
|
hasError.set(true);
|
|
634
697
|
}
|
|
635
698
|
});
|
|
@@ -676,8 +739,20 @@ public class DownloadService extends Worker {
|
|
|
676
739
|
String dest,
|
|
677
740
|
String version,
|
|
678
741
|
String sessionKey,
|
|
679
|
-
String checksum
|
|
742
|
+
String checksum,
|
|
743
|
+
String publicKey
|
|
680
744
|
) {
|
|
745
|
+
if (publicKey != null && !publicKey.isEmpty() && !CryptoCipher.isValidSessionKey(sessionKey)) {
|
|
746
|
+
logger.error("Public key present but no valid session key provided");
|
|
747
|
+
sendStatsAsync("session_key_required", version);
|
|
748
|
+
throw new RuntimeException("Session key required when public key is present");
|
|
749
|
+
}
|
|
750
|
+
if (checksum == null || checksum.isEmpty()) {
|
|
751
|
+
logger.error("No checksum provided");
|
|
752
|
+
sendStatsAsync("checksum_required", version);
|
|
753
|
+
throw new RuntimeException("Checksum required");
|
|
754
|
+
}
|
|
755
|
+
|
|
681
756
|
// Send stats for zip download start
|
|
682
757
|
sendStatsAsync("download_zip_start", version);
|
|
683
758
|
|
|
@@ -1059,7 +1134,7 @@ public class DownloadService extends Worker {
|
|
|
1059
1134
|
}
|
|
1060
1135
|
}
|
|
1061
1136
|
|
|
1062
|
-
boolean needDecrypt = publicKey != null && !publicKey.isEmpty() &&
|
|
1137
|
+
boolean needDecrypt = publicKey != null && !publicKey.isEmpty() && CryptoCipher.isValidSessionKey(sessionKey);
|
|
1063
1138
|
File source = partial;
|
|
1064
1139
|
if (needDecrypt) {
|
|
1065
1140
|
workFile = new File(cacheFolder, "work_" + UUID.randomUUID() + "_" + targetFile.getName() + ".tmp");
|
|
@@ -96,7 +96,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
96
96
|
deinit {
|
|
97
97
|
implementation.shutdown()
|
|
98
98
|
}
|
|
99
|
-
private let pluginVersion: String = "8.51.
|
|
99
|
+
private let pluginVersion: String = "8.51.23"
|
|
100
100
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
101
101
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
102
102
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
@@ -582,8 +582,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
582
582
|
var dest: URL
|
|
583
583
|
if BundleInfo.ID_BUILTIN == id {
|
|
584
584
|
dest = Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
585
|
+
} else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
|
|
586
|
+
dest = bundleDir
|
|
585
587
|
} else {
|
|
586
|
-
dest =
|
|
588
|
+
dest = Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
587
589
|
}
|
|
588
590
|
|
|
589
591
|
if !FileManager.default.fileExists(atPath: dest.path) {
|
|
@@ -1609,7 +1611,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1609
1611
|
throw makePreviewError("Invalid download URL")
|
|
1610
1612
|
}
|
|
1611
1613
|
|
|
1614
|
+
if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
|
|
1615
|
+
self.logger.error("Public key present but no valid session key provided")
|
|
1616
|
+
self.implementation.sendStats(action: "session_key_required", versionName: version)
|
|
1617
|
+
throw makePreviewError("Session key required when public key is present")
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1612
1620
|
var checksum = rawChecksum
|
|
1621
|
+
if manifestEntries == nil && checksum.isEmpty {
|
|
1622
|
+
self.logger.error("No checksum provided")
|
|
1623
|
+
self.implementation.sendStats(action: "checksum_required", versionName: version)
|
|
1624
|
+
throw makePreviewError("Checksum required")
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1613
1627
|
let next: BundleInfo
|
|
1614
1628
|
if let manifestEntries = manifestEntries {
|
|
1615
1629
|
next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
|
|
@@ -1617,29 +1631,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1617
1631
|
next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
|
|
1618
1632
|
}
|
|
1619
1633
|
|
|
1620
|
-
if
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
self.
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
1634
|
-
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
1635
|
-
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
1636
|
-
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
1637
|
-
let id = next.getId()
|
|
1638
|
-
let resDel = self.implementation.delete(id: id)
|
|
1639
|
-
if !resDel {
|
|
1640
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1634
|
+
if manifestEntries == nil {
|
|
1635
|
+
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
1636
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
1637
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
1638
|
+
if next.getChecksum() != checksum {
|
|
1639
|
+
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
1640
|
+
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
1641
|
+
let id = next.getId()
|
|
1642
|
+
let resDel = self.implementation.delete(id: id)
|
|
1643
|
+
if !resDel {
|
|
1644
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1645
|
+
}
|
|
1646
|
+
throw ObjectSavableError.checksum
|
|
1641
1647
|
}
|
|
1642
|
-
throw ObjectSavableError.checksum
|
|
1643
1648
|
}
|
|
1644
1649
|
|
|
1645
1650
|
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
@@ -1691,8 +1696,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1691
1696
|
let id = self.implementation.getCurrentBundleId()
|
|
1692
1697
|
if BundleInfo.ID_BUILTIN == id {
|
|
1693
1698
|
return Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
1699
|
+
} else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
|
|
1700
|
+
return bundleDir
|
|
1694
1701
|
} else {
|
|
1695
|
-
return
|
|
1702
|
+
return Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
1696
1703
|
}
|
|
1697
1704
|
}
|
|
1698
1705
|
|
|
@@ -3496,13 +3503,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3496
3503
|
}
|
|
3497
3504
|
|
|
3498
3505
|
private func performHideSplashscreen() {
|
|
3499
|
-
self.cancelSplashscreenTimeout()
|
|
3500
3506
|
self.removeSplashscreenLoader()
|
|
3501
3507
|
self.splashscreenInvocationToken += 1
|
|
3502
|
-
self.
|
|
3503
|
-
|
|
3504
|
-
callbackId: "autoHideSplashscreen",
|
|
3505
|
-
options: self.splashscreenOptions(methodName: "hide"),
|
|
3508
|
+
self.invokeSplashscreenAction(
|
|
3509
|
+
.hide,
|
|
3506
3510
|
retriesRemaining: self.splashscreenMaxRetries,
|
|
3507
3511
|
requestToken: self.splashscreenInvocationToken
|
|
3508
3512
|
)
|
|
@@ -3522,10 +3526,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3522
3526
|
self.cancelSplashscreenTimeout()
|
|
3523
3527
|
self.autoSplashscreenTimedOut = false
|
|
3524
3528
|
self.splashscreenInvocationToken += 1
|
|
3525
|
-
self.
|
|
3526
|
-
|
|
3527
|
-
callbackId: "autoShowSplashscreen",
|
|
3528
|
-
options: self.splashscreenOptions(methodName: "show"),
|
|
3529
|
+
self.invokeSplashscreenAction(
|
|
3530
|
+
.show,
|
|
3529
3531
|
retriesRemaining: self.splashscreenMaxRetries,
|
|
3530
3532
|
requestToken: self.splashscreenInvocationToken
|
|
3531
3533
|
)
|
|
@@ -3535,7 +3537,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3535
3537
|
}
|
|
3536
3538
|
|
|
3537
3539
|
private func splashscreenOptions(methodName: String) -> [String: Any] {
|
|
3538
|
-
methodName == "show" ? ["autoHide": false] : [:]
|
|
3540
|
+
methodName == "show" ? ["autoHide": false, "fadeInDuration": 0] : [:]
|
|
3539
3541
|
}
|
|
3540
3542
|
|
|
3541
3543
|
private func splashscreenCompletedMessage(methodName: String) -> String {
|
|
@@ -3554,20 +3556,59 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3554
3556
|
self.splashscreenInvocationToken += 1
|
|
3555
3557
|
}
|
|
3556
3558
|
|
|
3557
|
-
private
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3559
|
+
private enum SplashscreenAction {
|
|
3560
|
+
case show
|
|
3561
|
+
case hide
|
|
3562
|
+
|
|
3563
|
+
var methodName: String {
|
|
3564
|
+
switch self {
|
|
3565
|
+
case .show:
|
|
3566
|
+
return "show"
|
|
3567
|
+
case .hide:
|
|
3568
|
+
return "hide"
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
var unavailableMessageVerb: String {
|
|
3573
|
+
switch self {
|
|
3574
|
+
case .show:
|
|
3575
|
+
return "showing"
|
|
3576
|
+
case .hide:
|
|
3577
|
+
return "hiding"
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3565
3580
|
}
|
|
3566
3581
|
|
|
3567
|
-
private func
|
|
3568
|
-
methodName:
|
|
3569
|
-
|
|
3570
|
-
|
|
3582
|
+
private func splashscreenOptionsJSON(methodName: String) -> String {
|
|
3583
|
+
let options = self.splashscreenOptions(methodName: methodName)
|
|
3584
|
+
guard !options.isEmpty,
|
|
3585
|
+
let data = try? JSONSerialization.data(withJSONObject: options),
|
|
3586
|
+
let json = String(data: data, encoding: .utf8) else {
|
|
3587
|
+
return "{}"
|
|
3588
|
+
}
|
|
3589
|
+
return json
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
private func splashscreenPluginExposesMethod(_ methodName: String, on plugin: CAPPlugin) -> Bool {
|
|
3593
|
+
guard let bridgedPlugin = plugin as? CAPBridgedPlugin else {
|
|
3594
|
+
return false
|
|
3595
|
+
}
|
|
3596
|
+
return bridgedPlugin.pluginMethods.contains { $0.name == methodName }
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
private func splashscreenBridgeScript(for action: SplashscreenAction) -> String {
|
|
3600
|
+
let optionsJSON = self.splashscreenOptionsJSON(methodName: action.methodName)
|
|
3601
|
+
return """
|
|
3602
|
+
var cap = window.Capacitor;
|
|
3603
|
+
if (!cap || typeof cap.nativePromise !== 'function') {
|
|
3604
|
+
throw new Error('Capacitor bridge not ready');
|
|
3605
|
+
}
|
|
3606
|
+
return await cap.nativePromise('\(self.splashscreenPluginName)', '\(action.methodName)', \(optionsJSON));
|
|
3607
|
+
"""
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
private func invokeSplashscreenAction(
|
|
3611
|
+
_ action: SplashscreenAction,
|
|
3571
3612
|
retriesRemaining: Int,
|
|
3572
3613
|
requestToken: Int
|
|
3573
3614
|
) {
|
|
@@ -3576,22 +3617,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3576
3617
|
}
|
|
3577
3618
|
|
|
3578
3619
|
guard let bridge = self.bridge else {
|
|
3579
|
-
self.
|
|
3580
|
-
|
|
3581
|
-
callbackId: callbackId,
|
|
3582
|
-
options: options,
|
|
3620
|
+
self.retrySplashscreenAction(
|
|
3621
|
+
action,
|
|
3583
3622
|
retriesRemaining: retriesRemaining,
|
|
3584
3623
|
requestToken: requestToken,
|
|
3585
|
-
message: "Bridge not available for \(
|
|
3624
|
+
message: "Bridge not available for \(action.unavailableMessageVerb) splashscreen with autoSplashscreen"
|
|
3586
3625
|
)
|
|
3587
3626
|
return
|
|
3588
3627
|
}
|
|
3589
3628
|
|
|
3590
3629
|
guard let splashScreenPlugin = bridge.plugin(withName: self.splashscreenPluginName) else {
|
|
3591
|
-
self.
|
|
3592
|
-
|
|
3593
|
-
callbackId: callbackId,
|
|
3594
|
-
options: options,
|
|
3630
|
+
self.retrySplashscreenAction(
|
|
3631
|
+
action,
|
|
3595
3632
|
retriesRemaining: retriesRemaining,
|
|
3596
3633
|
requestToken: requestToken,
|
|
3597
3634
|
message: "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
|
|
@@ -3599,37 +3636,86 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3599
3636
|
return
|
|
3600
3637
|
}
|
|
3601
3638
|
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3639
|
+
guard self.splashscreenPluginExposesMethod(action.methodName, on: splashScreenPlugin) else {
|
|
3640
|
+
self.retrySplashscreenAction(
|
|
3641
|
+
action,
|
|
3642
|
+
retriesRemaining: retriesRemaining,
|
|
3643
|
+
requestToken: requestToken,
|
|
3644
|
+
message: "autoSplashscreen: SplashScreen plugin does not expose \(action.methodName)(). Make sure @capacitor/splash-screen plugin is properly installed."
|
|
3645
|
+
)
|
|
3646
|
+
return
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
guard let webView = bridge.webView else {
|
|
3650
|
+
self.retrySplashscreenAction(
|
|
3651
|
+
action,
|
|
3608
3652
|
retriesRemaining: retriesRemaining,
|
|
3609
3653
|
requestToken: requestToken,
|
|
3610
|
-
message: "
|
|
3654
|
+
message: "WebView not available for \(action.unavailableMessageVerb) splashscreen with autoSplashscreen"
|
|
3611
3655
|
)
|
|
3612
3656
|
return
|
|
3613
3657
|
}
|
|
3614
3658
|
|
|
3615
|
-
let
|
|
3616
|
-
|
|
3617
|
-
|
|
3659
|
+
let script = self.splashscreenBridgeScript(for: action)
|
|
3660
|
+
let runInvocation = { [weak self] in
|
|
3661
|
+
guard let self = self, requestToken == self.splashscreenInvocationToken else {
|
|
3662
|
+
return
|
|
3663
|
+
}
|
|
3664
|
+
webView.callAsyncJavaScript(
|
|
3665
|
+
script,
|
|
3666
|
+
arguments: [:],
|
|
3667
|
+
in: nil,
|
|
3668
|
+
in: .page
|
|
3669
|
+
) { [weak self] (result: Result<Any, Error>) in
|
|
3670
|
+
guard let self = self, requestToken == self.splashscreenInvocationToken else {
|
|
3671
|
+
return
|
|
3672
|
+
}
|
|
3673
|
+
if case .failure(let error) = result {
|
|
3674
|
+
self.retrySplashscreenAction(
|
|
3675
|
+
action,
|
|
3676
|
+
retriesRemaining: retriesRemaining,
|
|
3677
|
+
requestToken: requestToken,
|
|
3678
|
+
message: "Failed to invoke SplashScreen \(action.methodName) via Capacitor bridge: \(error.localizedDescription)"
|
|
3679
|
+
)
|
|
3680
|
+
return
|
|
3681
|
+
}
|
|
3682
|
+
if action == .hide {
|
|
3683
|
+
self.cancelSplashscreenTimeout()
|
|
3684
|
+
}
|
|
3685
|
+
self.logger.info("Called SplashScreen \(action.methodName) method")
|
|
3686
|
+
self.logger.info(self.splashscreenCompletedMessage(methodName: action.methodName))
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
if Thread.isMainThread {
|
|
3691
|
+
runInvocation()
|
|
3692
|
+
} else {
|
|
3693
|
+
DispatchQueue.main.async(execute: runInvocation)
|
|
3694
|
+
}
|
|
3618
3695
|
}
|
|
3619
3696
|
|
|
3620
|
-
private func
|
|
3621
|
-
|
|
3622
|
-
callbackId: String,
|
|
3623
|
-
options: [String: Any],
|
|
3697
|
+
private func retrySplashscreenAction(
|
|
3698
|
+
_ action: SplashscreenAction,
|
|
3624
3699
|
retriesRemaining: Int,
|
|
3625
3700
|
requestToken: Int,
|
|
3626
3701
|
message: String
|
|
3627
3702
|
) {
|
|
3628
3703
|
guard retriesRemaining > 0 else {
|
|
3629
|
-
if
|
|
3704
|
+
if action == .show {
|
|
3630
3705
|
self.logger.warn(message)
|
|
3631
|
-
|
|
3632
|
-
|
|
3706
|
+
return
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
self.logger.error("\(message). Scheduling another hide retry.")
|
|
3710
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.splashscreenRetryDelayMilliseconds)) { [weak self] in
|
|
3711
|
+
guard let self = self, requestToken == self.splashscreenInvocationToken else {
|
|
3712
|
+
return
|
|
3713
|
+
}
|
|
3714
|
+
self.invokeSplashscreenAction(
|
|
3715
|
+
action,
|
|
3716
|
+
retriesRemaining: self.splashscreenMaxRetries,
|
|
3717
|
+
requestToken: requestToken
|
|
3718
|
+
)
|
|
3633
3719
|
}
|
|
3634
3720
|
return
|
|
3635
3721
|
}
|
|
@@ -3639,10 +3725,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3639
3725
|
guard let self = self, requestToken == self.splashscreenInvocationToken else {
|
|
3640
3726
|
return
|
|
3641
3727
|
}
|
|
3642
|
-
self.
|
|
3643
|
-
|
|
3644
|
-
callbackId: callbackId,
|
|
3645
|
-
options: options,
|
|
3728
|
+
self.invokeSplashscreenAction(
|
|
3729
|
+
action,
|
|
3646
3730
|
retriesRemaining: retriesRemaining - 1,
|
|
3647
3731
|
requestToken: requestToken
|
|
3648
3732
|
)
|
|
@@ -4095,6 +4179,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4095
4179
|
self.updateUrl = updateUrl
|
|
4096
4180
|
}
|
|
4097
4181
|
|
|
4182
|
+
func downloadBundleForTesting(
|
|
4183
|
+
urlString: String,
|
|
4184
|
+
version: String,
|
|
4185
|
+
sessionKey: String,
|
|
4186
|
+
checksum: String,
|
|
4187
|
+
manifestEntries: [ManifestEntry]?
|
|
4188
|
+
) throws -> BundleInfo {
|
|
4189
|
+
try downloadBundle(
|
|
4190
|
+
urlString: urlString,
|
|
4191
|
+
version: version,
|
|
4192
|
+
sessionKey: sessionKey,
|
|
4193
|
+
checksum: checksum,
|
|
4194
|
+
manifestEntries: manifestEntries
|
|
4195
|
+
)
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4098
4198
|
func setAutoUpdateModeForTesting(_ autoUpdateMode: String) {
|
|
4099
4199
|
self.autoUpdateMode = Self.normalizedAutoUpdateMode(autoUpdateMode)
|
|
4100
4200
|
self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
|
|
@@ -4430,6 +4530,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4430
4530
|
if latestVersionName != "" && current.getVersionName() != latestVersionName {
|
|
4431
4531
|
do {
|
|
4432
4532
|
self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
|
|
4533
|
+
if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
|
|
4534
|
+
self.logger.error("Public key present but no valid session key provided")
|
|
4535
|
+
self.implementation.sendStats(action: "session_key_required", versionName: latestVersionName)
|
|
4536
|
+
self.endBackGroundTaskWithNotif(
|
|
4537
|
+
msg: "Session key required when public key is present",
|
|
4538
|
+
latestVersionName: latestVersionName,
|
|
4539
|
+
current: current,
|
|
4540
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4541
|
+
)
|
|
4542
|
+
return
|
|
4543
|
+
}
|
|
4433
4544
|
var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
|
|
4434
4545
|
let needsDownload = nextImpl.map(Self.shouldRetryDownloadForExistingBundle) ?? true
|
|
4435
4546
|
if needsDownload {
|
|
@@ -4447,6 +4558,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4447
4558
|
if res.manifest != nil {
|
|
4448
4559
|
nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
|
|
4449
4560
|
} else {
|
|
4561
|
+
if res.checksum.isEmpty {
|
|
4562
|
+
self.logger.error("No checksum provided")
|
|
4563
|
+
self.implementation.sendStats(action: "checksum_required", versionName: latestVersionName)
|
|
4564
|
+
self.endBackGroundTaskWithNotif(
|
|
4565
|
+
msg: "Checksum required",
|
|
4566
|
+
latestVersionName: latestVersionName,
|
|
4567
|
+
current: current,
|
|
4568
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4569
|
+
)
|
|
4570
|
+
return
|
|
4571
|
+
}
|
|
4450
4572
|
nextImpl = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
|
|
4451
4573
|
}
|
|
4452
4574
|
}
|
|
@@ -4475,24 +4597,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4475
4597
|
self.endBackGroundTask()
|
|
4476
4598
|
return
|
|
4477
4599
|
}
|
|
4478
|
-
res.
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4600
|
+
if res.manifest == nil {
|
|
4601
|
+
res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
|
|
4602
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
4603
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
|
|
4604
|
+
if next.getChecksum() != res.checksum {
|
|
4605
|
+
self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
|
|
4606
|
+
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
4607
|
+
let id = next.getId()
|
|
4608
|
+
let resDel = self.implementation.delete(id: id)
|
|
4609
|
+
if !resDel {
|
|
4610
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
4611
|
+
}
|
|
4612
|
+
self.endBackGroundTaskWithNotif(
|
|
4613
|
+
msg: "Error checksum",
|
|
4614
|
+
latestVersionName: latestVersionName,
|
|
4615
|
+
current: current,
|
|
4616
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4617
|
+
)
|
|
4618
|
+
return
|
|
4488
4619
|
}
|
|
4489
|
-
self.endBackGroundTaskWithNotif(
|
|
4490
|
-
msg: "Error checksum",
|
|
4491
|
-
latestVersionName: latestVersionName,
|
|
4492
|
-
current: current,
|
|
4493
|
-
plannedDirectUpdate: plannedDirectUpdate
|
|
4494
|
-
)
|
|
4495
|
-
return
|
|
4496
4620
|
}
|
|
4497
4621
|
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
4498
4622
|
self.clearDownloadInProgressState()
|