@capawesome/capacitor-live-update 8.7.0 → 8.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,8 @@ Pod::Spec.new do |s|
9
9
  s.license = package['license']
10
10
  s.homepage = package['repository']['url']
11
11
  s.author = package['author']
12
- s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
12
+ # The npm tarball has the same layout as a local node_modules install, so one podspec serves both.
13
+ s.source = { :http => "https://registry.npmjs.org/@capawesome/capacitor-live-update/-/capacitor-live-update-#{s.version}.tgz" }
13
14
  s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
14
15
  s.ios.deployment_target = '15.0'
15
16
  s.dependency 'Capacitor'
@@ -9,6 +9,7 @@ ext {
9
9
  zip4jVersion = project.hasProperty('zip4jVersion') ? rootProject.ext.zip4jVersion : '2.11.5'
10
10
  ionicLiveUpdateProviderVersion = project.hasProperty('ionicLiveUpdateProviderVersion') ? rootProject.ext.ionicLiveUpdateProviderVersion : '1.0.0'
11
11
  kotlinxCoroutinesVersion = project.hasProperty('kotlinxCoroutinesVersion') ? rootProject.ext.kotlinxCoroutinesVersion : '1.11.0'
12
+ capacitorCoreVersion = project.hasProperty('capacitorCoreVersion') ? rootProject.ext.capacitorCoreVersion : '8.0.0'
12
13
  }
13
14
 
14
15
  buildscript {
@@ -20,6 +21,9 @@ buildscript {
20
21
  dependencies {
21
22
  classpath 'com.android.tools.build:gradle:8.13.0'
22
23
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
24
+ if (project.hasProperty('mavenCentralPublishing')) {
25
+ classpath 'com.vanniktech:gradle-maven-publish-plugin:0.37.0'
26
+ }
23
27
  }
24
28
  }
25
29
 
@@ -66,7 +70,12 @@ repositories {
66
70
 
67
71
  dependencies {
68
72
  implementation fileTree(dir: 'libs', include: ['*.jar'])
69
- implementation project(':capacitor-android')
73
+ if (project.hasProperty('mavenCentralPublishing')) {
74
+ // The Maven Central artifact depends on the published Capacitor core, not on an app's local module.
75
+ implementation "com.capacitorjs:core:$capacitorCoreVersion"
76
+ } else {
77
+ implementation project(':capacitor-android')
78
+ }
70
79
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
71
80
  implementation "net.lingala.zip4j:zip4j:$zip4jVersion"
72
81
  implementation "com.squareup.okhttp3:okhttp:$okhttp3Version"
@@ -80,3 +89,7 @@ dependencies {
80
89
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
81
90
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
82
91
  }
92
+
93
+ if (project.hasProperty('mavenCentralPublishing')) {
94
+ apply from: 'publish.gradle'
95
+ }
@@ -9,6 +9,7 @@ import android.content.res.AssetManager;
9
9
  import android.os.Build;
10
10
  import android.os.Handler;
11
11
  import android.os.Looper;
12
+ import android.os.SystemClock;
12
13
  import android.util.Base64;
13
14
  import androidx.annotation.NonNull;
14
15
  import androidx.annotation.Nullable;
@@ -115,8 +116,18 @@ public class LiveUpdate {
115
116
  private final Handler rollbackHandler = new Handler(Looper.getMainLooper());
116
117
  private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
117
118
 
119
+ private boolean activityStopped = false;
118
120
  private boolean initialPageLoaded = false;
119
121
  private long lastAutoUpdateCheckTimestamp = 0;
122
+
123
+ /** Milliseconds left on the rollback timer while it is paused in the background. */
124
+ @Nullable
125
+ private Long remainingRollbackTimeout = null;
126
+
127
+ /** Uptime at which the scheduled rollback fires, or {@code null} if none is scheduled. */
128
+ @Nullable
129
+ private Long rollbackDeadline = null;
130
+
120
131
  private boolean rollbackPerformed = false;
121
132
  private boolean syncInProgress = false;
122
133
 
@@ -421,6 +432,16 @@ public class LiveUpdate {
421
432
  }
422
433
  }
423
434
 
435
+ public synchronized void handleOnStart() {
436
+ activityStopped = false;
437
+ resumeRollbackTimer();
438
+ }
439
+
440
+ public synchronized void handleOnStop() {
441
+ activityStopped = true;
442
+ pauseRollbackTimer();
443
+ }
444
+
424
445
  public void ready(@NonNull NonEmptyCallback callback) {
425
446
  Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
426
447
  if (config.getReadyTimeout() <= 0) {
@@ -1356,6 +1377,23 @@ public class LiveUpdate {
1356
1377
  sync(options, callback);
1357
1378
  }
1358
1379
 
1380
+ private void pauseRollbackTimer() {
1381
+ if (rollbackDeadline == null) {
1382
+ return;
1383
+ }
1384
+ rollbackHandler.removeCallbacksAndMessages(null);
1385
+ remainingRollbackTimeout = Math.max(0, rollbackDeadline - SystemClock.uptimeMillis());
1386
+ rollbackDeadline = null;
1387
+ }
1388
+
1389
+ private void resumeRollbackTimer() {
1390
+ if (remainingRollbackTimeout == null) {
1391
+ return;
1392
+ }
1393
+ scheduleRollback(remainingRollbackTimeout);
1394
+ remainingRollbackTimeout = null;
1395
+ }
1396
+
1359
1397
  @NonNull
1360
1398
  private String requireAppId() throws Exception {
1361
1399
  return requireAppId(null);
@@ -1532,16 +1570,33 @@ public class LiveUpdate {
1532
1570
  preferences.setPreviousBundleId(bundleId);
1533
1571
  }
1534
1572
 
1535
- private void startRollbackTimer() {
1573
+ private void scheduleRollback(long delayMillis) {
1574
+ rollbackDeadline = SystemClock.uptimeMillis() + delayMillis;
1575
+ rollbackHandler.postDelayed(() -> {
1576
+ synchronized (this) {
1577
+ rollbackDeadline = null;
1578
+ }
1579
+ rollback();
1580
+ }, delayMillis);
1581
+ }
1582
+
1583
+ /** The timer only counts time in the foreground, because a stopped activity may not run JS. */
1584
+ private synchronized void startRollbackTimer() {
1536
1585
  if (config.getReadyTimeout() <= 0) {
1537
1586
  return;
1538
1587
  }
1539
1588
  stopRollbackTimer();
1540
- rollbackHandler.postDelayed(() -> rollback(), config.getReadyTimeout());
1589
+ if (activityStopped) {
1590
+ remainingRollbackTimeout = (long) config.getReadyTimeout();
1591
+ } else {
1592
+ scheduleRollback(config.getReadyTimeout());
1593
+ }
1541
1594
  }
1542
1595
 
1543
- private void stopRollbackTimer() {
1596
+ private synchronized void stopRollbackTimer() {
1544
1597
  rollbackHandler.removeCallbacksAndMessages(null);
1598
+ rollbackDeadline = null;
1599
+ remainingRollbackTimeout = null;
1545
1600
  }
1546
1601
 
1547
1602
  private boolean tryCopyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) {
@@ -41,7 +41,7 @@ import java.util.Map;
41
41
  public class LiveUpdatePlugin extends Plugin {
42
42
 
43
43
  public static final String TAG = "LiveUpdate";
44
- public static final String VERSION = "8.7.0";
44
+ public static final String VERSION = "8.8.0";
45
45
  public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
46
46
  public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
47
47
  public static final String ERROR_BUNDLE_DIRECTORY_NOT_FOUND = "Bundle directory could not be resolved.";
@@ -132,6 +132,22 @@ public class LiveUpdatePlugin extends Plugin {
132
132
  }
133
133
  }
134
134
 
135
+ @Override
136
+ protected void handleOnStart() {
137
+ super.handleOnStart();
138
+ if (implementation != null) {
139
+ implementation.handleOnStart();
140
+ }
141
+ }
142
+
143
+ @Override
144
+ protected void handleOnStop() {
145
+ super.handleOnStop();
146
+ if (implementation != null) {
147
+ implementation.handleOnStop();
148
+ }
149
+ }
150
+
135
151
  @PluginMethod
136
152
  public void clearBlockedBundles(PluginCall call) {
137
153
  try {
@@ -27,9 +27,15 @@ import CommonCrypto
27
27
  private let plugin: LiveUpdatePlugin?
28
28
  private let preferences: LiveUpdatePreferences
29
29
 
30
+ private var isAppInBackground = false
31
+ private var lastAutoUpdateCheckTimestamp: Int64 = 0
32
+ /// Milliseconds left on the rollback timer while it is paused in the background.
33
+ private var remainingRollbackTimeout: Int?
34
+ private var rollbackDeadline: DispatchTime?
30
35
  private var rollbackDispatchWorkItem: DispatchWorkItem?
31
36
  private var rollbackPerformed = false
32
- private var lastAutoUpdateCheckTimestamp: Int64 = 0
37
+ /// Guards the rollback timer state: plugin methods run on the bridge queue, lifecycle callbacks on main.
38
+ private let rollbackTimerLock = NSLock()
33
39
  private var syncInProgress = false
34
40
 
35
41
  /// - Parameter plugin: The Capacitor plugin, or `nil` for a headless instance
@@ -225,7 +231,18 @@ import CommonCrypto
225
231
  }
226
232
  }
227
233
 
234
+ @objc public func handleAppDidEnterBackground() {
235
+ rollbackTimerLock.lock()
236
+ defer { rollbackTimerLock.unlock() }
237
+ isAppInBackground = true
238
+ pauseRollbackTimer()
239
+ }
240
+
228
241
  @objc public func handleAppWillEnterForeground() {
242
+ rollbackTimerLock.lock()
243
+ defer { rollbackTimerLock.unlock() }
244
+ isAppInBackground = false
245
+ resumeRollbackTimer()
229
246
  if config.autoUpdateStrategy == "background" {
230
247
  performAutoUpdate()
231
248
  }
@@ -410,6 +427,14 @@ import CommonCrypto
410
427
  return Self.downloadsDirectoryUrl
411
428
  }
412
429
 
430
+ /// Must be called with `rollbackTimerLock` held.
431
+ private func clearRollbackTimer() {
432
+ rollbackDispatchWorkItem?.cancel()
433
+ rollbackDispatchWorkItem = nil
434
+ rollbackDeadline = nil
435
+ remainingRollbackTimeout = nil
436
+ }
437
+
413
438
  private func copyCurrentBundleFile(fileToCopy: ManifestItem, toDirectory: URL) throws {
414
439
  let currentBundleId = getCurrentBundleId()
415
440
  let destination = toDirectory.appendingPathComponent(fileToCopy.href)
@@ -816,6 +841,27 @@ import CommonCrypto
816
841
  }
817
842
  }
818
843
 
844
+ /// Must be called with `rollbackTimerLock` held.
845
+ private func pauseRollbackTimer() {
846
+ guard let deadline = rollbackDeadline else {
847
+ return
848
+ }
849
+ rollbackDispatchWorkItem?.cancel()
850
+ rollbackDispatchWorkItem = nil
851
+ rollbackDeadline = nil
852
+ let now = DispatchTime.now()
853
+ remainingRollbackTimeout = deadline > now ? Int((deadline.uptimeNanoseconds - now.uptimeNanoseconds) / 1_000_000) : 0
854
+ }
855
+
856
+ /// Must be called with `rollbackTimerLock` held.
857
+ private func resumeRollbackTimer() {
858
+ guard let remainingRollbackTimeout = remainingRollbackTimeout else {
859
+ return
860
+ }
861
+ self.remainingRollbackTimeout = nil
862
+ scheduleRollback(afterMilliseconds: remainingRollbackTimeout)
863
+ }
864
+
819
865
  private func requireAppId(_ appIdOverride: String? = nil) throws -> String {
820
866
  guard let appId = appIdOverride ?? getAppId(), !appId.isEmpty else {
821
867
  throw CustomError.appIdMissing
@@ -968,19 +1014,41 @@ import CommonCrypto
968
1014
  preferences.setPreviousBundleId(bundleId)
969
1015
  }
970
1016
 
1017
+ private func scheduleRollback(afterMilliseconds milliseconds: Int) {
1018
+ let deadline = DispatchTime.now() + DispatchTimeInterval.milliseconds(milliseconds)
1019
+ let workItem = DispatchWorkItem { [weak self] in
1020
+ guard let self = self else {
1021
+ return
1022
+ }
1023
+ self.rollbackTimerLock.lock()
1024
+ self.rollbackDeadline = nil
1025
+ self.rollbackTimerLock.unlock()
1026
+ self.rollback()
1027
+ }
1028
+ rollbackDeadline = deadline
1029
+ rollbackDispatchWorkItem = workItem
1030
+ DispatchQueue.main.asyncAfter(deadline: deadline, execute: workItem)
1031
+ }
1032
+
1033
+ /// The timer only counts time in the foreground, because a suspended app cannot call `ready()`.
971
1034
  private func startRollbackTimer() {
972
1035
  guard config.readyTimeout > 0 else {
973
1036
  return
974
1037
  }
975
- stopRollbackTimer()
976
- rollbackDispatchWorkItem = DispatchWorkItem { [weak self] in
977
- self?.rollback()
1038
+ rollbackTimerLock.lock()
1039
+ defer { rollbackTimerLock.unlock() }
1040
+ clearRollbackTimer()
1041
+ if isAppInBackground {
1042
+ remainingRollbackTimeout = config.readyTimeout
1043
+ } else {
1044
+ scheduleRollback(afterMilliseconds: config.readyTimeout)
978
1045
  }
979
- DispatchQueue.main.asyncAfter(deadline: .now() + DispatchTimeInterval.milliseconds(config.readyTimeout), execute: rollbackDispatchWorkItem!)
980
1046
  }
981
1047
 
982
1048
  private func stopRollbackTimer() {
983
- rollbackDispatchWorkItem?.cancel()
1049
+ rollbackTimerLock.lock()
1050
+ defer { rollbackTimerLock.unlock() }
1051
+ clearRollbackTimer()
984
1052
  }
985
1053
 
986
1054
  private func tryCopyCurrentBundleFile(
@@ -8,7 +8,7 @@ import Capacitor
8
8
  @objc(LiveUpdatePlugin)
9
9
  public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
10
10
  public static let tag = "LiveUpdate"
11
- public static let version = "8.7.0"
11
+ public static let version = "8.8.0"
12
12
  public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
13
13
 
14
14
  public let identifier = "LiveUpdatePlugin"
@@ -54,13 +54,22 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
54
54
  // Notify implementation about load
55
55
  implementation?.handleLoad()
56
56
 
57
- // Register for app resume notifications
57
+ // Register for app lifecycle notifications
58
+ NotificationCenter.default.addObserver(
59
+ self,
60
+ selector: #selector(handleAppDidEnterBackground),
61
+ name: UIApplication.didEnterBackgroundNotification,
62
+ object: nil
63
+ )
58
64
  NotificationCenter.default.addObserver(
59
65
  self,
60
66
  selector: #selector(handleAppWillEnterForeground),
61
67
  name: UIApplication.willEnterForegroundNotification,
62
68
  object: nil
63
69
  )
70
+ if UIApplication.shared.applicationState == .background {
71
+ implementation?.handleAppDidEnterBackground()
72
+ }
64
73
  }
65
74
 
66
75
  @objc func clearBlockedBundles(_ call: CAPPluginCall) {
@@ -387,6 +396,10 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
387
396
  notifyListeners(eventReloaded, data: event, retainUntilConsumed: true)
388
397
  }
389
398
 
399
+ @objc private func handleAppDidEnterBackground() {
400
+ implementation?.handleAppDidEnterBackground()
401
+ }
402
+
390
403
  @objc private func handleAppWillEnterForeground() {
391
404
  implementation?.handleAppWillEnterForeground()
392
405
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/capacitor-live-update",
3
- "version": "8.7.0",
3
+ "version": "8.8.0",
4
4
  "description": "Capacitor plugin to update your app remotely in real-time on Android, iOS and Electron.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",