@capgo/capacitor-updater 8.45.0 → 8.45.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.
@@ -22,6 +22,7 @@ import android.view.View;
22
22
  import android.view.ViewGroup;
23
23
  import android.widget.FrameLayout;
24
24
  import android.widget.ProgressBar;
25
+ import com.getcapacitor.Bridge;
25
26
  import com.getcapacitor.CapConfig;
26
27
  import com.getcapacitor.JSArray;
27
28
  import com.getcapacitor.JSObject;
@@ -29,6 +30,7 @@ import com.getcapacitor.Plugin;
29
30
  import com.getcapacitor.PluginCall;
30
31
  import com.getcapacitor.PluginHandle;
31
32
  import com.getcapacitor.PluginMethod;
33
+ import com.getcapacitor.PluginResult;
32
34
  import com.getcapacitor.annotation.CapacitorPlugin;
33
35
  import com.getcapacitor.plugin.WebView;
34
36
  import com.google.android.gms.tasks.Task;
@@ -56,7 +58,6 @@ import java.util.Objects;
56
58
  import java.util.Set;
57
59
  import java.util.Timer;
58
60
  import java.util.TimerTask;
59
- import java.util.UUID;
60
61
  import java.util.concurrent.Phaser;
61
62
  import java.util.concurrent.Semaphore;
62
63
  import java.util.concurrent.TimeUnit;
@@ -83,8 +84,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
83
84
  private static final String DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.defaultChannel";
84
85
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
85
86
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
87
+ private static final String SPLASH_SCREEN_PLUGIN_ID = "SplashScreen";
88
+ private static final int SPLASH_SCREEN_RETRY_DELAY_MS = 100;
89
+ private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
86
90
 
87
- private final String pluginVersion = "8.45.0";
91
+ private final String pluginVersion = "8.45.1";
88
92
  private static final String DELAY_CONDITION_PREFERENCES = "";
89
93
 
90
94
  private SharedPreferences.Editor editor;
@@ -108,9 +112,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
108
112
  private Boolean autoSplashscreenLoader = false;
109
113
  private Integer autoSplashscreenTimeout = 10000;
110
114
  private Boolean autoSplashscreenTimedOut = false;
115
+ private int splashscreenInvocationToken = 0;
111
116
  private String directUpdateMode = "false";
112
117
  private Boolean wasRecentlyInstalledOrUpdated = false;
113
- private Boolean onLaunchDirectUpdateUsed = false;
118
+ private volatile boolean onLaunchDirectUpdateUsed = false;
114
119
  Boolean shakeMenuEnabled = false;
115
120
  Boolean shakeChannelSelectorEnabled = false;
116
121
  private Boolean allowManualBundleError = false;
@@ -145,6 +150,28 @@ public class CapacitorUpdaterPlugin extends Plugin {
145
150
  private FrameLayout splashscreenLoaderOverlay;
146
151
  private Runnable splashscreenTimeoutRunnable;
147
152
 
153
+ private static final class FireAndForgetPluginCall extends PluginCall {
154
+
155
+ FireAndForgetPluginCall(final String methodName, final JSObject data) {
156
+ super(null, SPLASH_SCREEN_PLUGIN_ID, PluginCall.CALLBACK_ID_DANGLING, methodName, data);
157
+ }
158
+
159
+ @Override
160
+ public void successCallback(final PluginResult successResult) {}
161
+
162
+ @Override
163
+ public void resolve(final JSObject data) {}
164
+
165
+ @Override
166
+ public void resolve() {}
167
+
168
+ @Override
169
+ public void errorCallback(final String msg) {}
170
+
171
+ @Override
172
+ public void reject(final String msg, final String code, final Exception ex, final JSObject data) {}
173
+ }
174
+
148
175
  // App lifecycle observer using ProcessLifecycleOwner for reliable foreground/background detection
149
176
  private AppLifecycleObserver appLifecycleObserver;
150
177
 
@@ -557,47 +584,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
557
584
  private void hideSplashscreenInternal() {
558
585
  cancelSplashscreenTimeout();
559
586
  removeSplashscreenLoader();
560
-
561
- try {
562
- if (getBridge() == null) {
563
- logger.warn("Bridge not ready for hiding splashscreen with autoSplashscreen");
564
- return;
565
- }
566
-
567
- // Try to call the SplashScreen plugin directly through the bridge
568
- PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
569
- if (splashScreenPlugin != null) {
570
- try {
571
- // Create a plugin call for the hide method using reflection to access private msgHandler
572
- JSObject options = new JSObject();
573
- java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
574
- msgHandlerField.setAccessible(true);
575
- Object msgHandler = msgHandlerField.get(getBridge());
576
-
577
- PluginCall call = new PluginCall(
578
- (com.getcapacitor.MessageHandler) msgHandler,
579
- "SplashScreen",
580
- "FAKE_CALLBACK_ID_HIDE",
581
- "hide",
582
- options
583
- );
584
-
585
- // Call the hide method directly
586
- splashScreenPlugin.invoke("hide", call);
587
- logger.info("Splashscreen hidden automatically via direct plugin call");
588
- } catch (Exception e) {
589
- logger.error("Failed to call SplashScreen hide method: " + e.getMessage());
590
- }
591
- } else {
592
- logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.");
593
- }
594
- } catch (Exception e) {
595
- logger.error(
596
- "Error hiding splashscreen with autoSplashscreen: " +
597
- e.getMessage() +
598
- ". Make sure @capacitor/splash-screen plugin is installed and configured."
599
- );
600
- }
587
+ invokeSplashScreenPluginMethod("hide", new JSObject(), SPLASH_SCREEN_MAX_RETRIES, ++this.splashscreenInvocationToken);
601
588
  }
602
589
 
603
590
  private void showSplashscreen() {
@@ -612,37 +599,87 @@ public class CapacitorUpdaterPlugin extends Plugin {
612
599
  cancelSplashscreenTimeout();
613
600
  this.autoSplashscreenTimedOut = false;
614
601
 
602
+ final JSObject options = new JSObject();
603
+ options.put("autoHide", false);
604
+ invokeSplashScreenPluginMethod("show", options, SPLASH_SCREEN_MAX_RETRIES, ++this.splashscreenInvocationToken);
605
+
606
+ addSplashscreenLoaderIfNeeded();
607
+ scheduleSplashscreenTimeout();
608
+ }
609
+
610
+ private void invokeSplashScreenPluginMethod(
611
+ final String methodName,
612
+ final JSObject options,
613
+ final int retriesRemaining,
614
+ final int requestToken
615
+ ) {
616
+ if (requestToken != this.splashscreenInvocationToken) {
617
+ return;
618
+ }
619
+
615
620
  try {
616
- if (getBridge() == null) {
617
- logger.warn("Bridge not ready for showing splashscreen with autoSplashscreen");
618
- } else {
619
- PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
620
- if (splashScreenPlugin != null) {
621
- JSObject options = new JSObject();
622
- java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
623
- msgHandlerField.setAccessible(true);
624
- Object msgHandler = msgHandlerField.get(getBridge());
625
-
626
- PluginCall call = new PluginCall(
627
- (com.getcapacitor.MessageHandler) msgHandler,
628
- "SplashScreen",
629
- "FAKE_CALLBACK_ID_SHOW",
630
- "show",
631
- options
632
- );
621
+ final Bridge bridge = getBridge();
622
+ if (bridge == null) {
623
+ retrySplashScreenInvocation(
624
+ methodName,
625
+ options,
626
+ retriesRemaining,
627
+ requestToken,
628
+ "Bridge not ready for " + ("show".equals(methodName) ? "showing" : "hiding") + " splashscreen"
629
+ );
630
+ return;
631
+ }
633
632
 
634
- splashScreenPlugin.invoke("show", call);
635
- logger.info("Splashscreen shown synchronously to prevent flash");
636
- } else {
637
- logger.warn("autoSplashscreen: SplashScreen plugin not found");
638
- }
633
+ final PluginHandle splashScreenPlugin = bridge.getPlugin(SPLASH_SCREEN_PLUGIN_ID);
634
+ if (splashScreenPlugin == null) {
635
+ retrySplashScreenInvocation(
636
+ methodName,
637
+ options,
638
+ retriesRemaining,
639
+ requestToken,
640
+ "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
641
+ );
642
+ return;
639
643
  }
640
- } catch (Exception e) {
641
- logger.error("Failed to show splashscreen synchronously: " + e.getMessage());
644
+
645
+ splashScreenPlugin.invoke(methodName, new FireAndForgetPluginCall(methodName, options));
646
+ logger.info("Splashscreen " + methodName + " invoked automatically");
647
+ } catch (final Exception e) {
648
+ retrySplashScreenInvocation(
649
+ methodName,
650
+ options,
651
+ retriesRemaining,
652
+ requestToken,
653
+ "Failed to call SplashScreen " + methodName + " method: " + e.getMessage()
654
+ );
642
655
  }
656
+ }
643
657
 
644
- addSplashscreenLoaderIfNeeded();
645
- scheduleSplashscreenTimeout();
658
+ private void retrySplashScreenInvocation(
659
+ final String methodName,
660
+ final JSObject options,
661
+ final int retriesRemaining,
662
+ final int requestToken,
663
+ final String message
664
+ ) {
665
+ if (retriesRemaining > 0) {
666
+ logger.info(message + ". Retrying.");
667
+ this.mainHandler.postDelayed(
668
+ () -> invokeSplashScreenPluginMethod(methodName, options, retriesRemaining - 1, requestToken),
669
+ SPLASH_SCREEN_RETRY_DELAY_MS
670
+ );
671
+ return;
672
+ }
673
+
674
+ if ("show".equals(methodName)) {
675
+ logger.warn(message);
676
+ } else {
677
+ logger.error(message);
678
+ }
679
+ }
680
+
681
+ boolean isCurrentSplashscreenInvocationTokenForTesting(final int requestToken) {
682
+ return requestToken == this.splashscreenInvocationToken;
646
683
  }
647
684
 
648
685
  private void addSplashscreenLoaderIfNeeded() {
@@ -783,6 +820,49 @@ public class CapacitorUpdaterPlugin extends Plugin {
783
820
  return plannedDirectUpdate && !Boolean.TRUE.equals(this.autoSplashscreenTimedOut);
784
821
  }
785
822
 
823
+ static boolean shouldConsumeOnLaunchDirectUpdate(final String directUpdateMode, final boolean plannedDirectUpdate) {
824
+ return plannedDirectUpdate && "onLaunch".equals(directUpdateMode);
825
+ }
826
+
827
+ private void consumeOnLaunchDirectUpdateAttempt(final boolean plannedDirectUpdate) {
828
+ if (!shouldConsumeOnLaunchDirectUpdate(this.directUpdateMode, plannedDirectUpdate)) {
829
+ return;
830
+ }
831
+
832
+ this.onLaunchDirectUpdateUsed = true;
833
+ }
834
+
835
+ void configureDirectUpdateModeForTesting(final String directUpdateMode, final boolean onLaunchDirectUpdateUsed) {
836
+ this.directUpdateMode = directUpdateMode;
837
+ this.onLaunchDirectUpdateUsed = onLaunchDirectUpdateUsed;
838
+ }
839
+
840
+ boolean shouldUseDirectUpdateForTesting() {
841
+ return this.shouldUseDirectUpdate();
842
+ }
843
+
844
+ boolean hasConsumedOnLaunchDirectUpdateForTesting() {
845
+ return this.onLaunchDirectUpdateUsed;
846
+ }
847
+
848
+ boolean isVersionDownloadInProgress(final String version) {
849
+ return (
850
+ version != null &&
851
+ !version.isEmpty() &&
852
+ this.implementation != null &&
853
+ this.implementation.activity != null &&
854
+ DownloadWorkerManager.isVersionDownloading(this.implementation.activity, version)
855
+ );
856
+ }
857
+
858
+ void setLoggerForTesting(final Logger logger) {
859
+ this.logger = logger;
860
+ }
861
+
862
+ void completeBackgroundTaskForTesting(final BundleInfo current, final boolean plannedDirectUpdate) {
863
+ this.endBackGroundTaskWithNotif("test", current.getVersionName(), current, false, plannedDirectUpdate);
864
+ }
865
+
786
866
  private void directUpdateFinish(final BundleInfo latest) {
787
867
  if ("onLaunch".equals(this.directUpdateMode)) {
788
868
  this.onLaunchDirectUpdateUsed = true;
@@ -1804,11 +1884,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1804
1884
  String latestVersionName,
1805
1885
  BundleInfo current,
1806
1886
  Boolean error,
1807
- Boolean isDirectUpdate,
1887
+ Boolean plannedDirectUpdate,
1808
1888
  String failureAction,
1809
1889
  String failureEvent,
1810
1890
  boolean shouldSendStats
1811
1891
  ) {
1892
+ this.consumeOnLaunchDirectUpdateAttempt(Boolean.TRUE.equals(plannedDirectUpdate));
1812
1893
  if (error) {
1813
1894
  logger.info(
1814
1895
  "endBackGroundTaskWithNotif error: " +
@@ -1828,7 +1909,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1828
1909
  final JSObject ret = new JSObject();
1829
1910
  ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
1830
1911
  this.notifyListeners("noNeedUpdate", ret);
1831
- this.sendReadyToJs(current, msg, isDirectUpdate);
1912
+ this.sendReadyToJs(current, msg, plannedDirectUpdate);
1832
1913
  this.backgroundDownloadTask = null;
1833
1914
  this.downloadStartTimeMs = 0;
1834
1915
  logger.info("endBackGroundTaskWithNotif " + msg);
@@ -1862,7 +1943,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
1862
1943
  private Thread backgroundDownload() {
1863
1944
  final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
1864
1945
  final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
1865
- this.implementation.directUpdate = initialDirectUpdateAllowed;
1866
1946
  final String messageUpdate = initialDirectUpdateAllowed
1867
1947
  ? "Update will occur now."
1868
1948
  : "Update will occur next time app moves to background.";
@@ -1930,7 +2010,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1930
2010
  "Next update will be to builtin version",
1931
2011
  latestVersionName,
1932
2012
  current,
1933
- false
2013
+ false,
2014
+ plannedDirectUpdate
1934
2015
  );
1935
2016
  }
1936
2017
  return;
@@ -1966,7 +2047,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1966
2047
  );
1967
2048
  return;
1968
2049
  }
1969
- if (latest.isDownloaded()) {
2050
+ if (latest.isDownloaded() && BundleStatus.DOWNLOADING != latest.getStatus()) {
1970
2051
  logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
1971
2052
  final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
1972
2053
  plannedDirectUpdate
@@ -2019,7 +2100,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2019
2100
  "update downloaded, will install next background",
2020
2101
  latestVersionName,
2021
2102
  latest,
2022
- false
2103
+ false,
2104
+ plannedDirectUpdate
2023
2105
  );
2024
2106
  }
2025
2107
  return;
@@ -2036,6 +2118,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
2036
2118
  }
2037
2119
  }
2038
2120
  }
2121
+ final boolean retryingInFlightDownload =
2122
+ latest != null &&
2123
+ BundleStatus.DOWNLOADING == latest.getStatus() &&
2124
+ CapacitorUpdaterPlugin.this.isVersionDownloadInProgress(latest.getVersionName());
2125
+ CapacitorUpdaterPlugin.this.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate);
2126
+ CapacitorUpdaterPlugin.this.implementation.directUpdate = retryingInFlightDownload
2127
+ ? Boolean.TRUE.equals(CapacitorUpdaterPlugin.this.implementation.directUpdate) || initialDirectUpdateAllowed
2128
+ : initialDirectUpdateAllowed;
2039
2129
  startNewThread(() -> {
2040
2130
  try {
2041
2131
  logger.info(
@@ -2084,7 +2174,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
2084
2174
  });
2085
2175
  } else {
2086
2176
  logger.info("No need to update, " + current.getId() + " is the latest bundle.");
2087
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif("No need to update", latestVersionName, current, false);
2177
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2178
+ "No need to update",
2179
+ latestVersionName,
2180
+ current,
2181
+ false,
2182
+ plannedDirectUpdate
2183
+ );
2088
2184
  }
2089
2185
  } catch (final Exception e) {
2090
2186
  logger.error("error in update check " + e.getMessage());
@@ -72,7 +72,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
72
72
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
73
73
  ]
74
74
  public var implementation = CapgoUpdater()
75
- private let pluginVersion: String = "8.45.0"
75
+ private let pluginVersion: String = "8.45.1"
76
76
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
77
77
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
78
78
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -102,7 +102,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
102
102
  private var autoSplashscreenTimeoutWorkItem: DispatchWorkItem?
103
103
  private var splashscreenLoaderView: UIActivityIndicatorView?
104
104
  private var splashscreenLoaderContainer: UIView?
105
+ private let splashscreenPluginName = "SplashScreen"
106
+ private let splashscreenRetryDelayMilliseconds = 100
107
+ private let splashscreenMaxRetries = 20
105
108
  private var autoSplashscreenTimedOut = false
109
+ private var splashscreenInvocationToken = 0
106
110
  private var autoDeleteFailed = false
107
111
  private var autoDeletePrevious = false
108
112
  var allowSetDefaultChannel = true
@@ -111,6 +115,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
111
115
  private var taskRunning = false
112
116
  private var periodCheckDelay = 0
113
117
  private let downloadLock = NSLock()
118
+ private let onLaunchDirectUpdateStateLock = NSLock()
114
119
  private var downloadInProgress = false
115
120
  private var downloadStartTime: Date?
116
121
  private let downloadTimeout: TimeInterval = 3600 // 1 hour timeout
@@ -1135,32 +1140,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1135
1140
  private func performHideSplashscreen() {
1136
1141
  self.cancelSplashscreenTimeout()
1137
1142
  self.removeSplashscreenLoader()
1138
-
1139
- guard let bridge = self.bridge else {
1140
- self.logger.warn("Bridge not available for hiding splashscreen with autoSplashscreen")
1141
- return
1142
- }
1143
-
1144
- // Create a plugin call for the hide method
1145
- let call = CAPPluginCall(callbackId: "autoHideSplashscreen", options: [:], success: { (_, _) in
1146
- self.logger.info("Splashscreen hidden automatically")
1147
- }, error: { (_) in
1148
- self.logger.error("Failed to auto-hide splashscreen")
1149
- })
1150
-
1151
- // Try to call the SplashScreen hide method directly through the bridge
1152
- if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
1153
- // Use runtime method invocation to call hide method
1154
- let selector = NSSelectorFromString("hide:")
1155
- if splashScreenPlugin.responds(to: selector) {
1156
- _ = splashScreenPlugin.perform(selector, with: call)
1157
- self.logger.info("Called SplashScreen hide method")
1158
- } else {
1159
- self.logger.warn("autoSplashscreen: SplashScreen plugin does not respond to hide: method. Make sure @capacitor/splash-screen plugin is properly installed.")
1160
- }
1161
- } else {
1162
- self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
1163
- }
1143
+ self.splashscreenInvocationToken += 1
1144
+ self.invokeSplashscreenMethod(
1145
+ methodName: "hide",
1146
+ callbackId: "autoHideSplashscreen",
1147
+ options: self.splashscreenOptions(methodName: "hide"),
1148
+ retriesRemaining: self.splashscreenMaxRetries,
1149
+ requestToken: self.splashscreenInvocationToken
1150
+ )
1164
1151
  }
1165
1152
 
1166
1153
  private func showSplashscreen() {
@@ -1176,35 +1163,132 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1176
1163
  private func performShowSplashscreen() {
1177
1164
  self.cancelSplashscreenTimeout()
1178
1165
  self.autoSplashscreenTimedOut = false
1166
+ self.splashscreenInvocationToken += 1
1167
+ self.invokeSplashscreenMethod(
1168
+ methodName: "show",
1169
+ callbackId: "autoShowSplashscreen",
1170
+ options: self.splashscreenOptions(methodName: "show"),
1171
+ retriesRemaining: self.splashscreenMaxRetries,
1172
+ requestToken: self.splashscreenInvocationToken
1173
+ )
1174
+
1175
+ self.addSplashscreenLoaderIfNeeded()
1176
+ self.scheduleSplashscreenTimeout()
1177
+ }
1178
+
1179
+ private func splashscreenOptions(methodName: String) -> [String: Any] {
1180
+ methodName == "show" ? ["autoHide": false] : [:]
1181
+ }
1182
+
1183
+ private func splashscreenCompletedMessage(methodName: String) -> String {
1184
+ methodName == "show" ? "Splashscreen shown automatically" : "Splashscreen hidden automatically"
1185
+ }
1186
+
1187
+ func splashscreenOptionsForTesting(methodName: String) -> [String: Any] {
1188
+ self.splashscreenOptions(methodName: methodName)
1189
+ }
1190
+
1191
+ func isCurrentSplashscreenInvocationTokenForTesting(_ requestToken: Int) -> Bool {
1192
+ requestToken == self.splashscreenInvocationToken
1193
+ }
1194
+
1195
+ func advanceSplashscreenInvocationTokenForTesting() {
1196
+ self.splashscreenInvocationToken += 1
1197
+ }
1198
+
1199
+ private func makeSplashscreenCall(callbackId: String, options: [String: Any], methodName: String) -> CAPPluginCall {
1200
+ CAPPluginCall(callbackId: callbackId, options: options, success: { [weak self] (_, _) in
1201
+ guard let self = self else { return }
1202
+ self.logger.info(self.splashscreenCompletedMessage(methodName: methodName))
1203
+ }, error: { [weak self] (_) in
1204
+ guard let self = self else { return }
1205
+ self.logger.error("Failed to auto-\(methodName) splashscreen")
1206
+ })
1207
+ }
1208
+
1209
+ private func invokeSplashscreenMethod(
1210
+ methodName: String,
1211
+ callbackId: String,
1212
+ options: [String: Any],
1213
+ retriesRemaining: Int,
1214
+ requestToken: Int
1215
+ ) {
1216
+ guard requestToken == self.splashscreenInvocationToken else {
1217
+ return
1218
+ }
1179
1219
 
1180
1220
  guard let bridge = self.bridge else {
1181
- self.logger.warn("Bridge not available for showing splashscreen with autoSplashscreen")
1221
+ self.retrySplashscreenMethod(
1222
+ methodName: methodName,
1223
+ callbackId: callbackId,
1224
+ options: options,
1225
+ retriesRemaining: retriesRemaining,
1226
+ requestToken: requestToken,
1227
+ message: "Bridge not available for \(methodName == "show" ? "showing" : "hiding") splashscreen with autoSplashscreen"
1228
+ )
1182
1229
  return
1183
1230
  }
1184
1231
 
1185
- // Create a plugin call for the show method
1186
- let call = CAPPluginCall(callbackId: "autoShowSplashscreen", options: [:], success: { (_, _) in
1187
- self.logger.info("Splashscreen shown automatically")
1188
- }, error: { (_) in
1189
- self.logger.error("Failed to auto-show splashscreen")
1190
- })
1232
+ guard let splashScreenPlugin = bridge.plugin(withName: self.splashscreenPluginName) else {
1233
+ self.retrySplashscreenMethod(
1234
+ methodName: methodName,
1235
+ callbackId: callbackId,
1236
+ options: options,
1237
+ retriesRemaining: retriesRemaining,
1238
+ requestToken: requestToken,
1239
+ message: "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
1240
+ )
1241
+ return
1242
+ }
1243
+
1244
+ let selector = NSSelectorFromString("\(methodName):")
1245
+ guard splashScreenPlugin.responds(to: selector) else {
1246
+ self.retrySplashscreenMethod(
1247
+ methodName: methodName,
1248
+ callbackId: callbackId,
1249
+ options: options,
1250
+ retriesRemaining: retriesRemaining,
1251
+ requestToken: requestToken,
1252
+ message: "autoSplashscreen: SplashScreen plugin does not respond to \(methodName): method. Make sure @capacitor/splash-screen plugin is properly installed."
1253
+ )
1254
+ return
1255
+ }
1256
+
1257
+ let call = self.makeSplashscreenCall(callbackId: callbackId, options: options, methodName: methodName)
1258
+ _ = splashScreenPlugin.perform(selector, with: call)
1259
+ self.logger.info("Called SplashScreen \(methodName) method")
1260
+ }
1191
1261
 
1192
- // Try to call the SplashScreen show method directly through the bridge
1193
- if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
1194
- // Use runtime method invocation to call show method
1195
- let selector = NSSelectorFromString("show:")
1196
- if splashScreenPlugin.responds(to: selector) {
1197
- _ = splashScreenPlugin.perform(selector, with: call)
1198
- self.logger.info("Called SplashScreen show method")
1262
+ private func retrySplashscreenMethod(
1263
+ methodName: String,
1264
+ callbackId: String,
1265
+ options: [String: Any],
1266
+ retriesRemaining: Int,
1267
+ requestToken: Int,
1268
+ message: String
1269
+ ) {
1270
+ guard retriesRemaining > 0 else {
1271
+ if methodName == "show" {
1272
+ self.logger.warn(message)
1199
1273
  } else {
1200
- self.logger.warn("autoSplashscreen: SplashScreen plugin does not respond to show: method. Make sure @capacitor/splash-screen plugin is properly installed.")
1274
+ self.logger.error(message)
1201
1275
  }
1202
- } else {
1203
- self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
1276
+ return
1204
1277
  }
1205
1278
 
1206
- self.addSplashscreenLoaderIfNeeded()
1207
- self.scheduleSplashscreenTimeout()
1279
+ self.logger.info("\(message). Retrying.")
1280
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.splashscreenRetryDelayMilliseconds)) { [weak self] in
1281
+ guard let self = self, requestToken == self.splashscreenInvocationToken else {
1282
+ return
1283
+ }
1284
+ self.invokeSplashscreenMethod(
1285
+ methodName: methodName,
1286
+ callbackId: callbackId,
1287
+ options: options,
1288
+ retriesRemaining: retriesRemaining - 1,
1289
+ requestToken: requestToken
1290
+ )
1291
+ }
1208
1292
  }
1209
1293
 
1210
1294
  private func addSplashscreenLoaderIfNeeded() {
@@ -1358,7 +1442,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1358
1442
  }
1359
1443
  return false
1360
1444
  case "onLaunch":
1361
- if !self.onLaunchDirectUpdateUsed {
1445
+ if !self.getOnLaunchDirectUpdateUsed() {
1362
1446
  return true
1363
1447
  }
1364
1448
  return false
@@ -1368,6 +1452,47 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1368
1452
  }
1369
1453
  }
1370
1454
 
1455
+ static func shouldConsumeOnLaunchDirectUpdate(directUpdateMode: String, plannedDirectUpdate: Bool) -> Bool {
1456
+ plannedDirectUpdate && directUpdateMode == "onLaunch"
1457
+ }
1458
+
1459
+ private func getOnLaunchDirectUpdateUsed() -> Bool {
1460
+ self.onLaunchDirectUpdateStateLock.lock()
1461
+ defer { self.onLaunchDirectUpdateStateLock.unlock() }
1462
+ return self.onLaunchDirectUpdateUsed
1463
+ }
1464
+
1465
+ private func setOnLaunchDirectUpdateUsed(_ used: Bool) {
1466
+ self.onLaunchDirectUpdateStateLock.lock()
1467
+ self.onLaunchDirectUpdateUsed = used
1468
+ self.onLaunchDirectUpdateStateLock.unlock()
1469
+ }
1470
+
1471
+ private func consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: Bool) {
1472
+ guard Self.shouldConsumeOnLaunchDirectUpdate(directUpdateMode: self.directUpdateMode, plannedDirectUpdate: plannedDirectUpdate) else {
1473
+ return
1474
+ }
1475
+
1476
+ self.setOnLaunchDirectUpdateUsed(true)
1477
+ }
1478
+
1479
+ func configureDirectUpdateModeForTesting(_ directUpdateMode: String, onLaunchDirectUpdateUsed: Bool = false) {
1480
+ self.directUpdateMode = directUpdateMode
1481
+ self.setOnLaunchDirectUpdateUsed(onLaunchDirectUpdateUsed)
1482
+ }
1483
+
1484
+ func setUpdateUrlForTesting(_ updateUrl: String) {
1485
+ self.updateUrl = updateUrl
1486
+ }
1487
+
1488
+ func shouldUseDirectUpdateForTesting() -> Bool {
1489
+ self.shouldUseDirectUpdate()
1490
+ }
1491
+
1492
+ var hasConsumedOnLaunchDirectUpdateForTesting: Bool {
1493
+ self.getOnLaunchDirectUpdateUsed()
1494
+ }
1495
+
1371
1496
  private func notifyBreakingEvents(version: String) {
1372
1497
  guard !version.isEmpty else {
1373
1498
  return
@@ -1382,6 +1507,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1382
1507
  latestVersionName: String,
1383
1508
  current: BundleInfo,
1384
1509
  error: Bool = true,
1510
+ plannedDirectUpdate: Bool = false,
1385
1511
  failureAction: String = "download_fail",
1386
1512
  failureEvent: String = "downloadFailed",
1387
1513
  sendStats: Bool = true
@@ -1393,6 +1519,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1393
1519
  downloadInProgress = false
1394
1520
  downloadStartTime = nil
1395
1521
 
1522
+ self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
1523
+
1396
1524
  if error {
1397
1525
  if sendStats {
1398
1526
  self.implementation.sendStats(action: failureAction, versionName: current.getVersionName())
@@ -1427,6 +1555,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1427
1555
  return true
1428
1556
  }
1429
1557
 
1558
+ func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
1559
+ DispatchQueue.global(qos: .background).async(execute: work)
1560
+ }
1561
+
1430
1562
  func backgroundDownload() {
1431
1563
  // Set download in progress flag (thread-safe)
1432
1564
  downloadLock.lock()
@@ -1446,7 +1578,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1446
1578
  return
1447
1579
  }
1448
1580
 
1449
- DispatchQueue.global(qos: .background).async {
1581
+ self.runBackgroundDownloadWork {
1450
1582
  // Wait for cleanup to complete before starting download
1451
1583
  self.waitForCleanupIfNeeded()
1452
1584
  self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Download Tasks") {
@@ -1467,6 +1599,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1467
1599
  latestVersionName: res.version,
1468
1600
  current: current,
1469
1601
  error: true,
1602
+ plannedDirectUpdate: plannedDirectUpdate,
1470
1603
  sendStats: !responseIsOk
1471
1604
  )
1472
1605
  return
@@ -1476,26 +1609,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1476
1609
  let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
1477
1610
  if directUpdateAllowed {
1478
1611
  self.logger.info("Direct update to builtin version")
1479
- if self.directUpdateMode == "onLaunch" {
1480
- self.onLaunchDirectUpdateUsed = true
1481
- self.directUpdate = false
1482
- }
1483
1612
  _ = self._reset(toLastSuccessful: false)
1484
- self.endBackGroundTaskWithNotif(msg: "Updated to builtin version", latestVersionName: res.version, current: self.implementation.getCurrentBundle(), error: false)
1613
+ self.endBackGroundTaskWithNotif(
1614
+ msg: "Updated to builtin version",
1615
+ latestVersionName: res.version,
1616
+ current: self.implementation.getCurrentBundle(),
1617
+ error: false,
1618
+ plannedDirectUpdate: plannedDirectUpdate
1619
+ )
1485
1620
  } else {
1486
1621
  if plannedDirectUpdate && !directUpdateAllowed {
1487
1622
  self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will apply later.")
1488
1623
  }
1489
1624
  self.logger.info("Setting next bundle to builtin")
1490
1625
  _ = self.implementation.setNextBundle(next: BundleInfo.ID_BUILTIN)
1491
- self.endBackGroundTaskWithNotif(msg: "Next update will be to builtin version", latestVersionName: res.version, current: current, error: false)
1626
+ self.endBackGroundTaskWithNotif(
1627
+ msg: "Next update will be to builtin version",
1628
+ latestVersionName: res.version,
1629
+ current: current,
1630
+ error: false,
1631
+ plannedDirectUpdate: plannedDirectUpdate
1632
+ )
1492
1633
  }
1493
1634
  return
1494
1635
  }
1495
1636
  let sessionKey = res.sessionKey ?? ""
1496
1637
  guard let downloadUrl = URL(string: res.url) else {
1497
1638
  self.logger.error("Error no url or wrong format")
1498
- self.endBackGroundTaskWithNotif(msg: "Error no url or wrong format", latestVersionName: res.version, current: current)
1639
+ self.endBackGroundTaskWithNotif(
1640
+ msg: "Error no url or wrong format",
1641
+ latestVersionName: res.version,
1642
+ current: current,
1643
+ plannedDirectUpdate: plannedDirectUpdate
1644
+ )
1499
1645
  return
1500
1646
  }
1501
1647
  let latestVersionName = res.version
@@ -1513,6 +1659,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1513
1659
  self.logger.error("Failed to delete failed bundle: \(nextImpl!.toString())")
1514
1660
  }
1515
1661
  }
1662
+ self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
1516
1663
  if res.manifest != nil {
1517
1664
  nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
1518
1665
  } else {
@@ -1521,12 +1668,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1521
1668
  }
1522
1669
  guard let next = nextImpl else {
1523
1670
  self.logger.error("Error downloading file")
1524
- self.endBackGroundTaskWithNotif(msg: "Error downloading file", latestVersionName: latestVersionName, current: current)
1671
+ self.endBackGroundTaskWithNotif(
1672
+ msg: "Error downloading file",
1673
+ latestVersionName: latestVersionName,
1674
+ current: current,
1675
+ plannedDirectUpdate: plannedDirectUpdate
1676
+ )
1525
1677
  return
1526
1678
  }
1527
1679
  if next.isErrorStatus() {
1528
1680
  self.logger.error("Latest bundle already exists and is in error state. Aborting update.")
1529
- self.endBackGroundTaskWithNotif(msg: "Latest version is in error state. Aborting update.", latestVersionName: latestVersionName, current: current)
1681
+ self.endBackGroundTaskWithNotif(
1682
+ msg: "Latest version is in error state. Aborting update.",
1683
+ latestVersionName: latestVersionName,
1684
+ current: current,
1685
+ plannedDirectUpdate: plannedDirectUpdate
1686
+ )
1530
1687
  return
1531
1688
  }
1532
1689
  res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
@@ -1540,7 +1697,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1540
1697
  if !resDel {
1541
1698
  self.logger.error("Delete failed, id \(id) doesn't exist")
1542
1699
  }
1543
- self.endBackGroundTaskWithNotif(msg: "Error checksum", latestVersionName: latestVersionName, current: current)
1700
+ self.endBackGroundTaskWithNotif(
1701
+ msg: "Error checksum",
1702
+ latestVersionName: latestVersionName,
1703
+ current: current,
1704
+ plannedDirectUpdate: plannedDirectUpdate
1705
+ )
1544
1706
  return
1545
1707
  }
1546
1708
  let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
@@ -1553,18 +1715,31 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1553
1715
  }
1554
1716
  if !delayConditionList.isEmpty {
1555
1717
  self.logger.info("Update delayed until delay conditions met")
1556
- self.endBackGroundTaskWithNotif(msg: "Update delayed until delay conditions met", latestVersionName: latestVersionName, current: next, error: false)
1718
+ self.endBackGroundTaskWithNotif(
1719
+ msg: "Update delayed until delay conditions met",
1720
+ latestVersionName: latestVersionName,
1721
+ current: next,
1722
+ error: false,
1723
+ plannedDirectUpdate: plannedDirectUpdate
1724
+ )
1557
1725
  return
1558
1726
  }
1559
- if self.directUpdateMode == "onLaunch" {
1560
- self.onLaunchDirectUpdateUsed = true
1561
- self.directUpdate = false
1562
- }
1563
1727
  if self.implementation.set(bundle: next) && self._reload() {
1564
1728
  self.notifyBundleSet(next)
1565
- self.endBackGroundTaskWithNotif(msg: "update installed", latestVersionName: latestVersionName, current: next, error: false)
1729
+ self.endBackGroundTaskWithNotif(
1730
+ msg: "update installed",
1731
+ latestVersionName: latestVersionName,
1732
+ current: next,
1733
+ error: false,
1734
+ plannedDirectUpdate: plannedDirectUpdate
1735
+ )
1566
1736
  } else {
1567
- self.endBackGroundTaskWithNotif(msg: "Update install failed", latestVersionName: latestVersionName, current: next)
1737
+ self.endBackGroundTaskWithNotif(
1738
+ msg: "Update install failed",
1739
+ latestVersionName: latestVersionName,
1740
+ current: next,
1741
+ plannedDirectUpdate: plannedDirectUpdate
1742
+ )
1568
1743
  }
1569
1744
  } else {
1570
1745
  if plannedDirectUpdate && !directUpdateAllowed {
@@ -1572,18 +1747,35 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1572
1747
  }
1573
1748
  self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
1574
1749
  _ = self.implementation.setNextBundle(next: next.getId())
1575
- self.endBackGroundTaskWithNotif(msg: "update downloaded, will install next background", latestVersionName: latestVersionName, current: current, error: false)
1750
+ self.endBackGroundTaskWithNotif(
1751
+ msg: "update downloaded, will install next background",
1752
+ latestVersionName: latestVersionName,
1753
+ current: current,
1754
+ error: false,
1755
+ plannedDirectUpdate: plannedDirectUpdate
1756
+ )
1576
1757
  }
1577
1758
  return
1578
1759
  } catch {
1579
1760
  self.logger.error("Error downloading file \(error.localizedDescription)")
1580
1761
  let current: BundleInfo = self.implementation.getCurrentBundle()
1581
- self.endBackGroundTaskWithNotif(msg: "Error downloading file", latestVersionName: latestVersionName, current: current)
1762
+ self.endBackGroundTaskWithNotif(
1763
+ msg: "Error downloading file",
1764
+ latestVersionName: latestVersionName,
1765
+ current: current,
1766
+ plannedDirectUpdate: plannedDirectUpdate
1767
+ )
1582
1768
  return
1583
1769
  }
1584
1770
  } else {
1585
1771
  self.logger.info("No need to update, \(current.getId()) is the latest bundle.")
1586
- self.endBackGroundTaskWithNotif(msg: "No need to update, \(current.getId()) is the latest bundle.", latestVersionName: latestVersionName, current: current, error: false)
1772
+ self.endBackGroundTaskWithNotif(
1773
+ msg: "No need to update, \(current.getId()) is the latest bundle.",
1774
+ latestVersionName: latestVersionName,
1775
+ current: current,
1776
+ error: false,
1777
+ plannedDirectUpdate: plannedDirectUpdate
1778
+ )
1587
1779
  return
1588
1780
  }
1589
1781
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.45.0",
3
+ "version": "8.45.1",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",