@capgo/capacitor-updater 8.50.2 → 8.51.1
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 +44 -42
- package/android/build.gradle +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +251 -93
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +450 -69
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +19 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +90 -116
- package/dist/docs.json +13 -5
- package/dist/esm/definitions.d.ts +15 -10
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +5 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +167 -49
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +188 -34
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +28 -0
- package/package.json +6 -2
|
@@ -85,7 +85,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
85
85
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
86
86
|
]
|
|
87
87
|
public var implementation = CapgoUpdater()
|
|
88
|
-
private let pluginVersion: String = "8.
|
|
88
|
+
private let pluginVersion: String = "8.51.1"
|
|
89
|
+
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
89
90
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
90
91
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
91
92
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -135,6 +136,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
135
136
|
private var currentBuildVersion: String = "0"
|
|
136
137
|
private var autoUpdate = false
|
|
137
138
|
private var autoUpdateMode = CapacitorUpdaterPlugin.autoUpdateModeOff
|
|
139
|
+
private var launchStartReported = false
|
|
140
|
+
private var launchReadyReported = false
|
|
138
141
|
private var appReadyTimeout = 10000
|
|
139
142
|
private var appReadyCheck: DispatchWorkItem?
|
|
140
143
|
private var resetWhenUpdate = true
|
|
@@ -172,6 +175,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
172
175
|
|
|
173
176
|
// Lock to ensure cleanup completes before downloads start
|
|
174
177
|
private let cleanupLock = NSLock()
|
|
178
|
+
private let cleanupGroup = DispatchGroup()
|
|
175
179
|
private var cleanupComplete = false
|
|
176
180
|
private var cleanupThread: Thread?
|
|
177
181
|
private var persistCustomId = false
|
|
@@ -191,6 +195,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
191
195
|
private var isLeavingPreviewForIncomingLink = false
|
|
192
196
|
private var previewTransitionClearWorkItem: DispatchWorkItem?
|
|
193
197
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
198
|
+
// Best-effort flag: set before we expect notifyAppReady (load/reload).
|
|
199
|
+
// No lock — never held across waits; a rare race only mis-times one wait.
|
|
200
|
+
private var pendingNotifyAppReady = false
|
|
194
201
|
|
|
195
202
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
196
203
|
|
|
@@ -339,6 +346,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
339
346
|
} else {
|
|
340
347
|
implementation.defaultChannel = getConfig().getString("defaultChannel", "")!
|
|
341
348
|
}
|
|
349
|
+
self.reportAppLaunchStart()
|
|
342
350
|
self.implementation.autoReset()
|
|
343
351
|
let appHealthTracker = AppHealthTracker(implementation: self.implementation)
|
|
344
352
|
self.appHealthTracker = appHealthTracker
|
|
@@ -353,10 +361,21 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
353
361
|
}
|
|
354
362
|
self.leavePreviewSessionForLaunchURLIfNeeded()
|
|
355
363
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
self
|
|
364
|
+
// Downloads (including shake-menu / CapgoUpdater entry points) wait on this gate.
|
|
365
|
+
self.implementation.beforeDownload = { [weak self] in
|
|
366
|
+
self?.waitForCleanupIfNeeded()
|
|
359
367
|
}
|
|
368
|
+
// Always run async cleanup: delete obsolete bundles on native update (when enabled)
|
|
369
|
+
// and sweep orphan directories every launch. Must not block app startup.
|
|
370
|
+
if !resetWhenUpdate {
|
|
371
|
+
UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
|
|
372
|
+
UserDefaults.standard.synchronize()
|
|
373
|
+
}
|
|
374
|
+
let didResetCurrentBundle = resetWhenUpdate ? self.resetCurrentBundleForNativeBuildChangeIfNeeded() : false
|
|
375
|
+
self.cleanupObsoleteVersions(
|
|
376
|
+
resetWhenUpdate: resetWhenUpdate,
|
|
377
|
+
didResetCurrentBundle: didResetCurrentBundle
|
|
378
|
+
)
|
|
360
379
|
self.reportNativeVersionStatsIfChanged()
|
|
361
380
|
|
|
362
381
|
// Load the server
|
|
@@ -518,6 +537,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
518
537
|
}
|
|
519
538
|
|
|
520
539
|
private func semaphoreUp() {
|
|
540
|
+
pendingNotifyAppReady = true
|
|
521
541
|
DispatchQueue.global().async {
|
|
522
542
|
self.semaphoreWait(waitTime: 0)
|
|
523
543
|
}
|
|
@@ -629,12 +649,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
629
649
|
return true
|
|
630
650
|
}
|
|
631
651
|
|
|
632
|
-
private func cleanupObsoleteVersions(didResetCurrentBundle: Bool = false) {
|
|
652
|
+
private func cleanupObsoleteVersions(resetWhenUpdate: Bool = true, didResetCurrentBundle: Bool = false) {
|
|
653
|
+
// Enter before start so waiters never race past an unstarted cleanup thread.
|
|
654
|
+
self.cleanupComplete = false
|
|
655
|
+
self.cleanupGroup.enter()
|
|
633
656
|
cleanupThread = Thread {
|
|
657
|
+
let bgTaskLock = NSLock()
|
|
658
|
+
var cleanupBackgroundTask = UIBackgroundTaskIdentifier.invalid
|
|
659
|
+
let endCleanupBackgroundTask = {
|
|
660
|
+
bgTaskLock.lock()
|
|
661
|
+
defer { bgTaskLock.unlock() }
|
|
662
|
+
if cleanupBackgroundTask != .invalid {
|
|
663
|
+
UIApplication.shared.endBackgroundTask(cleanupBackgroundTask)
|
|
664
|
+
cleanupBackgroundTask = .invalid
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
cleanupBackgroundTask = UIApplication.shared.beginBackgroundTask(withName: "CapgoBundleCleanup") {
|
|
668
|
+
endCleanupBackgroundTask()
|
|
669
|
+
}
|
|
634
670
|
self.cleanupLock.lock()
|
|
635
671
|
defer {
|
|
636
672
|
self.cleanupComplete = true
|
|
637
673
|
self.cleanupLock.unlock()
|
|
674
|
+
self.cleanupGroup.leave()
|
|
675
|
+
endCleanupBackgroundTask()
|
|
638
676
|
self.logger.info("Cleanup complete")
|
|
639
677
|
}
|
|
640
678
|
|
|
@@ -665,54 +703,37 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
665
703
|
// 2. Compare both keys. If any is not equal to "currentBuildVersion", then revert to builtin version. This fixes the part 2 of this bug
|
|
666
704
|
|
|
667
705
|
let previous = self.storedNativeBuildVersion()
|
|
668
|
-
|
|
706
|
+
let nativeVersionChanged = previous != "0" && self.currentBuildVersion != previous
|
|
707
|
+
if resetWhenUpdate && nativeVersionChanged {
|
|
669
708
|
if !didResetCurrentBundle {
|
|
670
709
|
self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting current bundle to builtin.")
|
|
671
710
|
self.implementation.reset(isInternal: true)
|
|
672
711
|
}
|
|
673
712
|
let res = self.implementation.list()
|
|
674
713
|
for version in res {
|
|
675
|
-
// Check if thread was cancelled
|
|
676
|
-
if Thread.current.isCancelled {
|
|
677
|
-
self.logger.warn("Cleanup was cancelled, stopping")
|
|
678
|
-
return
|
|
679
|
-
}
|
|
680
714
|
self.logger.info("Deleting obsolete bundle: \(version.getId())")
|
|
681
|
-
let
|
|
682
|
-
if !
|
|
715
|
+
let deleted = self.implementation.delete(id: version.getId())
|
|
716
|
+
if !deleted {
|
|
683
717
|
self.logger.error("Delete failed, id \(version.getId()) doesn't exist")
|
|
684
718
|
}
|
|
719
|
+
Thread.sleep(forTimeInterval: 0.075)
|
|
685
720
|
}
|
|
686
|
-
|
|
687
|
-
let storedBundles = self.implementation.list(raw: true)
|
|
688
|
-
let allowedIds = Set(storedBundles.compactMap { info -> String? in
|
|
689
|
-
let id = info.getId()
|
|
690
|
-
return id.isEmpty ? nil : id
|
|
691
|
-
})
|
|
692
|
-
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds, threadToCheck: Thread.current)
|
|
693
|
-
self.implementation.cleanupOrphanedTempFolders(threadToCheck: Thread.current)
|
|
694
|
-
|
|
695
|
-
// Check again before the expensive delta cache cleanup
|
|
696
|
-
if Thread.current.isCancelled {
|
|
697
|
-
self.logger.warn("Cleanup was cancelled before delta cache cleanup")
|
|
698
|
-
return
|
|
699
|
-
}
|
|
700
|
-
self.implementation.cleanupDeltaCache(threadToCheck: Thread.current)
|
|
721
|
+
self.implementation.cleanupDeltaCache()
|
|
701
722
|
}
|
|
723
|
+
|
|
724
|
+
// Resume any DELETING leftovers from prior kills, one-by-one.
|
|
725
|
+
self.implementation.drainPendingDeletes()
|
|
726
|
+
|
|
727
|
+
// Always sweep orphan directories so incomplete prior cleanups (or failed deletes)
|
|
728
|
+
// cannot leave hundreds of MB behind across launches.
|
|
729
|
+
let allowedIds = self.implementation.allowedBundleIdsForCleanup()
|
|
730
|
+
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds)
|
|
731
|
+
self.implementation.cleanupOrphanedTempFolders(threadToCheck: nil)
|
|
732
|
+
|
|
702
733
|
UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
|
|
703
734
|
UserDefaults.standard.synchronize()
|
|
704
735
|
}
|
|
705
736
|
cleanupThread?.start()
|
|
706
|
-
|
|
707
|
-
// Start a timeout watchdog thread to cancel cleanup if it takes too long
|
|
708
|
-
let timeout = Double(self.appReadyTimeout / 2) / 1000.0
|
|
709
|
-
Thread.detachNewThread {
|
|
710
|
-
Thread.sleep(forTimeInterval: timeout)
|
|
711
|
-
if let thread = self.cleanupThread, !thread.isFinished && !self.cleanupComplete {
|
|
712
|
-
self.logger.warn("Cleanup timeout exceeded (\(timeout)s), cancelling cleanup thread")
|
|
713
|
-
thread.cancel()
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
737
|
}
|
|
717
738
|
|
|
718
739
|
private func waitForCleanupIfNeeded() {
|
|
@@ -721,11 +742,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
721
742
|
}
|
|
722
743
|
|
|
723
744
|
logger.info("Waiting for cleanup to complete before starting download...")
|
|
724
|
-
|
|
725
|
-
// Wait for cleanup to complete - blocks until lock is released
|
|
726
|
-
cleanupLock.lock()
|
|
727
|
-
cleanupLock.unlock()
|
|
728
|
-
|
|
745
|
+
cleanupGroup.wait()
|
|
729
746
|
logger.info("Cleanup finished, proceeding with download")
|
|
730
747
|
}
|
|
731
748
|
|
|
@@ -1142,6 +1159,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1142
1159
|
}
|
|
1143
1160
|
|
|
1144
1161
|
private func downloadBundle(urlString: String, version: String, sessionKey: String, checksum rawChecksum: String, manifestEntries: [ManifestEntry]?) throws -> BundleInfo {
|
|
1162
|
+
// Manual/preview downloads must wait too - launch orphan sweep can delete their temps.
|
|
1163
|
+
self.waitForCleanupIfNeeded()
|
|
1145
1164
|
guard let url = URL(string: urlString) else {
|
|
1146
1165
|
throw makePreviewError("Invalid download URL")
|
|
1147
1166
|
}
|
|
@@ -1293,6 +1312,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1293
1312
|
|
|
1294
1313
|
let performReload: () -> Bool = {
|
|
1295
1314
|
guard self.applyCurrentBundleToBridge(bridge) else {
|
|
1315
|
+
self.clearPendingNotifyAppReady()
|
|
1296
1316
|
return false
|
|
1297
1317
|
}
|
|
1298
1318
|
self.checkAppReady()
|
|
@@ -2674,10 +2694,67 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2674
2694
|
])
|
|
2675
2695
|
}
|
|
2676
2696
|
|
|
2697
|
+
private func reportAppLaunchStart() {
|
|
2698
|
+
guard !self.implementation.statsUrl.isEmpty, !launchStartReported else {
|
|
2699
|
+
return
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
launchStartReported = true
|
|
2703
|
+
let current = self.implementation.getCurrentBundle()
|
|
2704
|
+
self.implementation.sendStats(
|
|
2705
|
+
action: "app_launch_start",
|
|
2706
|
+
versionName: current.getVersionName(),
|
|
2707
|
+
oldVersionName: "",
|
|
2708
|
+
metadata: [
|
|
2709
|
+
"launch_started_at": String(launchStartedAtMs),
|
|
2710
|
+
"source": "plugin_load"
|
|
2711
|
+
]
|
|
2712
|
+
)
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
private func reportAppLaunchReady(_ bundle: BundleInfo) {
|
|
2716
|
+
guard !self.implementation.statsUrl.isEmpty, !launchReadyReported else {
|
|
2717
|
+
return
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
launchReadyReported = true
|
|
2721
|
+
let duration = max(0, Int64(Date().timeIntervalSince1970 * 1000) - launchStartedAtMs)
|
|
2722
|
+
self.implementation.sendStats(
|
|
2723
|
+
action: "app_launch_ready",
|
|
2724
|
+
versionName: bundle.getVersionName(),
|
|
2725
|
+
oldVersionName: "",
|
|
2726
|
+
metadata: [
|
|
2727
|
+
"duration_ms": String(duration),
|
|
2728
|
+
"launch_started_at": String(launchStartedAtMs),
|
|
2729
|
+
"source": "notify_app_ready"
|
|
2730
|
+
]
|
|
2731
|
+
)
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
private func reportAppLaunchTimeout(_ bundle: BundleInfo) {
|
|
2735
|
+
guard !self.implementation.statsUrl.isEmpty else {
|
|
2736
|
+
return
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
let duration = max(0, Int64(Date().timeIntervalSince1970 * 1000) - launchStartedAtMs)
|
|
2740
|
+
self.implementation.sendStats(
|
|
2741
|
+
action: "app_launch_timeout",
|
|
2742
|
+
versionName: bundle.getVersionName(),
|
|
2743
|
+
oldVersionName: "",
|
|
2744
|
+
metadata: [
|
|
2745
|
+
"duration_ms": String(duration),
|
|
2746
|
+
"launch_started_at": String(launchStartedAtMs),
|
|
2747
|
+
"timeout_ms": String(appReadyTimeout),
|
|
2748
|
+
"source": "app_ready_timeout"
|
|
2749
|
+
]
|
|
2750
|
+
)
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2677
2753
|
@objc func notifyAppReady(_ call: CAPPluginCall) {
|
|
2678
2754
|
self.semaphoreDown()
|
|
2679
2755
|
let bundle = self.implementation.getCurrentBundle()
|
|
2680
2756
|
self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
|
|
2757
|
+
self.reportAppLaunchReady(bundle)
|
|
2681
2758
|
logger.info("Current bundle loaded successfully. [notifyAppReady was called] \(bundle.toString())")
|
|
2682
2759
|
self.clearIncomingPreviewTransition()
|
|
2683
2760
|
self.hidePreviewTransitionLoader(reason: "notify-app-ready")
|
|
@@ -2785,16 +2862,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2785
2862
|
"bundle": current.toJSON()
|
|
2786
2863
|
])
|
|
2787
2864
|
self.persistLastFailedBundle(current)
|
|
2865
|
+
self.reportAppLaunchTimeout(current)
|
|
2788
2866
|
self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
|
|
2789
2867
|
self.implementation.setError(bundle: current)
|
|
2790
2868
|
_ = self.performReset(toLastSuccessful: true, usePendingBundle: false, isInternal: true)
|
|
2791
2869
|
if self.autoDeleteFailed && !current.isBuiltin() {
|
|
2870
|
+
let failedId = current.getId()
|
|
2792
2871
|
logger.info("Deleting failing bundle: \(current.toString())")
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2872
|
+
// Mark before async work so kill/OOM still resumes via drainPendingDeletes.
|
|
2873
|
+
self.implementation.saveBundleInfo(
|
|
2874
|
+
id: failedId,
|
|
2875
|
+
bundle: current.setStatus(status: BundleStatus.DELETING.storedValue)
|
|
2876
|
+
)
|
|
2877
|
+
UserDefaults.standard.synchronize()
|
|
2878
|
+
DispatchQueue.global(qos: .utility).async {
|
|
2879
|
+
let res = self.implementation.delete(id: failedId, removeInfo: false)
|
|
2880
|
+
if res {
|
|
2881
|
+
self.logger.info("Failed bundle deleted: \(failedId)")
|
|
2882
|
+
} else {
|
|
2883
|
+
self.logger.error("Failed to delete failed bundle: \(failedId)")
|
|
2884
|
+
}
|
|
2798
2885
|
}
|
|
2799
2886
|
}
|
|
2800
2887
|
} else {
|
|
@@ -2819,7 +2906,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2819
2906
|
func sendReadyToJs(current: BundleInfo, msg: String) {
|
|
2820
2907
|
logger.info("sendReadyToJs")
|
|
2821
2908
|
DispatchQueue.global().async {
|
|
2822
|
-
|
|
2909
|
+
// Only wait after load()/reload() armed the semaphore. Foreground resumes
|
|
2910
|
+
// with autoUpdate disabled call sendReadyToJs again without a fresh
|
|
2911
|
+
// notifyAppReady(), so waiting there always timed out after appReadyTimeout.
|
|
2912
|
+
if self.consumePendingNotifyAppReady() {
|
|
2913
|
+
self.semaphoreWait(waitTime: self.appReadyTimeout)
|
|
2914
|
+
}
|
|
2823
2915
|
self.notifyListeners("appReady", data: ["bundle": current.toJSON(), "status": msg], retainUntilConsumed: true)
|
|
2824
2916
|
|
|
2825
2917
|
// Auto hide splashscreen if enabled
|
|
@@ -2831,6 +2923,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2831
2923
|
}
|
|
2832
2924
|
}
|
|
2833
2925
|
|
|
2926
|
+
private func consumePendingNotifyAppReady() -> Bool {
|
|
2927
|
+
let shouldWait = pendingNotifyAppReady
|
|
2928
|
+
pendingNotifyAppReady = false
|
|
2929
|
+
return shouldWait
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
private func clearPendingNotifyAppReady() {
|
|
2933
|
+
pendingNotifyAppReady = false
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2834
2936
|
private func hideSplashscreen() {
|
|
2835
2937
|
if Thread.isMainThread {
|
|
2836
2938
|
self.performHideSplashscreen()
|
|
@@ -3444,6 +3546,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3444
3546
|
self.currentBuildVersion = currentBuildVersion
|
|
3445
3547
|
}
|
|
3446
3548
|
|
|
3549
|
+
func setAppReadyTimeoutForTesting(_ timeout: Int) {
|
|
3550
|
+
self.appReadyTimeout = timeout
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
func armPendingNotifyAppReadyForTesting() {
|
|
3554
|
+
pendingNotifyAppReady = true
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
func clearPendingNotifyAppReadyForTesting() {
|
|
3558
|
+
clearPendingNotifyAppReady()
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
var isPendingNotifyAppReadyForTesting: Bool {
|
|
3562
|
+
pendingNotifyAppReady
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3447
3565
|
func shouldUseDirectUpdateForTesting() -> Bool {
|
|
3448
3566
|
self.shouldUseDirectUpdate()
|
|
3449
3567
|
}
|