@capgo/capacitor-updater 7.42.9 → 7.45.10
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/Package.swift +5 -2
- package/README.md +250 -77
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +682 -213
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +222 -35
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +49 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +38 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +13 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +476 -2
- package/dist/docs.json +402 -10
- package/dist/esm/definitions.d.ts +183 -22
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +3 -2
- package/dist/esm/web.js +6 -4
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +6 -4
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +6 -4
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +653 -140
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +213 -50
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +37 -16
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +2 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +347 -2
- package/package.json +12 -8
|
@@ -44,7 +44,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
44
44
|
CAPPluginMethod(name: "current", returnType: CAPPluginReturnPromise),
|
|
45
45
|
CAPPluginMethod(name: "reload", returnType: CAPPluginReturnPromise),
|
|
46
46
|
CAPPluginMethod(name: "notifyAppReady", returnType: CAPPluginReturnPromise),
|
|
47
|
-
CAPPluginMethod(name: "setDelay", returnType: CAPPluginReturnPromise),
|
|
48
47
|
CAPPluginMethod(name: "setMultiDelay", returnType: CAPPluginReturnPromise),
|
|
49
48
|
CAPPluginMethod(name: "cancelDelay", returnType: CAPPluginReturnPromise),
|
|
50
49
|
CAPPluginMethod(name: "getLatest", returnType: CAPPluginReturnPromise),
|
|
@@ -63,6 +62,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
63
62
|
CAPPluginMethod(name: "getFailedUpdate", returnType: CAPPluginReturnPromise),
|
|
64
63
|
CAPPluginMethod(name: "setShakeMenu", returnType: CAPPluginReturnPromise),
|
|
65
64
|
CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise),
|
|
65
|
+
CAPPluginMethod(name: "setShakeChannelSelector", returnType: CAPPluginReturnPromise),
|
|
66
|
+
CAPPluginMethod(name: "isShakeChannelSelectorEnabled", returnType: CAPPluginReturnPromise),
|
|
66
67
|
// App Store update methods
|
|
67
68
|
CAPPluginMethod(name: "getAppUpdateInfo", returnType: CAPPluginReturnPromise),
|
|
68
69
|
CAPPluginMethod(name: "openAppStore", returnType: CAPPluginReturnPromise),
|
|
@@ -71,7 +72,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
71
72
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
72
73
|
]
|
|
73
74
|
public var implementation = CapgoUpdater()
|
|
74
|
-
private let pluginVersion: String = "7.
|
|
75
|
+
private let pluginVersion: String = "7.45.10"
|
|
75
76
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
76
77
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
77
78
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -101,14 +102,23 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
101
102
|
private var autoSplashscreenTimeoutWorkItem: DispatchWorkItem?
|
|
102
103
|
private var splashscreenLoaderView: UIActivityIndicatorView?
|
|
103
104
|
private var splashscreenLoaderContainer: UIView?
|
|
105
|
+
private let splashscreenPluginName = "SplashScreen"
|
|
106
|
+
private let splashscreenRetryDelayMilliseconds = 100
|
|
107
|
+
private let splashscreenMaxRetries = 20
|
|
104
108
|
private var autoSplashscreenTimedOut = false
|
|
109
|
+
private var splashscreenInvocationToken = 0
|
|
105
110
|
private var autoDeleteFailed = false
|
|
106
111
|
private var autoDeletePrevious = false
|
|
107
|
-
|
|
112
|
+
var allowSetDefaultChannel = true
|
|
108
113
|
private var keepUrlPathAfterReload = false
|
|
109
114
|
private var backgroundWork: DispatchWorkItem?
|
|
110
115
|
private var taskRunning = false
|
|
111
116
|
private var periodCheckDelay = 0
|
|
117
|
+
private let downloadLock = NSLock()
|
|
118
|
+
private let onLaunchDirectUpdateStateLock = NSLock()
|
|
119
|
+
private var downloadInProgress = false
|
|
120
|
+
private var downloadStartTime: Date?
|
|
121
|
+
private let downloadTimeout: TimeInterval = 3600 // 1 hour timeout
|
|
112
122
|
|
|
113
123
|
// Lock to ensure cleanup completes before downloads start
|
|
114
124
|
private let cleanupLock = NSLock()
|
|
@@ -119,6 +129,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
119
129
|
private var allowManualBundleError = false
|
|
120
130
|
private var keepUrlPathFlagLastValue: Bool?
|
|
121
131
|
public var shakeMenuEnabled = false
|
|
132
|
+
public var shakeChannelSelectorEnabled = false
|
|
122
133
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
123
134
|
|
|
124
135
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
@@ -211,6 +222,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
211
222
|
implementation.timeout = Double(getConfig().getInt("responseTimeout", 20))
|
|
212
223
|
resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
|
|
213
224
|
shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
225
|
+
shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
214
226
|
let periodCheckDelayValue = getConfig().getInt("periodCheckDelay", 0)
|
|
215
227
|
if periodCheckDelayValue >= 0 && periodCheckDelayValue > 600 {
|
|
216
228
|
periodCheckDelay = 600
|
|
@@ -220,6 +232,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
220
232
|
|
|
221
233
|
implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
|
|
222
234
|
implementation.notifyDownloadRaw = notifyDownload
|
|
235
|
+
implementation.notifyListeners = { [weak self] eventName, data in
|
|
236
|
+
self?.notifyListeners(eventName, data: data)
|
|
237
|
+
}
|
|
223
238
|
implementation.pluginVersion = self.pluginVersion
|
|
224
239
|
|
|
225
240
|
// Set logger for shared classes
|
|
@@ -264,11 +279,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
264
279
|
}
|
|
265
280
|
self.implementation.autoReset()
|
|
266
281
|
|
|
267
|
-
// Check if app was recently installed/updated BEFORE
|
|
282
|
+
// Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
|
|
268
283
|
self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
|
|
269
284
|
|
|
270
285
|
if resetWhenUpdate {
|
|
271
|
-
self.
|
|
286
|
+
let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
|
|
287
|
+
self.cleanupObsoleteVersions(didResetCurrentBundle: didResetCurrentBundle)
|
|
272
288
|
}
|
|
273
289
|
|
|
274
290
|
// Load the server
|
|
@@ -384,7 +400,29 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
384
400
|
semaphoreReady.signal()
|
|
385
401
|
}
|
|
386
402
|
|
|
387
|
-
|
|
403
|
+
func storedNativeBuildVersion() -> String {
|
|
404
|
+
UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
func hasNativeBuildVersionChanged() -> Bool {
|
|
408
|
+
let previous = self.storedNativeBuildVersion()
|
|
409
|
+
return previous != "0" && self.currentBuildVersion != previous
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@discardableResult
|
|
413
|
+
func resetCurrentBundleForNativeBuildChangeIfNeeded() -> Bool {
|
|
414
|
+
let previous = self.storedNativeBuildVersion()
|
|
415
|
+
guard previous != "0" && self.currentBuildVersion != previous else {
|
|
416
|
+
return false
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Reset startup state synchronously so initialLoad() boots from the builtin bundle.
|
|
420
|
+
self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting startup bundle to builtin.")
|
|
421
|
+
self.implementation.reset(isInternal: true)
|
|
422
|
+
return true
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private func cleanupObsoleteVersions(didResetCurrentBundle: Bool = false) {
|
|
388
426
|
cleanupThread = Thread {
|
|
389
427
|
self.cleanupLock.lock()
|
|
390
428
|
defer {
|
|
@@ -419,9 +457,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
419
457
|
// 1. Write "LatestVersionNative" - this fixes the part 1 of this bug
|
|
420
458
|
// 2. Compare both keys. If any is not equal to "currentBuildVersion", then revert to builtin version. This fixes the part 2 of this bug
|
|
421
459
|
|
|
422
|
-
let previous =
|
|
460
|
+
let previous = self.storedNativeBuildVersion()
|
|
423
461
|
if previous != "0" && self.currentBuildVersion != previous {
|
|
424
|
-
|
|
462
|
+
if !didResetCurrentBundle {
|
|
463
|
+
self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting current bundle to builtin.")
|
|
464
|
+
self.implementation.reset(isInternal: true)
|
|
465
|
+
}
|
|
425
466
|
let res = self.implementation.list()
|
|
426
467
|
for version in res {
|
|
427
468
|
// Check if thread was cancelled
|
|
@@ -511,6 +552,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
511
552
|
call.resolve()
|
|
512
553
|
}
|
|
513
554
|
|
|
555
|
+
func getUpdateUrl() -> String {
|
|
556
|
+
return updateUrl
|
|
557
|
+
}
|
|
558
|
+
|
|
514
559
|
@objc func setStatsUrl(_ call: CAPPluginCall) {
|
|
515
560
|
if !getConfig().getBoolean("allowModifyUrl", false) {
|
|
516
561
|
logger.error("setStatsUrl called without allowModifyUrl")
|
|
@@ -636,47 +681,77 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
636
681
|
}
|
|
637
682
|
}
|
|
638
683
|
|
|
639
|
-
|
|
640
|
-
guard let bridge = self.bridge else { return false }
|
|
641
|
-
self.semaphoreUp()
|
|
684
|
+
private func currentReloadDestination() -> URL {
|
|
642
685
|
let id = self.implementation.getCurrentBundleId()
|
|
643
|
-
let dest: URL
|
|
644
686
|
if BundleInfo.ID_BUILTIN == id {
|
|
645
|
-
|
|
687
|
+
return Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
646
688
|
} else {
|
|
647
|
-
|
|
689
|
+
return self.implementation.getBundleDirectory(id: id)
|
|
648
690
|
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private func applyCurrentBundleToBridge(_ bridge: CAPBridgeProtocol) -> Bool {
|
|
694
|
+
let id = self.implementation.getCurrentBundleId()
|
|
695
|
+
let dest = self.currentReloadDestination()
|
|
649
696
|
logger.info("Reloading \(id)")
|
|
650
697
|
|
|
651
|
-
let
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
_ = vc.webView?.load(URLRequest(url: finalUrl))
|
|
669
|
-
} else {
|
|
670
|
-
self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
|
|
671
|
-
vc.setServerBasePath(path: dest.path)
|
|
672
|
-
}
|
|
698
|
+
guard let vc = bridge.viewController as? CAPBridgeViewController else {
|
|
699
|
+
self.logger.error("Cannot get viewController")
|
|
700
|
+
return false
|
|
701
|
+
}
|
|
702
|
+
guard let capBridge = vc.bridge else {
|
|
703
|
+
self.logger.error("Cannot get capBridge")
|
|
704
|
+
return false
|
|
705
|
+
}
|
|
706
|
+
if self.keepUrlPathAfterReload {
|
|
707
|
+
if let currentURL = vc.webView?.url {
|
|
708
|
+
capBridge.setServerBasePath(dest.path)
|
|
709
|
+
var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
|
|
710
|
+
urlComponents.path = currentURL.path
|
|
711
|
+
urlComponents.query = currentURL.query
|
|
712
|
+
urlComponents.fragment = currentURL.fragment
|
|
713
|
+
if let finalUrl = urlComponents.url {
|
|
714
|
+
_ = vc.webView?.load(URLRequest(url: finalUrl))
|
|
673
715
|
} else {
|
|
674
|
-
self.logger.error("
|
|
716
|
+
self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
|
|
675
717
|
vc.setServerBasePath(path: dest.path)
|
|
676
718
|
}
|
|
677
719
|
} else {
|
|
720
|
+
self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
|
|
678
721
|
vc.setServerBasePath(path: dest.path)
|
|
679
722
|
}
|
|
723
|
+
} else {
|
|
724
|
+
vc.setServerBasePath(path: dest.path)
|
|
725
|
+
}
|
|
726
|
+
return true
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
func restoreLiveBundleStateAfterFailedReload() {
|
|
730
|
+
guard let bridge = self.bridge else {
|
|
731
|
+
return
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
let restoreLiveState = {
|
|
735
|
+
_ = self.applyCurrentBundleToBridge(bridge)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if Thread.isMainThread {
|
|
739
|
+
restoreLiveState()
|
|
740
|
+
} else {
|
|
741
|
+
DispatchQueue.main.sync {
|
|
742
|
+
restoreLiveState()
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
public func _reload() -> Bool {
|
|
748
|
+
guard let bridge = self.bridge else { return false }
|
|
749
|
+
self.semaphoreUp()
|
|
750
|
+
|
|
751
|
+
let performReload: () -> Bool = {
|
|
752
|
+
guard self.applyCurrentBundleToBridge(bridge) else {
|
|
753
|
+
return false
|
|
754
|
+
}
|
|
680
755
|
self.checkAppReady()
|
|
681
756
|
self.notifyListeners("appReloaded", data: [:])
|
|
682
757
|
return true
|
|
@@ -694,6 +769,38 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
694
769
|
}
|
|
695
770
|
|
|
696
771
|
@objc func reload(_ call: CAPPluginCall) {
|
|
772
|
+
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
773
|
+
let next: BundleInfo? = self.implementation.getNextBundle()
|
|
774
|
+
|
|
775
|
+
if let next = next, !next.isErrorStatus(), next.getId() != current.getId() {
|
|
776
|
+
let previousState = self.implementation.captureResetState()
|
|
777
|
+
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
778
|
+
logger.info("Applying pending bundle before reload: \(next.toString())")
|
|
779
|
+
let didApplyPendingBundle: Bool
|
|
780
|
+
if next.isBuiltin() {
|
|
781
|
+
self.implementation.prepareResetStateForTransition()
|
|
782
|
+
didApplyPendingBundle = true
|
|
783
|
+
} else {
|
|
784
|
+
didApplyPendingBundle = self.implementation.stagePendingReload(bundle: next)
|
|
785
|
+
}
|
|
786
|
+
if didApplyPendingBundle && self._reload() {
|
|
787
|
+
if next.isBuiltin() {
|
|
788
|
+
self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: false)
|
|
789
|
+
} else {
|
|
790
|
+
self.implementation.finalizePendingReload(bundle: next, previousBundleName: previousBundleName)
|
|
791
|
+
}
|
|
792
|
+
self.notifyBundleSet(next)
|
|
793
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
794
|
+
call.resolve()
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
self.implementation.restoreResetState(previousState)
|
|
798
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
799
|
+
logger.error("Reload failed after applying pending bundle: \(next.toString())")
|
|
800
|
+
call.reject("Reload failed after applying pending bundle")
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
|
|
697
804
|
if self._reload() {
|
|
698
805
|
call.resolve()
|
|
699
806
|
} else {
|
|
@@ -728,8 +835,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
728
835
|
if !res {
|
|
729
836
|
logger.info("Bundle successfully set to: \(id) ")
|
|
730
837
|
call.reject("Update failed, id \(id) doesn't exist")
|
|
838
|
+
} else if !self._reload() {
|
|
839
|
+
call.reject("Reload failed after setting bundle \(id)")
|
|
731
840
|
} else {
|
|
732
|
-
self.
|
|
841
|
+
self.notifyBundleSet(self.implementation.getBundleInfo(id: id))
|
|
842
|
+
call.resolve()
|
|
733
843
|
}
|
|
734
844
|
}
|
|
735
845
|
|
|
@@ -792,12 +902,32 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
792
902
|
|
|
793
903
|
@objc func getLatest(_ call: CAPPluginCall) {
|
|
794
904
|
let channel = call.getString("channel")
|
|
795
|
-
|
|
905
|
+
runGetLatestWork {
|
|
796
906
|
let res = self.implementation.getLatest(url: URL(string: self.updateUrl)!, channel: channel)
|
|
797
|
-
if res.error
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
907
|
+
if let error = res.error, !error.isEmpty {
|
|
908
|
+
let responseKind = self.updateResponseKind(kind: res.kind)
|
|
909
|
+
res.kind = responseKind
|
|
910
|
+
if responseKind == "failed" {
|
|
911
|
+
call.reject(error)
|
|
912
|
+
} else {
|
|
913
|
+
if res.version.isEmpty {
|
|
914
|
+
res.version = self.implementation.getCurrentBundle().getVersionName()
|
|
915
|
+
}
|
|
916
|
+
call.resolve(res.toDict())
|
|
917
|
+
}
|
|
918
|
+
} else if let kind = res.kind, !kind.isEmpty {
|
|
919
|
+
let responseKind = self.updateResponseKind(kind: kind)
|
|
920
|
+
res.kind = responseKind
|
|
921
|
+
if responseKind != "failed" {
|
|
922
|
+
if res.version.isEmpty {
|
|
923
|
+
res.version = self.implementation.getCurrentBundle().getVersionName()
|
|
924
|
+
}
|
|
925
|
+
call.resolve(res.toDict())
|
|
926
|
+
} else {
|
|
927
|
+
call.reject(res.message ?? "server did not provide a message")
|
|
928
|
+
}
|
|
929
|
+
} else if let message = res.message, !message.isEmpty {
|
|
930
|
+
call.reject(message)
|
|
801
931
|
} else {
|
|
802
932
|
call.resolve(res.toDict())
|
|
803
933
|
}
|
|
@@ -817,7 +947,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
817
947
|
} else {
|
|
818
948
|
if self._isAutoUpdateEnabled() && triggerAutoUpdate {
|
|
819
949
|
self.logger.info("Calling autoupdater after channel change!")
|
|
820
|
-
|
|
950
|
+
// Check if download is already in progress (with timeout protection)
|
|
951
|
+
if !self.isDownloadStuckOrTimedOut() {
|
|
952
|
+
self.backgroundDownload()
|
|
953
|
+
} else {
|
|
954
|
+
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
955
|
+
}
|
|
821
956
|
}
|
|
822
957
|
call.resolve(res.toDict())
|
|
823
958
|
}
|
|
@@ -851,7 +986,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
851
986
|
} else {
|
|
852
987
|
if self._isAutoUpdateEnabled() && triggerAutoUpdate {
|
|
853
988
|
self.logger.info("Calling autoupdater after channel change!")
|
|
854
|
-
|
|
989
|
+
// Check if download is already in progress (with timeout protection)
|
|
990
|
+
if !self.isDownloadStuckOrTimedOut() {
|
|
991
|
+
self.backgroundDownload()
|
|
992
|
+
} else {
|
|
993
|
+
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
994
|
+
}
|
|
855
995
|
}
|
|
856
996
|
call.resolve(res.toDict())
|
|
857
997
|
}
|
|
@@ -904,32 +1044,90 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
904
1044
|
call.resolve()
|
|
905
1045
|
}
|
|
906
1046
|
|
|
907
|
-
@objc func _reset(toLastSuccessful: Bool) -> Bool {
|
|
908
|
-
|
|
1047
|
+
@objc func _reset(toLastSuccessful: Bool, usePendingBundle: Bool) -> Bool {
|
|
1048
|
+
self.performReset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle, isInternal: false)
|
|
1049
|
+
}
|
|
909
1050
|
|
|
910
|
-
|
|
911
|
-
|
|
1051
|
+
func performReset(toLastSuccessful: Bool, usePendingBundle: Bool, isInternal: Bool) -> Bool {
|
|
1052
|
+
guard self.canPerformResetTransition() else { return false }
|
|
912
1053
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
return self.implementation.set(bundle: fallback) && self._reload()
|
|
918
|
-
}
|
|
1054
|
+
let fallback: BundleInfo = self.implementation.getFallbackBundle()
|
|
1055
|
+
let pending: BundleInfo? = self.implementation.getNextBundle()
|
|
1056
|
+
let previousState = self.implementation.captureResetState()
|
|
1057
|
+
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
919
1058
|
|
|
920
|
-
|
|
1059
|
+
if usePendingBundle {
|
|
1060
|
+
guard let pending = pending, !pending.isErrorStatus() else {
|
|
1061
|
+
logger.error("No pending bundle available to reset to")
|
|
1062
|
+
return false
|
|
1063
|
+
}
|
|
1064
|
+
guard self.implementation.canSet(bundle: pending) else {
|
|
1065
|
+
logger.error("Pending bundle is not installable")
|
|
1066
|
+
return false
|
|
1067
|
+
}
|
|
1068
|
+
self.implementation.prepareResetStateForTransition()
|
|
1069
|
+
logger.info("Resetting to pending bundle: \(pending.toString())")
|
|
1070
|
+
let didApplyPendingBundle: Bool
|
|
1071
|
+
if pending.isBuiltin() {
|
|
1072
|
+
didApplyPendingBundle = true
|
|
1073
|
+
} else {
|
|
1074
|
+
didApplyPendingBundle = self.implementation.set(bundle: pending)
|
|
1075
|
+
}
|
|
1076
|
+
if didApplyPendingBundle && self._reload() {
|
|
1077
|
+
self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
|
|
1078
|
+
self.notifyBundleSet(pending)
|
|
1079
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
1080
|
+
return true
|
|
1081
|
+
}
|
|
1082
|
+
self.implementation.restoreResetState(previousState)
|
|
1083
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
1084
|
+
return false
|
|
1085
|
+
}
|
|
921
1086
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1087
|
+
// If developer wants to reset to the last successful bundle, and that bundle is not
|
|
1088
|
+
// the built-in bundle, set it as the bundle to use and reload.
|
|
1089
|
+
if toLastSuccessful && !fallback.isBuiltin() {
|
|
1090
|
+
if self.implementation.canSet(bundle: fallback) {
|
|
1091
|
+
self.implementation.prepareResetStateForTransition()
|
|
1092
|
+
logger.info("Resetting to: \(fallback.toString())")
|
|
1093
|
+
if self.implementation.set(bundle: fallback) && self._reload() {
|
|
1094
|
+
self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
|
|
1095
|
+
self.notifyBundleSet(fallback)
|
|
1096
|
+
return true
|
|
1097
|
+
}
|
|
1098
|
+
if !isInternal {
|
|
1099
|
+
self.implementation.restoreResetState(previousState)
|
|
1100
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
1101
|
+
return false
|
|
1102
|
+
}
|
|
1103
|
+
logger.warn("Fallback reload failed during internal reset, resetting to builtin instead")
|
|
1104
|
+
} else {
|
|
1105
|
+
logger.warn("Fallback bundle is not installable, resetting to builtin instead")
|
|
1106
|
+
}
|
|
925
1107
|
}
|
|
926
1108
|
|
|
1109
|
+
self.implementation.prepareResetStateForTransition()
|
|
1110
|
+
logger.info("Resetting to builtin version")
|
|
1111
|
+
if self._reload() {
|
|
1112
|
+
self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
|
|
1113
|
+
return true
|
|
1114
|
+
}
|
|
1115
|
+
if !isInternal {
|
|
1116
|
+
self.implementation.restoreResetState(previousState)
|
|
1117
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
1118
|
+
}
|
|
927
1119
|
return false
|
|
928
1120
|
}
|
|
929
1121
|
|
|
1122
|
+
func canPerformResetTransition() -> Bool {
|
|
1123
|
+
guard let bridge = self.bridge else { return false }
|
|
1124
|
+
return (bridge.viewController as? CAPBridgeViewController) != nil
|
|
1125
|
+
}
|
|
1126
|
+
|
|
930
1127
|
@objc func reset(_ call: CAPPluginCall) {
|
|
931
1128
|
let toLastSuccessful = call.getBool("toLastSuccessful") ?? false
|
|
932
|
-
|
|
1129
|
+
let usePendingBundle = call.getBool("usePendingBundle") ?? false
|
|
1130
|
+
if self._reset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle) {
|
|
933
1131
|
call.resolve()
|
|
934
1132
|
} else {
|
|
935
1133
|
logger.error("Reset failed")
|
|
@@ -1048,7 +1246,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1048
1246
|
self.persistLastFailedBundle(current)
|
|
1049
1247
|
self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
|
|
1050
1248
|
self.implementation.setError(bundle: current)
|
|
1051
|
-
_ = self.
|
|
1249
|
+
_ = self.performReset(toLastSuccessful: true, usePendingBundle: false, isInternal: true)
|
|
1052
1250
|
if self.autoDeleteFailed && !current.isBuiltin() {
|
|
1053
1251
|
logger.info("Deleting failing bundle: \(current.toString())")
|
|
1054
1252
|
let res = self.implementation.delete(id: current.getId(), removeInfo: false)
|
|
@@ -1073,6 +1271,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1073
1271
|
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
|
|
1074
1272
|
}
|
|
1075
1273
|
|
|
1274
|
+
private func notifyBundleSet(_ bundle: BundleInfo) {
|
|
1275
|
+
self.notifyListeners("set", data: ["bundle": bundle.toJSON()], retainUntilConsumed: true)
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1076
1278
|
func sendReadyToJs(current: BundleInfo, msg: String) {
|
|
1077
1279
|
logger.info("sendReadyToJs")
|
|
1078
1280
|
DispatchQueue.global().async {
|
|
@@ -1100,32 +1302,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1100
1302
|
private func performHideSplashscreen() {
|
|
1101
1303
|
self.cancelSplashscreenTimeout()
|
|
1102
1304
|
self.removeSplashscreenLoader()
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
self.logger.info("Splashscreen hidden automatically")
|
|
1112
|
-
}, error: { (_) in
|
|
1113
|
-
self.logger.error("Failed to auto-hide splashscreen")
|
|
1114
|
-
})
|
|
1115
|
-
|
|
1116
|
-
// Try to call the SplashScreen hide method directly through the bridge
|
|
1117
|
-
if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
|
|
1118
|
-
// Use runtime method invocation to call hide method
|
|
1119
|
-
let selector = NSSelectorFromString("hide:")
|
|
1120
|
-
if splashScreenPlugin.responds(to: selector) {
|
|
1121
|
-
_ = splashScreenPlugin.perform(selector, with: call)
|
|
1122
|
-
self.logger.info("Called SplashScreen hide method")
|
|
1123
|
-
} else {
|
|
1124
|
-
self.logger.warn("autoSplashscreen: SplashScreen plugin does not respond to hide: method. Make sure @capacitor/splash-screen plugin is properly installed.")
|
|
1125
|
-
}
|
|
1126
|
-
} else {
|
|
1127
|
-
self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
|
|
1128
|
-
}
|
|
1305
|
+
self.splashscreenInvocationToken += 1
|
|
1306
|
+
self.invokeSplashscreenMethod(
|
|
1307
|
+
methodName: "hide",
|
|
1308
|
+
callbackId: "autoHideSplashscreen",
|
|
1309
|
+
options: self.splashscreenOptions(methodName: "hide"),
|
|
1310
|
+
retriesRemaining: self.splashscreenMaxRetries,
|
|
1311
|
+
requestToken: self.splashscreenInvocationToken
|
|
1312
|
+
)
|
|
1129
1313
|
}
|
|
1130
1314
|
|
|
1131
1315
|
private func showSplashscreen() {
|
|
@@ -1141,35 +1325,132 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1141
1325
|
private func performShowSplashscreen() {
|
|
1142
1326
|
self.cancelSplashscreenTimeout()
|
|
1143
1327
|
self.autoSplashscreenTimedOut = false
|
|
1328
|
+
self.splashscreenInvocationToken += 1
|
|
1329
|
+
self.invokeSplashscreenMethod(
|
|
1330
|
+
methodName: "show",
|
|
1331
|
+
callbackId: "autoShowSplashscreen",
|
|
1332
|
+
options: self.splashscreenOptions(methodName: "show"),
|
|
1333
|
+
retriesRemaining: self.splashscreenMaxRetries,
|
|
1334
|
+
requestToken: self.splashscreenInvocationToken
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
self.addSplashscreenLoaderIfNeeded()
|
|
1338
|
+
self.scheduleSplashscreenTimeout()
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
private func splashscreenOptions(methodName: String) -> [String: Any] {
|
|
1342
|
+
methodName == "show" ? ["autoHide": false] : [:]
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
private func splashscreenCompletedMessage(methodName: String) -> String {
|
|
1346
|
+
methodName == "show" ? "Splashscreen shown automatically" : "Splashscreen hidden automatically"
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
func splashscreenOptionsForTesting(methodName: String) -> [String: Any] {
|
|
1350
|
+
self.splashscreenOptions(methodName: methodName)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
func isCurrentSplashscreenInvocationTokenForTesting(_ requestToken: Int) -> Bool {
|
|
1354
|
+
requestToken == self.splashscreenInvocationToken
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
func advanceSplashscreenInvocationTokenForTesting() {
|
|
1358
|
+
self.splashscreenInvocationToken += 1
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
private func makeSplashscreenCall(callbackId: String, options: [String: Any], methodName: String) -> CAPPluginCall {
|
|
1362
|
+
CAPPluginCall(callbackId: callbackId, options: options, success: { [weak self] (_, _) in
|
|
1363
|
+
guard let self = self else { return }
|
|
1364
|
+
self.logger.info(self.splashscreenCompletedMessage(methodName: methodName))
|
|
1365
|
+
}, error: { [weak self] (_) in
|
|
1366
|
+
guard let self = self else { return }
|
|
1367
|
+
self.logger.error("Failed to auto-\(methodName) splashscreen")
|
|
1368
|
+
})
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
private func invokeSplashscreenMethod(
|
|
1372
|
+
methodName: String,
|
|
1373
|
+
callbackId: String,
|
|
1374
|
+
options: [String: Any],
|
|
1375
|
+
retriesRemaining: Int,
|
|
1376
|
+
requestToken: Int
|
|
1377
|
+
) {
|
|
1378
|
+
guard requestToken == self.splashscreenInvocationToken else {
|
|
1379
|
+
return
|
|
1380
|
+
}
|
|
1144
1381
|
|
|
1145
1382
|
guard let bridge = self.bridge else {
|
|
1146
|
-
self.
|
|
1383
|
+
self.retrySplashscreenMethod(
|
|
1384
|
+
methodName: methodName,
|
|
1385
|
+
callbackId: callbackId,
|
|
1386
|
+
options: options,
|
|
1387
|
+
retriesRemaining: retriesRemaining,
|
|
1388
|
+
requestToken: requestToken,
|
|
1389
|
+
message: "Bridge not available for \(methodName == "show" ? "showing" : "hiding") splashscreen with autoSplashscreen"
|
|
1390
|
+
)
|
|
1147
1391
|
return
|
|
1148
1392
|
}
|
|
1149
1393
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1394
|
+
guard let splashScreenPlugin = bridge.plugin(withName: self.splashscreenPluginName) else {
|
|
1395
|
+
self.retrySplashscreenMethod(
|
|
1396
|
+
methodName: methodName,
|
|
1397
|
+
callbackId: callbackId,
|
|
1398
|
+
options: options,
|
|
1399
|
+
retriesRemaining: retriesRemaining,
|
|
1400
|
+
requestToken: requestToken,
|
|
1401
|
+
message: "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
|
|
1402
|
+
)
|
|
1403
|
+
return
|
|
1404
|
+
}
|
|
1156
1405
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1406
|
+
let selector = NSSelectorFromString("\(methodName):")
|
|
1407
|
+
guard splashScreenPlugin.responds(to: selector) else {
|
|
1408
|
+
self.retrySplashscreenMethod(
|
|
1409
|
+
methodName: methodName,
|
|
1410
|
+
callbackId: callbackId,
|
|
1411
|
+
options: options,
|
|
1412
|
+
retriesRemaining: retriesRemaining,
|
|
1413
|
+
requestToken: requestToken,
|
|
1414
|
+
message: "autoSplashscreen: SplashScreen plugin does not respond to \(methodName): method. Make sure @capacitor/splash-screen plugin is properly installed."
|
|
1415
|
+
)
|
|
1416
|
+
return
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
let call = self.makeSplashscreenCall(callbackId: callbackId, options: options, methodName: methodName)
|
|
1420
|
+
_ = splashScreenPlugin.perform(selector, with: call)
|
|
1421
|
+
self.logger.info("Called SplashScreen \(methodName) method")
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
private func retrySplashscreenMethod(
|
|
1425
|
+
methodName: String,
|
|
1426
|
+
callbackId: String,
|
|
1427
|
+
options: [String: Any],
|
|
1428
|
+
retriesRemaining: Int,
|
|
1429
|
+
requestToken: Int,
|
|
1430
|
+
message: String
|
|
1431
|
+
) {
|
|
1432
|
+
guard retriesRemaining > 0 else {
|
|
1433
|
+
if methodName == "show" {
|
|
1434
|
+
self.logger.warn(message)
|
|
1164
1435
|
} else {
|
|
1165
|
-
self.logger.
|
|
1436
|
+
self.logger.error(message)
|
|
1166
1437
|
}
|
|
1167
|
-
|
|
1168
|
-
self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
|
|
1438
|
+
return
|
|
1169
1439
|
}
|
|
1170
1440
|
|
|
1171
|
-
self.
|
|
1172
|
-
|
|
1441
|
+
self.logger.info("\(message). Retrying.")
|
|
1442
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.splashscreenRetryDelayMilliseconds)) { [weak self] in
|
|
1443
|
+
guard let self = self, requestToken == self.splashscreenInvocationToken else {
|
|
1444
|
+
return
|
|
1445
|
+
}
|
|
1446
|
+
self.invokeSplashscreenMethod(
|
|
1447
|
+
methodName: methodName,
|
|
1448
|
+
callbackId: callbackId,
|
|
1449
|
+
options: options,
|
|
1450
|
+
retriesRemaining: retriesRemaining - 1,
|
|
1451
|
+
requestToken: requestToken
|
|
1452
|
+
)
|
|
1453
|
+
}
|
|
1173
1454
|
}
|
|
1174
1455
|
|
|
1175
1456
|
private func addSplashscreenLoaderIfNeeded() {
|
|
@@ -1323,7 +1604,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1323
1604
|
}
|
|
1324
1605
|
return false
|
|
1325
1606
|
case "onLaunch":
|
|
1326
|
-
if !self.
|
|
1607
|
+
if !self.getOnLaunchDirectUpdateUsed() {
|
|
1327
1608
|
return true
|
|
1328
1609
|
}
|
|
1329
1610
|
return false
|
|
@@ -1333,6 +1614,51 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1333
1614
|
}
|
|
1334
1615
|
}
|
|
1335
1616
|
|
|
1617
|
+
static func shouldConsumeOnLaunchDirectUpdate(directUpdateMode: String, plannedDirectUpdate: Bool) -> Bool {
|
|
1618
|
+
plannedDirectUpdate && directUpdateMode == "onLaunch"
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
private func getOnLaunchDirectUpdateUsed() -> Bool {
|
|
1622
|
+
self.onLaunchDirectUpdateStateLock.lock()
|
|
1623
|
+
defer { self.onLaunchDirectUpdateStateLock.unlock() }
|
|
1624
|
+
return self.onLaunchDirectUpdateUsed
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
private func setOnLaunchDirectUpdateUsed(_ used: Bool) {
|
|
1628
|
+
self.onLaunchDirectUpdateStateLock.lock()
|
|
1629
|
+
self.onLaunchDirectUpdateUsed = used
|
|
1630
|
+
self.onLaunchDirectUpdateStateLock.unlock()
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
private func consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: Bool) {
|
|
1634
|
+
guard Self.shouldConsumeOnLaunchDirectUpdate(directUpdateMode: self.directUpdateMode, plannedDirectUpdate: plannedDirectUpdate) else {
|
|
1635
|
+
return
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
self.setOnLaunchDirectUpdateUsed(true)
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
func configureDirectUpdateModeForTesting(_ directUpdateMode: String, onLaunchDirectUpdateUsed: Bool = false) {
|
|
1642
|
+
self.directUpdateMode = directUpdateMode
|
|
1643
|
+
self.setOnLaunchDirectUpdateUsed(onLaunchDirectUpdateUsed)
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
func setUpdateUrlForTesting(_ updateUrl: String) {
|
|
1647
|
+
self.updateUrl = updateUrl
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
func setCurrentBuildVersionForTesting(_ currentBuildVersion: String) {
|
|
1651
|
+
self.currentBuildVersion = currentBuildVersion
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
func shouldUseDirectUpdateForTesting() -> Bool {
|
|
1655
|
+
self.shouldUseDirectUpdate()
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
var hasConsumedOnLaunchDirectUpdateForTesting: Bool {
|
|
1659
|
+
self.getOnLaunchDirectUpdateUsed()
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1336
1662
|
private func notifyBreakingEvents(version: String) {
|
|
1337
1663
|
guard !version.isEmpty else {
|
|
1338
1664
|
return
|
|
@@ -1342,15 +1668,71 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1342
1668
|
self.notifyListeners("majorAvailable", data: payload)
|
|
1343
1669
|
}
|
|
1344
1670
|
|
|
1671
|
+
private func updateResponseKind(kind: String?) -> String {
|
|
1672
|
+
if let kind, ["up_to_date", "blocked", "failed"].contains(kind) {
|
|
1673
|
+
return kind
|
|
1674
|
+
}
|
|
1675
|
+
return "failed"
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
private func endBackgroundDownloadAfterLatestError(
|
|
1679
|
+
backendError: String,
|
|
1680
|
+
res: AppVersion,
|
|
1681
|
+
current: BundleInfo,
|
|
1682
|
+
plannedDirectUpdate: Bool
|
|
1683
|
+
) {
|
|
1684
|
+
let statusCode = res.statusCode
|
|
1685
|
+
let responseKind = self.updateResponseKind(kind: res.kind)
|
|
1686
|
+
let responseMessage = res.message?.isEmpty == false ? res.message : nil
|
|
1687
|
+
let message = responseMessage ?? (backendError.isEmpty ? "server did not provide a message" : backendError)
|
|
1688
|
+
let latestVersionName = res.version.isEmpty ? current.getVersionName() : res.version
|
|
1689
|
+
self.notifyListeners("updateCheckResult", data: [
|
|
1690
|
+
"kind": responseKind,
|
|
1691
|
+
"error": backendError,
|
|
1692
|
+
"message": message,
|
|
1693
|
+
"statusCode": statusCode,
|
|
1694
|
+
"version": latestVersionName,
|
|
1695
|
+
"bundle": current.toJSON()
|
|
1696
|
+
])
|
|
1697
|
+
|
|
1698
|
+
if responseKind == "up_to_date" {
|
|
1699
|
+
self.logger.info("No new version available")
|
|
1700
|
+
} else if responseKind == "blocked" {
|
|
1701
|
+
self.logger.info("Update check blocked with error: \(backendError)")
|
|
1702
|
+
} else {
|
|
1703
|
+
self.logger.error("getLatest failed with error: \(backendError)")
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
let isFailure = responseKind == "failed"
|
|
1707
|
+
self.endBackGroundTaskWithNotif(
|
|
1708
|
+
msg: message,
|
|
1709
|
+
latestVersionName: latestVersionName,
|
|
1710
|
+
current: current,
|
|
1711
|
+
error: isFailure,
|
|
1712
|
+
plannedDirectUpdate: plannedDirectUpdate,
|
|
1713
|
+
sendStats: isFailure
|
|
1714
|
+
)
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1345
1717
|
func endBackGroundTaskWithNotif(
|
|
1346
1718
|
msg: String,
|
|
1347
1719
|
latestVersionName: String,
|
|
1348
1720
|
current: BundleInfo,
|
|
1349
1721
|
error: Bool = true,
|
|
1722
|
+
plannedDirectUpdate: Bool = false,
|
|
1350
1723
|
failureAction: String = "download_fail",
|
|
1351
1724
|
failureEvent: String = "downloadFailed",
|
|
1352
1725
|
sendStats: Bool = true
|
|
1353
1726
|
) {
|
|
1727
|
+
// Clear download in progress flag - this is called at the end of every download attempt
|
|
1728
|
+
// whether it succeeds, fails, or is skipped (e.g., already up to date)
|
|
1729
|
+
downloadLock.lock()
|
|
1730
|
+
defer { downloadLock.unlock() }
|
|
1731
|
+
downloadInProgress = false
|
|
1732
|
+
downloadStartTime = nil
|
|
1733
|
+
|
|
1734
|
+
self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
|
|
1735
|
+
|
|
1354
1736
|
if error {
|
|
1355
1737
|
if sendStats {
|
|
1356
1738
|
self.implementation.sendStats(action: failureAction, versionName: current.getVersionName())
|
|
@@ -1363,14 +1745,56 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1363
1745
|
self.endBackGroundTask()
|
|
1364
1746
|
}
|
|
1365
1747
|
|
|
1748
|
+
private func isDownloadStuckOrTimedOut() -> Bool {
|
|
1749
|
+
downloadLock.lock()
|
|
1750
|
+
defer { downloadLock.unlock() }
|
|
1751
|
+
|
|
1752
|
+
guard downloadInProgress else {
|
|
1753
|
+
return false
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Check if download has timed out
|
|
1757
|
+
if let startTime = downloadStartTime {
|
|
1758
|
+
let elapsed = Date().timeIntervalSince(startTime)
|
|
1759
|
+
if elapsed > downloadTimeout {
|
|
1760
|
+
self.logger.warn("Download has been in progress for \(elapsed) seconds, exceeding timeout of \(downloadTimeout) seconds. Clearing stuck state.")
|
|
1761
|
+
downloadInProgress = false
|
|
1762
|
+
downloadStartTime = nil
|
|
1763
|
+
return false // Now it's not stuck anymore, caller can proceed
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
return true
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
|
|
1771
|
+
DispatchQueue.global(qos: .background).async(execute: work)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
func runGetLatestWork(_ work: @escaping () -> Void) {
|
|
1775
|
+
DispatchQueue.global(qos: .background).async(execute: work)
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1366
1778
|
func backgroundDownload() {
|
|
1779
|
+
// Set download in progress flag (thread-safe)
|
|
1780
|
+
downloadLock.lock()
|
|
1781
|
+
downloadInProgress = true
|
|
1782
|
+
downloadStartTime = Date()
|
|
1783
|
+
downloadLock.unlock()
|
|
1784
|
+
|
|
1367
1785
|
let plannedDirectUpdate = self.shouldUseDirectUpdate()
|
|
1368
1786
|
let messageUpdate = plannedDirectUpdate ? "Update will occur now." : "Update will occur next time app moves to background."
|
|
1369
1787
|
guard let url = URL(string: self.updateUrl) else {
|
|
1370
1788
|
logger.error("Error no url or wrong format")
|
|
1789
|
+
// Clear the flag if we return early
|
|
1790
|
+
downloadLock.lock()
|
|
1791
|
+
defer { downloadLock.unlock() }
|
|
1792
|
+
downloadInProgress = false
|
|
1793
|
+
downloadStartTime = nil
|
|
1371
1794
|
return
|
|
1372
1795
|
}
|
|
1373
|
-
|
|
1796
|
+
|
|
1797
|
+
self.runBackgroundDownloadWork {
|
|
1374
1798
|
// Wait for cleanup to complete before starting download
|
|
1375
1799
|
self.waitForCleanupIfNeeded()
|
|
1376
1800
|
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Download Tasks") {
|
|
@@ -1382,16 +1806,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1382
1806
|
let current = self.implementation.getCurrentBundle()
|
|
1383
1807
|
|
|
1384
1808
|
// Handle network errors and other failures first
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
latestVersionName: res.version,
|
|
1809
|
+
let backendError = res.error ?? ""
|
|
1810
|
+
let backendKind = res.kind ?? ""
|
|
1811
|
+
if !backendError.isEmpty || !backendKind.isEmpty {
|
|
1812
|
+
self.endBackgroundDownloadAfterLatestError(
|
|
1813
|
+
backendError: backendError,
|
|
1814
|
+
res: res,
|
|
1392
1815
|
current: current,
|
|
1393
|
-
|
|
1394
|
-
sendStats: !responseIsOk
|
|
1816
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1395
1817
|
)
|
|
1396
1818
|
return
|
|
1397
1819
|
}
|
|
@@ -1400,26 +1822,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1400
1822
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
1401
1823
|
if directUpdateAllowed {
|
|
1402
1824
|
self.logger.info("Direct update to builtin version")
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1825
|
+
_ = self._reset(toLastSuccessful: false, usePendingBundle: false)
|
|
1826
|
+
self.endBackGroundTaskWithNotif(
|
|
1827
|
+
msg: "Updated to builtin version",
|
|
1828
|
+
latestVersionName: res.version,
|
|
1829
|
+
current: self.implementation.getCurrentBundle(),
|
|
1830
|
+
error: false,
|
|
1831
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1832
|
+
)
|
|
1409
1833
|
} else {
|
|
1410
1834
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
1411
1835
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will apply later.")
|
|
1412
1836
|
}
|
|
1413
1837
|
self.logger.info("Setting next bundle to builtin")
|
|
1414
1838
|
_ = self.implementation.setNextBundle(next: BundleInfo.ID_BUILTIN)
|
|
1415
|
-
self.endBackGroundTaskWithNotif(
|
|
1839
|
+
self.endBackGroundTaskWithNotif(
|
|
1840
|
+
msg: "Next update will be to builtin version",
|
|
1841
|
+
latestVersionName: res.version,
|
|
1842
|
+
current: current,
|
|
1843
|
+
error: false,
|
|
1844
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1845
|
+
)
|
|
1416
1846
|
}
|
|
1417
1847
|
return
|
|
1418
1848
|
}
|
|
1419
1849
|
let sessionKey = res.sessionKey ?? ""
|
|
1420
1850
|
guard let downloadUrl = URL(string: res.url) else {
|
|
1421
1851
|
self.logger.error("Error no url or wrong format")
|
|
1422
|
-
self.endBackGroundTaskWithNotif(
|
|
1852
|
+
self.endBackGroundTaskWithNotif(
|
|
1853
|
+
msg: "Error no url or wrong format",
|
|
1854
|
+
latestVersionName: res.version,
|
|
1855
|
+
current: current,
|
|
1856
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1857
|
+
)
|
|
1423
1858
|
return
|
|
1424
1859
|
}
|
|
1425
1860
|
let latestVersionName = res.version
|
|
@@ -1437,6 +1872,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1437
1872
|
self.logger.error("Failed to delete failed bundle: \(nextImpl!.toString())")
|
|
1438
1873
|
}
|
|
1439
1874
|
}
|
|
1875
|
+
self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
|
|
1440
1876
|
if res.manifest != nil {
|
|
1441
1877
|
nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
|
|
1442
1878
|
} else {
|
|
@@ -1445,12 +1881,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1445
1881
|
}
|
|
1446
1882
|
guard let next = nextImpl else {
|
|
1447
1883
|
self.logger.error("Error downloading file")
|
|
1448
|
-
self.endBackGroundTaskWithNotif(
|
|
1884
|
+
self.endBackGroundTaskWithNotif(
|
|
1885
|
+
msg: "Error downloading file",
|
|
1886
|
+
latestVersionName: latestVersionName,
|
|
1887
|
+
current: current,
|
|
1888
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1889
|
+
)
|
|
1449
1890
|
return
|
|
1450
1891
|
}
|
|
1451
1892
|
if next.isErrorStatus() {
|
|
1452
1893
|
self.logger.error("Latest bundle already exists and is in error state. Aborting update.")
|
|
1453
|
-
self.endBackGroundTaskWithNotif(
|
|
1894
|
+
self.endBackGroundTaskWithNotif(
|
|
1895
|
+
msg: "Latest version is in error state. Aborting update.",
|
|
1896
|
+
latestVersionName: latestVersionName,
|
|
1897
|
+
current: current,
|
|
1898
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1899
|
+
)
|
|
1454
1900
|
return
|
|
1455
1901
|
}
|
|
1456
1902
|
res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
|
|
@@ -1464,7 +1910,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1464
1910
|
if !resDel {
|
|
1465
1911
|
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1466
1912
|
}
|
|
1467
|
-
self.endBackGroundTaskWithNotif(
|
|
1913
|
+
self.endBackGroundTaskWithNotif(
|
|
1914
|
+
msg: "Error checksum",
|
|
1915
|
+
latestVersionName: latestVersionName,
|
|
1916
|
+
current: current,
|
|
1917
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1918
|
+
)
|
|
1468
1919
|
return
|
|
1469
1920
|
}
|
|
1470
1921
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
@@ -1477,34 +1928,67 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1477
1928
|
}
|
|
1478
1929
|
if !delayConditionList.isEmpty {
|
|
1479
1930
|
self.logger.info("Update delayed until delay conditions met")
|
|
1480
|
-
self.endBackGroundTaskWithNotif(
|
|
1931
|
+
self.endBackGroundTaskWithNotif(
|
|
1932
|
+
msg: "Update delayed until delay conditions met",
|
|
1933
|
+
latestVersionName: latestVersionName,
|
|
1934
|
+
current: next,
|
|
1935
|
+
error: false,
|
|
1936
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1937
|
+
)
|
|
1481
1938
|
return
|
|
1482
1939
|
}
|
|
1483
|
-
if self.
|
|
1484
|
-
self.
|
|
1485
|
-
self.
|
|
1940
|
+
if self.implementation.set(bundle: next) && self._reload() {
|
|
1941
|
+
self.notifyBundleSet(next)
|
|
1942
|
+
self.endBackGroundTaskWithNotif(
|
|
1943
|
+
msg: "update installed",
|
|
1944
|
+
latestVersionName: latestVersionName,
|
|
1945
|
+
current: next,
|
|
1946
|
+
error: false,
|
|
1947
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1948
|
+
)
|
|
1949
|
+
} else {
|
|
1950
|
+
self.endBackGroundTaskWithNotif(
|
|
1951
|
+
msg: "Update install failed",
|
|
1952
|
+
latestVersionName: latestVersionName,
|
|
1953
|
+
current: next,
|
|
1954
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1955
|
+
)
|
|
1486
1956
|
}
|
|
1487
|
-
_ = self.implementation.set(bundle: next)
|
|
1488
|
-
_ = self._reload()
|
|
1489
|
-
self.endBackGroundTaskWithNotif(msg: "update installed", latestVersionName: latestVersionName, current: next, error: false)
|
|
1490
1957
|
} else {
|
|
1491
1958
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
1492
1959
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
1493
1960
|
}
|
|
1494
1961
|
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
1495
1962
|
_ = self.implementation.setNextBundle(next: next.getId())
|
|
1496
|
-
self.endBackGroundTaskWithNotif(
|
|
1963
|
+
self.endBackGroundTaskWithNotif(
|
|
1964
|
+
msg: "update downloaded, will install next background",
|
|
1965
|
+
latestVersionName: latestVersionName,
|
|
1966
|
+
current: current,
|
|
1967
|
+
error: false,
|
|
1968
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1969
|
+
)
|
|
1497
1970
|
}
|
|
1498
1971
|
return
|
|
1499
1972
|
} catch {
|
|
1500
1973
|
self.logger.error("Error downloading file \(error.localizedDescription)")
|
|
1501
1974
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1502
|
-
self.endBackGroundTaskWithNotif(
|
|
1975
|
+
self.endBackGroundTaskWithNotif(
|
|
1976
|
+
msg: "Error downloading file",
|
|
1977
|
+
latestVersionName: latestVersionName,
|
|
1978
|
+
current: current,
|
|
1979
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1980
|
+
)
|
|
1503
1981
|
return
|
|
1504
1982
|
}
|
|
1505
1983
|
} else {
|
|
1506
1984
|
self.logger.info("No need to update, \(current.getId()) is the latest bundle.")
|
|
1507
|
-
self.endBackGroundTaskWithNotif(
|
|
1985
|
+
self.endBackGroundTaskWithNotif(
|
|
1986
|
+
msg: "No need to update, \(current.getId()) is the latest bundle.",
|
|
1987
|
+
latestVersionName: latestVersionName,
|
|
1988
|
+
current: current,
|
|
1989
|
+
error: false,
|
|
1990
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1991
|
+
)
|
|
1508
1992
|
return
|
|
1509
1993
|
}
|
|
1510
1994
|
}
|
|
@@ -1528,6 +2012,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1528
2012
|
logger.info("Next bundle is: \(next!.toString())")
|
|
1529
2013
|
if self.implementation.set(bundle: next!) && self._reload() {
|
|
1530
2014
|
logger.info("Updated to bundle: \(next!.toString())")
|
|
2015
|
+
self.notifyBundleSet(next!)
|
|
1531
2016
|
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
1532
2017
|
} else {
|
|
1533
2018
|
logger.error("Update to bundle: \(next!.toString()) Failed!")
|
|
@@ -1563,7 +2048,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1563
2048
|
logger.info("Background Timer Task canceled, Activity resumed before timer completes")
|
|
1564
2049
|
}
|
|
1565
2050
|
if self._isAutoUpdateEnabled() {
|
|
1566
|
-
|
|
2051
|
+
// Check if download is already in progress (with timeout protection)
|
|
2052
|
+
if !isDownloadStuckOrTimedOut() {
|
|
2053
|
+
self.backgroundDownload()
|
|
2054
|
+
} else {
|
|
2055
|
+
logger.info("Download already in progress, skipping duplicate download request")
|
|
2056
|
+
}
|
|
1567
2057
|
} else {
|
|
1568
2058
|
let instanceDescriptor = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor()
|
|
1569
2059
|
if instanceDescriptor?.serverURL != nil {
|
|
@@ -1600,7 +2090,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1600
2090
|
|
|
1601
2091
|
if res.version != current.getVersionName() {
|
|
1602
2092
|
self.logger.info("New version found: \(res.version)")
|
|
1603
|
-
|
|
2093
|
+
// Check if download is already in progress (with timeout protection)
|
|
2094
|
+
if !self.isDownloadStuckOrTimedOut() {
|
|
2095
|
+
self.backgroundDownload()
|
|
2096
|
+
} else {
|
|
2097
|
+
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
2098
|
+
}
|
|
1604
2099
|
}
|
|
1605
2100
|
}
|
|
1606
2101
|
}
|
|
@@ -1686,6 +2181,24 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1686
2181
|
])
|
|
1687
2182
|
}
|
|
1688
2183
|
|
|
2184
|
+
@objc func setShakeChannelSelector(_ call: CAPPluginCall) {
|
|
2185
|
+
guard let enabled = call.getBool("enabled") else {
|
|
2186
|
+
logger.error("setShakeChannelSelector called without enabled parameter")
|
|
2187
|
+
call.reject("setShakeChannelSelector called without enabled parameter")
|
|
2188
|
+
return
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
self.shakeChannelSelectorEnabled = enabled
|
|
2192
|
+
logger.info("Shake channel selector \(enabled ? "enabled" : "disabled")")
|
|
2193
|
+
call.resolve()
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
@objc func isShakeChannelSelectorEnabled(_ call: CAPPluginCall) {
|
|
2197
|
+
call.resolve([
|
|
2198
|
+
"enabled": self.shakeChannelSelectorEnabled
|
|
2199
|
+
])
|
|
2200
|
+
}
|
|
2201
|
+
|
|
1689
2202
|
@objc func getAppId(_ call: CAPPluginCall) {
|
|
1690
2203
|
call.resolve([
|
|
1691
2204
|
"appId": implementation.appId
|