@capgo/capacitor-updater 7.43.3 → 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.
@@ -22,6 +22,8 @@ import android.view.View;
22
22
  import android.view.ViewGroup;
23
23
  import android.widget.FrameLayout;
24
24
  import android.widget.ProgressBar;
25
+ import androidx.core.content.pm.PackageInfoCompat;
26
+ import com.getcapacitor.Bridge;
25
27
  import com.getcapacitor.CapConfig;
26
28
  import com.getcapacitor.JSArray;
27
29
  import com.getcapacitor.JSObject;
@@ -29,6 +31,7 @@ import com.getcapacitor.Plugin;
29
31
  import com.getcapacitor.PluginCall;
30
32
  import com.getcapacitor.PluginHandle;
31
33
  import com.getcapacitor.PluginMethod;
34
+ import com.getcapacitor.PluginResult;
32
35
  import com.getcapacitor.annotation.CapacitorPlugin;
33
36
  import com.getcapacitor.plugin.WebView;
34
37
  import com.google.android.gms.tasks.Task;
@@ -47,7 +50,6 @@ import java.io.IOException;
47
50
  import java.net.MalformedURLException;
48
51
  import java.net.URL;
49
52
  import java.util.ArrayList;
50
- import java.util.Arrays;
51
53
  import java.util.Date;
52
54
  import java.util.HashSet;
53
55
  import java.util.List;
@@ -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,12 @@ 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;
90
+ private static final long PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS = 30000L;
86
91
 
87
- private final String pluginVersion = "7.43.3";
92
+ private final String pluginVersion = "7.45.10";
88
93
  private static final String DELAY_CONDITION_PREFERENCES = "";
89
94
 
90
95
  private SharedPreferences.Editor editor;
@@ -108,9 +113,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
108
113
  private Boolean autoSplashscreenLoader = false;
109
114
  private Integer autoSplashscreenTimeout = 10000;
110
115
  private Boolean autoSplashscreenTimedOut = false;
116
+ private int splashscreenInvocationToken = 0;
111
117
  private String directUpdateMode = "false";
112
118
  private Boolean wasRecentlyInstalledOrUpdated = false;
113
- private Boolean onLaunchDirectUpdateUsed = false;
119
+ private volatile boolean onLaunchDirectUpdateUsed = false;
114
120
  Boolean shakeMenuEnabled = false;
115
121
  Boolean shakeChannelSelectorEnabled = false;
116
122
  private Boolean allowManualBundleError = false;
@@ -128,8 +134,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
128
134
  private volatile long downloadStartTimeMs = 0;
129
135
  private static final long DOWNLOAD_TIMEOUT_MS = 3600000; // 1 hour timeout
130
136
 
131
- // private static final CountDownLatch semaphoreReady = new CountDownLatch(1);
132
- private static final Phaser semaphoreReady = new Phaser(1);
137
+ private final Phaser semaphoreReady = new Phaser(0) {
138
+ @Override
139
+ protected boolean onAdvance(final int phase, final int registeredParties) {
140
+ return false;
141
+ }
142
+ };
133
143
 
134
144
  // Lock to ensure cleanup completes before downloads start
135
145
  private final Object cleanupLock = new Object();
@@ -145,6 +155,28 @@ public class CapacitorUpdaterPlugin extends Plugin {
145
155
  private FrameLayout splashscreenLoaderOverlay;
146
156
  private Runnable splashscreenTimeoutRunnable;
147
157
 
158
+ private static final class FireAndForgetPluginCall extends PluginCall {
159
+
160
+ FireAndForgetPluginCall(final String methodName, final JSObject data) {
161
+ super(null, SPLASH_SCREEN_PLUGIN_ID, PluginCall.CALLBACK_ID_DANGLING, methodName, data);
162
+ }
163
+
164
+ @Override
165
+ public void successCallback(final PluginResult successResult) {}
166
+
167
+ @Override
168
+ public void resolve(final JSObject data) {}
169
+
170
+ @Override
171
+ public void resolve() {}
172
+
173
+ @Override
174
+ public void errorCallback(final String msg) {}
175
+
176
+ @Override
177
+ public void reject(final String msg, final String code, final Exception ex, final JSObject data) {}
178
+ }
179
+
148
180
  // App lifecycle observer using ProcessLifecycleOwner for reliable foreground/background detection
149
181
  private AppLifecycleObserver appLifecycleObserver;
150
182
 
@@ -154,6 +186,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
154
186
  private static final int APP_UPDATE_REQUEST_CODE = 9001;
155
187
  private InstallStateUpdatedListener installStateUpdatedListener;
156
188
 
189
+ private PackageInfo getCurrentPackageInfo() throws PackageManager.NameNotFoundException {
190
+ final PackageManager packageManager = this.getContext().getPackageManager();
191
+ final String packageName = this.getContext().getPackageName();
192
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
193
+ return packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
194
+ }
195
+ return packageManager.getPackageInfo(packageName, 0);
196
+ }
197
+
198
+ private String getVersionCode(final PackageInfo packageInfo) {
199
+ return Long.toString(PackageInfoCompat.getLongVersionCode(packageInfo));
200
+ }
201
+
157
202
  private void notifyBreakingEvents(final String version) {
158
203
  if (version == null || version.isEmpty()) {
159
204
  return;
@@ -243,13 +288,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
243
288
 
244
289
  @Override
245
290
  public void directUpdateFinish(final BundleInfo latest) {
246
- if (activity != null) {
247
- activity.runOnUiThread(() -> {
248
- CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
249
- });
250
- } else {
251
- logger.warn("directUpdateFinish: Activity is null, skipping notification");
252
- }
291
+ CapacitorUpdaterPlugin.this.scheduleDirectUpdateFinish(latest);
253
292
  }
254
293
 
255
294
  @Override
@@ -263,12 +302,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
263
302
  }
264
303
  }
265
304
  };
266
- final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
305
+ final PackageInfo pInfo = this.getCurrentPackageInfo();
267
306
  this.implementation.activity = this.getActivity();
268
307
  this.implementation.versionBuild = this.getConfig().getString("version", pInfo.versionName);
269
308
  this.implementation.CAP_SERVER_PATH = WebView.CAP_SERVER_PATH;
270
309
  this.implementation.pluginVersion = this.pluginVersion;
271
- this.implementation.versionCode = Integer.toString(pInfo.versionCode);
310
+ this.implementation.versionCode = this.getVersionCode(pInfo);
272
311
  // Removed unused OkHttpClient creation - using shared client in DownloadService instead
273
312
  // Handle directUpdate configuration - support string values and backward compatibility
274
313
  String directUpdateConfig = this.getConfig().getString("directUpdate", null);
@@ -310,7 +349,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
310
349
  }
311
350
  }
312
351
  this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
313
- this.currentBuildVersion = Integer.toString(pInfo.versionCode);
352
+ this.currentBuildVersion = this.getVersionCode(pInfo);
314
353
  this.delayUpdateUtils = new DelayUpdateUtils(this.prefs, this.editor, this.currentVersionNative, logger);
315
354
  } catch (final PackageManager.NameNotFoundException e) {
316
355
  logger.error("Error instantiating implementation " + e.getMessage());
@@ -480,34 +519,82 @@ public class CapacitorUpdaterPlugin extends Plugin {
480
519
  }
481
520
  }
482
521
 
483
- private void semaphoreWait(Number waitTime) {
522
+ private boolean semaphoreWait(final int phase, Number waitTime) {
484
523
  try {
485
- semaphoreReady.awaitAdvanceInterruptibly(semaphoreReady.getPhase(), waitTime.longValue(), TimeUnit.SECONDS);
524
+ semaphoreReady.awaitAdvanceInterruptibly(phase, waitTime.longValue(), TimeUnit.MILLISECONDS);
486
525
  logger.info("semaphoreReady count " + semaphoreReady.getPhase());
526
+ return true;
487
527
  } catch (InterruptedException e) {
488
528
  logger.info("semaphoreWait InterruptedException");
529
+ cleanupTimedOutSemaphoreWait(phase);
489
530
  Thread.currentThread().interrupt(); // Restore interrupted status
531
+ return false;
490
532
  } catch (TimeoutException e) {
491
533
  logger.error("Semaphore timeout: " + e.getMessage());
492
- // Don't throw runtime exception, just log and continue
534
+ cleanupTimedOutSemaphoreWait(phase);
535
+ return false;
493
536
  }
494
537
  }
495
538
 
496
- private void semaphoreUp() {
539
+ private int semaphoreUp() {
497
540
  logger.info("semaphoreUp");
498
- semaphoreReady.register();
541
+ return semaphoreReady.register();
499
542
  }
500
543
 
501
544
  private void semaphoreDown() {
545
+ if (semaphoreReady.getRegisteredParties() == 0) {
546
+ logger.info("semaphoreDown skipped, no pending app ready wait");
547
+ return;
548
+ }
502
549
  logger.info("semaphoreDown");
503
550
  logger.info("semaphoreDown count " + semaphoreReady.getPhase());
504
551
  semaphoreReady.arriveAndDeregister();
505
552
  }
506
553
 
554
+ private void cleanupTimedOutSemaphoreWait(final int phase) {
555
+ if (semaphoreReady.getPhase() != phase || semaphoreReady.getRegisteredParties() == 0) {
556
+ return;
557
+ }
558
+ logger.info("Cleaning up stale app ready wait for phase " + phase);
559
+ semaphoreReady.arriveAndDeregister();
560
+ }
561
+
562
+ protected long getMinimumPendingBundleAppReadyTimeoutMs() {
563
+ return PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS;
564
+ }
565
+
566
+ private long resolveAppReadyCheckTimeoutMs() {
567
+ long configuredTimeoutMs = this.appReadyTimeout.longValue();
568
+ try {
569
+ if (this.implementation == null) {
570
+ return configuredTimeoutMs;
571
+ }
572
+
573
+ final BundleInfo current = this.implementation.getCurrentBundle();
574
+ if (current == null || BundleStatus.SUCCESS == current.getStatus()) {
575
+ return configuredTimeoutMs;
576
+ }
577
+
578
+ return Math.max(configuredTimeoutMs, this.getMinimumPendingBundleAppReadyTimeoutMs());
579
+ } catch (final Exception e) {
580
+ logger.warn("Falling back to configured appReadyTimeout: " + e.getMessage());
581
+ return configuredTimeoutMs;
582
+ }
583
+ }
584
+
507
585
  private void sendReadyToJs(final BundleInfo current, final String msg) {
508
586
  sendReadyToJs(current, msg, false);
509
587
  }
510
588
 
589
+ private void notifyBundleSet(final BundleInfo bundle) {
590
+ if (bundle == null) {
591
+ return;
592
+ }
593
+ final JSObject ret = new JSObject();
594
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
595
+ this.notifyListeners("set", ret, true);
596
+ }
597
+
511
598
  private void sendReadyToJs(final BundleInfo current, final String msg, final boolean isDirectUpdate) {
512
599
  logger.info("sendReadyToJs: " + msg);
513
600
  final JSObject ret = new JSObject();
@@ -535,47 +622,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
535
622
  private void hideSplashscreenInternal() {
536
623
  cancelSplashscreenTimeout();
537
624
  removeSplashscreenLoader();
538
-
539
- try {
540
- if (getBridge() == null) {
541
- logger.warn("Bridge not ready for hiding splashscreen with autoSplashscreen");
542
- return;
543
- }
544
-
545
- // Try to call the SplashScreen plugin directly through the bridge
546
- PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
547
- if (splashScreenPlugin != null) {
548
- try {
549
- // Create a plugin call for the hide method using reflection to access private msgHandler
550
- JSObject options = new JSObject();
551
- java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
552
- msgHandlerField.setAccessible(true);
553
- Object msgHandler = msgHandlerField.get(getBridge());
554
-
555
- PluginCall call = new PluginCall(
556
- (com.getcapacitor.MessageHandler) msgHandler,
557
- "SplashScreen",
558
- "FAKE_CALLBACK_ID_HIDE",
559
- "hide",
560
- options
561
- );
562
-
563
- // Call the hide method directly
564
- splashScreenPlugin.invoke("hide", call);
565
- logger.info("Splashscreen hidden automatically via direct plugin call");
566
- } catch (Exception e) {
567
- logger.error("Failed to call SplashScreen hide method: " + e.getMessage());
568
- }
569
- } else {
570
- logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.");
571
- }
572
- } catch (Exception e) {
573
- logger.error(
574
- "Error hiding splashscreen with autoSplashscreen: " +
575
- e.getMessage() +
576
- ". Make sure @capacitor/splash-screen plugin is installed and configured."
577
- );
578
- }
625
+ invokeSplashScreenPluginMethod("hide", new JSObject(), SPLASH_SCREEN_MAX_RETRIES, ++this.splashscreenInvocationToken);
579
626
  }
580
627
 
581
628
  private void showSplashscreen() {
@@ -590,37 +637,87 @@ public class CapacitorUpdaterPlugin extends Plugin {
590
637
  cancelSplashscreenTimeout();
591
638
  this.autoSplashscreenTimedOut = false;
592
639
 
640
+ final JSObject options = new JSObject();
641
+ options.put("autoHide", false);
642
+ invokeSplashScreenPluginMethod("show", options, SPLASH_SCREEN_MAX_RETRIES, ++this.splashscreenInvocationToken);
643
+
644
+ addSplashscreenLoaderIfNeeded();
645
+ scheduleSplashscreenTimeout();
646
+ }
647
+
648
+ private void invokeSplashScreenPluginMethod(
649
+ final String methodName,
650
+ final JSObject options,
651
+ final int retriesRemaining,
652
+ final int requestToken
653
+ ) {
654
+ if (requestToken != this.splashscreenInvocationToken) {
655
+ return;
656
+ }
657
+
593
658
  try {
594
- if (getBridge() == null) {
595
- logger.warn("Bridge not ready for showing splashscreen with autoSplashscreen");
596
- } else {
597
- PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
598
- if (splashScreenPlugin != null) {
599
- JSObject options = new JSObject();
600
- java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
601
- msgHandlerField.setAccessible(true);
602
- Object msgHandler = msgHandlerField.get(getBridge());
603
-
604
- PluginCall call = new PluginCall(
605
- (com.getcapacitor.MessageHandler) msgHandler,
606
- "SplashScreen",
607
- "FAKE_CALLBACK_ID_SHOW",
608
- "show",
609
- options
610
- );
659
+ final Bridge bridge = getBridge();
660
+ if (bridge == null) {
661
+ retrySplashScreenInvocation(
662
+ methodName,
663
+ options,
664
+ retriesRemaining,
665
+ requestToken,
666
+ "Bridge not ready for " + ("show".equals(methodName) ? "showing" : "hiding") + " splashscreen"
667
+ );
668
+ return;
669
+ }
611
670
 
612
- splashScreenPlugin.invoke("show", call);
613
- logger.info("Splashscreen shown synchronously to prevent flash");
614
- } else {
615
- logger.warn("autoSplashscreen: SplashScreen plugin not found");
616
- }
671
+ final PluginHandle splashScreenPlugin = bridge.getPlugin(SPLASH_SCREEN_PLUGIN_ID);
672
+ if (splashScreenPlugin == null) {
673
+ retrySplashScreenInvocation(
674
+ methodName,
675
+ options,
676
+ retriesRemaining,
677
+ requestToken,
678
+ "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
679
+ );
680
+ return;
617
681
  }
618
- } catch (Exception e) {
619
- logger.error("Failed to show splashscreen synchronously: " + e.getMessage());
682
+
683
+ splashScreenPlugin.invoke(methodName, new FireAndForgetPluginCall(methodName, options));
684
+ logger.info("Splashscreen " + methodName + " invoked automatically");
685
+ } catch (final Exception e) {
686
+ retrySplashScreenInvocation(
687
+ methodName,
688
+ options,
689
+ retriesRemaining,
690
+ requestToken,
691
+ "Failed to call SplashScreen " + methodName + " method: " + e.getMessage()
692
+ );
620
693
  }
694
+ }
621
695
 
622
- addSplashscreenLoaderIfNeeded();
623
- scheduleSplashscreenTimeout();
696
+ private void retrySplashScreenInvocation(
697
+ final String methodName,
698
+ final JSObject options,
699
+ final int retriesRemaining,
700
+ final int requestToken,
701
+ final String message
702
+ ) {
703
+ if (retriesRemaining > 0) {
704
+ logger.info(message + ". Retrying.");
705
+ this.mainHandler.postDelayed(
706
+ () -> invokeSplashScreenPluginMethod(methodName, options, retriesRemaining - 1, requestToken),
707
+ SPLASH_SCREEN_RETRY_DELAY_MS
708
+ );
709
+ return;
710
+ }
711
+
712
+ if ("show".equals(methodName)) {
713
+ logger.warn(message);
714
+ } else {
715
+ logger.error(message);
716
+ }
717
+ }
718
+
719
+ boolean isCurrentSplashscreenInvocationTokenForTesting(final int requestToken) {
720
+ return requestToken == this.splashscreenInvocationToken;
624
721
  }
625
722
 
626
723
  private void addSplashscreenLoaderIfNeeded() {
@@ -761,14 +858,74 @@ public class CapacitorUpdaterPlugin extends Plugin {
761
858
  return plannedDirectUpdate && !Boolean.TRUE.equals(this.autoSplashscreenTimedOut);
762
859
  }
763
860
 
861
+ static boolean shouldConsumeOnLaunchDirectUpdate(final String directUpdateMode, final boolean plannedDirectUpdate) {
862
+ return plannedDirectUpdate && "onLaunch".equals(directUpdateMode);
863
+ }
864
+
865
+ private void consumeOnLaunchDirectUpdateAttempt(final boolean plannedDirectUpdate) {
866
+ if (!shouldConsumeOnLaunchDirectUpdate(this.directUpdateMode, plannedDirectUpdate)) {
867
+ return;
868
+ }
869
+
870
+ this.onLaunchDirectUpdateUsed = true;
871
+ }
872
+
873
+ void configureDirectUpdateModeForTesting(final String directUpdateMode, final boolean onLaunchDirectUpdateUsed) {
874
+ this.directUpdateMode = directUpdateMode;
875
+ this.onLaunchDirectUpdateUsed = onLaunchDirectUpdateUsed;
876
+ }
877
+
878
+ boolean shouldUseDirectUpdateForTesting() {
879
+ return this.shouldUseDirectUpdate();
880
+ }
881
+
882
+ boolean hasConsumedOnLaunchDirectUpdateForTesting() {
883
+ return this.onLaunchDirectUpdateUsed;
884
+ }
885
+
886
+ boolean isVersionDownloadInProgress(final String version) {
887
+ return (
888
+ version != null &&
889
+ !version.isEmpty() &&
890
+ this.implementation != null &&
891
+ this.implementation.activity != null &&
892
+ DownloadWorkerManager.isVersionDownloading(this.implementation.activity, version)
893
+ );
894
+ }
895
+
896
+ void setLoggerForTesting(final Logger logger) {
897
+ this.logger = logger;
898
+ }
899
+
900
+ void completeBackgroundTaskForTesting(final BundleInfo current, final boolean plannedDirectUpdate) {
901
+ this.endBackGroundTaskWithNotif("test", current.getVersionName(), current, false, plannedDirectUpdate);
902
+ }
903
+
904
+ void scheduleDirectUpdateFinish(final BundleInfo latest) {
905
+ startNewThread(() -> {
906
+ try {
907
+ Activity currentActivity = this.getActivity();
908
+ if (currentActivity != null) {
909
+ this.implementation.activity = currentActivity;
910
+ } else {
911
+ logger.warn("directUpdateFinish: Activity is null, proceeding without refreshing the activity reference");
912
+ }
913
+ this.directUpdateFinish(latest);
914
+ } catch (final Exception e) {
915
+ logger.error("directUpdateFinish failed: " + e.getMessage());
916
+ }
917
+ });
918
+ }
919
+
764
920
  private void directUpdateFinish(final BundleInfo latest) {
765
921
  if ("onLaunch".equals(this.directUpdateMode)) {
766
922
  this.onLaunchDirectUpdateUsed = true;
767
923
  this.implementation.directUpdate = false;
768
924
  }
769
- CapacitorUpdaterPlugin.this.implementation.set(latest);
770
- CapacitorUpdaterPlugin.this._reload();
771
- sendReadyToJs(latest, "update installed", true);
925
+ if (CapacitorUpdaterPlugin.this.implementation.set(latest) && CapacitorUpdaterPlugin.this._reload()) {
926
+ this.notifyBundleSet(latest);
927
+ sendReadyToJs(latest, "update installed", true);
928
+ }
772
929
  }
773
930
 
774
931
  private void cleanupObsoleteVersions() {
@@ -1179,8 +1336,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
1179
1336
  final BundleInfo downloaded;
1180
1337
  if (manifest != null) {
1181
1338
  // For manifest downloads, we need to handle this asynchronously
1182
- // since there's no synchronous downloadManifest method in Java
1183
- CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest);
1339
+ // to avoid automatically scheduling/applying the downloaded bundle.
1340
+ // Manual download must not schedule/apply the bundle automatically.
1341
+ CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest, false);
1184
1342
  // Return immediately with a pending status - the actual result will come via listeners
1185
1343
  final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
1186
1344
  downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
@@ -1229,12 +1387,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1229
1387
  this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
1230
1388
  }
1231
1389
 
1232
- protected boolean _reload() {
1390
+ private void applyCurrentBundleToBridge() {
1233
1391
  final String path = this.implementation.getCurrentBundlePath();
1392
+ final boolean usingBuiltin = this.implementation.isUsingBuiltin();
1234
1393
  if (this.keepUrlPathAfterReload) {
1235
1394
  this.syncKeepUrlPathFlag(true);
1236
1395
  }
1237
- this.semaphoreUp();
1238
1396
  logger.info("Reloading: " + path);
1239
1397
 
1240
1398
  AtomicReference<URL> url = new AtomicReference<>();
@@ -1279,7 +1437,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1279
1437
  }
1280
1438
 
1281
1439
  if (url.get() != null) {
1282
- if (this.implementation.isUsingBuiltin()) {
1440
+ if (usingBuiltin) {
1283
1441
  this.bridge.getLocalServer().hostAssets(path);
1284
1442
  } else {
1285
1443
  this.bridge.getLocalServer().hostFiles(path);
@@ -1299,14 +1457,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
1299
1457
  } catch (MalformedURLException e) {
1300
1458
  logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
1301
1459
 
1302
- if (this.implementation.isUsingBuiltin()) {
1460
+ if (usingBuiltin) {
1303
1461
  this.bridge.setServerAssetPath(path);
1304
1462
  } else {
1305
1463
  this.bridge.setServerBasePath(path);
1306
1464
  }
1307
1465
  }
1308
1466
  } else {
1309
- if (this.implementation.isUsingBuiltin()) {
1467
+ if (usingBuiltin) {
1310
1468
  this.bridge.setServerAssetPath(path);
1311
1469
  } else {
1312
1470
  this.bridge.setServerBasePath(path);
@@ -1322,34 +1480,75 @@ public class CapacitorUpdaterPlugin extends Plugin {
1322
1480
  });
1323
1481
  }
1324
1482
  }
1483
+ }
1325
1484
 
1326
- this.checkAppReady();
1327
- this.notifyListeners("appReloaded", new JSObject());
1328
-
1329
- // Wait for the reload to complete (until notifyAppReady is called)
1485
+ protected void restoreLiveBundleStateAfterFailedReload() {
1330
1486
  try {
1331
- this.semaphoreWait(this.appReadyTimeout);
1332
- } catch (Exception e) {
1333
- logger.error("Error waiting for app ready: " + e.getMessage());
1334
- return false;
1487
+ this.applyCurrentBundleToBridge();
1488
+ } catch (final Exception e) {
1489
+ logger.warn("Failed to restore live bundle after rejected reload: " + e.getMessage());
1335
1490
  }
1491
+ }
1336
1492
 
1337
- return true;
1493
+ protected boolean _reload() {
1494
+ final int phase = this.semaphoreUp();
1495
+ this.applyCurrentBundleToBridge();
1496
+
1497
+ final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
1498
+ this.checkAppReady(waitTimeMs);
1499
+ this.notifyListeners("appReloaded", new JSObject());
1500
+
1501
+ // Wait for the reload to complete (until notifyAppReady is called)
1502
+ return this.semaphoreWait(phase, waitTimeMs);
1338
1503
  }
1339
1504
 
1340
1505
  @PluginMethod
1341
1506
  public void reload(final PluginCall call) {
1342
- try {
1343
- if (this._reload()) {
1344
- call.resolve();
1345
- } else {
1346
- logger.error("Reload failed");
1347
- call.reject("Reload failed");
1507
+ startNewThread(() -> {
1508
+ try {
1509
+ final BundleInfo current = this.implementation.getCurrentBundle();
1510
+ final BundleInfo next = this.implementation.getNextBundle();
1511
+
1512
+ if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
1513
+ final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
1514
+ final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
1515
+ logger.info("Applying pending bundle before reload: " + next.getVersionName());
1516
+ final boolean didApplyPendingBundle;
1517
+ if (next.isBuiltin()) {
1518
+ this.implementation.prepareResetStateForTransition();
1519
+ didApplyPendingBundle = true;
1520
+ } else {
1521
+ didApplyPendingBundle = this.implementation.stagePendingReload(next);
1522
+ }
1523
+ if (didApplyPendingBundle && this._reload()) {
1524
+ if (next.isBuiltin()) {
1525
+ this.implementation.finalizeResetTransition(previousBundleName, false);
1526
+ } else {
1527
+ this.implementation.finalizePendingReload(next, previousBundleName);
1528
+ }
1529
+ this.notifyBundleSet(next);
1530
+ this.implementation.setNextBundle(null);
1531
+ call.resolve();
1532
+ return;
1533
+ }
1534
+ this.implementation.restoreResetState(previousState);
1535
+ this.restoreLiveBundleStateAfterFailedReload();
1536
+ logger.error("Reload failed after applying pending bundle: " + next.getVersionName());
1537
+ call.reject("Reload failed after applying pending bundle: " + next.getVersionName());
1538
+ return;
1539
+ }
1540
+
1541
+ if (this._reload()) {
1542
+ call.resolve();
1543
+ } else {
1544
+ logger.error("Reload failed");
1545
+ call.reject("Reload failed");
1546
+ }
1547
+ } catch (final Exception e) {
1548
+ logger.error("Could not reload " + e.getMessage());
1549
+ call.reject("Could not reload", e);
1348
1550
  }
1349
- } catch (final Exception e) {
1350
- logger.error("Could not reload " + e.getMessage());
1351
- call.reject("Could not reload", e);
1352
- }
1551
+ });
1353
1552
  }
1354
1553
 
1355
1554
  @PluginMethod
@@ -1387,9 +1586,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
1387
1586
  if (!this.implementation.set(id)) {
1388
1587
  logger.info("No such bundle " + id);
1389
1588
  call.reject("Update failed, id " + id + " does not exist.");
1589
+ } else if (!this._reload()) {
1590
+ logger.error("Reload failed after setting bundle " + id);
1591
+ call.reject("Reload failed after setting bundle " + id);
1390
1592
  } else {
1391
1593
  logger.info("Bundle successfully set to " + id);
1392
- this.reload(call);
1594
+ this.notifyBundleSet(this.implementation.getBundleInfo(id));
1595
+ call.resolve();
1393
1596
  }
1394
1597
  } catch (final Exception e) {
1395
1598
  logger.error("Could not set id " + id + " " + e.getMessage());
@@ -1481,11 +1684,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
1481
1684
  startNewThread(() ->
1482
1685
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
1483
1686
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1484
- if (jsRes.has("error")) {
1485
- String error = jsRes.getString("error");
1687
+ if (jsRes.has("error") || jsRes.has("kind")) {
1688
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1486
1689
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1487
- logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1488
- call.reject(jsRes.getString("error"));
1690
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
1691
+ jsRes.put("kind", kind);
1692
+ if ("failed".equals(kind)) {
1693
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1694
+ call.reject(error.isEmpty() ? errorMessage : error);
1695
+ } else {
1696
+ if (!jsRes.has("version") || jsRes.getString("version").isEmpty()) {
1697
+ jsRes.put("version", CapacitorUpdaterPlugin.this.implementation.getCurrentBundle().getVersionName());
1698
+ }
1699
+ logger.info("getLatest returned " + kind + ": " + errorMessage);
1700
+ call.resolve(jsRes);
1701
+ }
1489
1702
  return;
1490
1703
  } else if (jsRes.has("message")) {
1491
1704
  call.reject(jsRes.getString("message"));
@@ -1497,24 +1710,83 @@ public class CapacitorUpdaterPlugin extends Plugin {
1497
1710
  );
1498
1711
  }
1499
1712
 
1500
- private boolean _reset(final Boolean toLastSuccessful) {
1713
+ private boolean _reset(final Boolean toLastSuccessful, final Boolean usePendingBundle) {
1714
+ return this.performReset(toLastSuccessful, usePendingBundle, false);
1715
+ }
1716
+
1717
+ private boolean performReset(final Boolean toLastSuccessful, final Boolean usePendingBundle, final boolean internal) {
1501
1718
  final BundleInfo fallback = this.implementation.getFallbackBundle();
1502
- this.implementation.reset();
1719
+ final BundleInfo pending = this.implementation.getNextBundle();
1720
+ final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
1721
+ final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
1503
1722
 
1504
- if (toLastSuccessful && !fallback.isBuiltin()) {
1505
- logger.info("Resetting to: " + fallback);
1506
- return this.implementation.set(fallback) && this._reload();
1723
+ if (Boolean.TRUE.equals(usePendingBundle)) {
1724
+ if (pending == null || pending.isErrorStatus()) {
1725
+ logger.error("No pending bundle available to reset to");
1726
+ return false;
1727
+ }
1728
+ if (!this.implementation.canSet(pending)) {
1729
+ logger.error("Pending bundle is not installable");
1730
+ return false;
1731
+ }
1732
+ this.implementation.prepareResetStateForTransition();
1733
+ logger.info("Resetting to pending bundle: " + pending.getVersionName());
1734
+ final boolean didApplyPendingBundle;
1735
+ if (pending.isBuiltin()) {
1736
+ didApplyPendingBundle = true;
1737
+ } else {
1738
+ didApplyPendingBundle = this.implementation.set(pending);
1739
+ }
1740
+ if (didApplyPendingBundle && this._reload()) {
1741
+ this.implementation.finalizeResetTransition(previousBundleName, internal);
1742
+ this.notifyBundleSet(pending);
1743
+ this.implementation.setNextBundle(null);
1744
+ return true;
1745
+ }
1746
+ this.implementation.restoreResetState(previousState);
1747
+ this.restoreLiveBundleStateAfterFailedReload();
1748
+ return false;
1507
1749
  }
1508
1750
 
1751
+ if (Boolean.TRUE.equals(toLastSuccessful) && !fallback.isBuiltin()) {
1752
+ if (this.implementation.canSet(fallback)) {
1753
+ this.implementation.prepareResetStateForTransition();
1754
+ logger.info("Resetting to: " + fallback);
1755
+ if (this.implementation.set(fallback) && this._reload()) {
1756
+ this.implementation.finalizeResetTransition(previousBundleName, internal);
1757
+ this.notifyBundleSet(fallback);
1758
+ return true;
1759
+ }
1760
+ if (!internal) {
1761
+ this.implementation.restoreResetState(previousState);
1762
+ this.restoreLiveBundleStateAfterFailedReload();
1763
+ return false;
1764
+ }
1765
+ logger.warn("Fallback reload failed during internal reset, resetting to native instead");
1766
+ } else {
1767
+ logger.warn("Fallback bundle is not installable, resetting to native instead");
1768
+ }
1769
+ }
1770
+
1771
+ this.implementation.prepareResetStateForTransition();
1509
1772
  logger.info("Resetting to native.");
1510
- return this._reload();
1773
+ if (this._reload()) {
1774
+ this.implementation.finalizeResetTransition(previousBundleName, internal);
1775
+ return true;
1776
+ }
1777
+ if (!internal) {
1778
+ this.implementation.restoreResetState(previousState);
1779
+ this.restoreLiveBundleStateAfterFailedReload();
1780
+ }
1781
+ return false;
1511
1782
  }
1512
1783
 
1513
1784
  @PluginMethod
1514
1785
  public void reset(final PluginCall call) {
1515
1786
  try {
1516
1787
  final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
1517
- if (this._reset(toLastSuccessful)) {
1788
+ final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
1789
+ if (this._reset(toLastSuccessful, usePendingBundle)) {
1518
1790
  call.resolve();
1519
1791
  return;
1520
1792
  }
@@ -1589,12 +1861,33 @@ public class CapacitorUpdaterPlugin extends Plugin {
1589
1861
  try {
1590
1862
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1591
1863
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1592
- if (jsRes.has("error")) {
1593
- String error = jsRes.getString("error");
1864
+ if (jsRes.has("error") || jsRes.has("kind")) {
1865
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1866
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1594
1867
  String errorMessage = jsRes.has("message")
1595
1868
  ? jsRes.getString("message")
1596
1869
  : "server did not provide a message";
1597
- logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1870
+ int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
1871
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(
1872
+ jsRes.has("kind") ? jsRes.getString("kind") : null
1873
+ );
1874
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
1875
+ CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
1876
+ kind,
1877
+ error,
1878
+ errorMessage,
1879
+ statusCode,
1880
+ latestVersion,
1881
+ current
1882
+ );
1883
+
1884
+ if ("failed".equals(kind)) {
1885
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1886
+ } else if ("blocked".equals(kind)) {
1887
+ logger.info("Update check blocked with error: " + error);
1888
+ } else {
1889
+ logger.info("No new version available");
1890
+ }
1598
1891
  } else if (jsRes.has("version")) {
1599
1892
  String newVersion = jsRes.getString("version");
1600
1893
  String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
@@ -1716,11 +2009,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
1716
2009
  }
1717
2010
 
1718
2011
  private void checkAppReady() {
2012
+ this.checkAppReady(this.resolveAppReadyCheckTimeoutMs());
2013
+ }
2014
+
2015
+ private void checkAppReady(final long waitTimeMs) {
1719
2016
  try {
1720
2017
  if (this.appReadyCheck != null) {
1721
2018
  this.appReadyCheck.interrupt();
1722
2019
  }
1723
- this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck());
2020
+ this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck(waitTimeMs));
1724
2021
  } catch (final Exception e) {
1725
2022
  logger.error("Failed to start " + DeferredNotifyAppReadyCheck.class.getName() + " " + e.getMessage());
1726
2023
  }
@@ -1735,6 +2032,31 @@ public class CapacitorUpdaterPlugin extends Plugin {
1735
2032
  }
1736
2033
  }
1737
2034
 
2035
+ private String getUpdateResponseKind(final String kind) {
2036
+ if ("up_to_date".equals(kind) || "blocked".equals(kind) || "failed".equals(kind)) {
2037
+ return kind;
2038
+ }
2039
+ return "failed";
2040
+ }
2041
+
2042
+ private void notifyUpdateCheckResult(
2043
+ final String kind,
2044
+ final String error,
2045
+ final String message,
2046
+ final int statusCode,
2047
+ final String version,
2048
+ final BundleInfo current
2049
+ ) {
2050
+ JSObject ret = new JSObject();
2051
+ ret.put("kind", kind);
2052
+ ret.put("error", error);
2053
+ ret.put("message", message);
2054
+ ret.put("statusCode", statusCode);
2055
+ ret.put("version", version);
2056
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
2057
+ this.notifyListeners("updateCheckResult", ret);
2058
+ }
2059
+
1738
2060
  private void ensureBridgeSet() {
1739
2061
  if (this.bridge != null && this.bridge.getWebView() != null) {
1740
2062
  logger.setBridge(this.bridge);
@@ -1772,11 +2094,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1772
2094
  String latestVersionName,
1773
2095
  BundleInfo current,
1774
2096
  Boolean error,
1775
- Boolean isDirectUpdate,
2097
+ Boolean plannedDirectUpdate,
1776
2098
  String failureAction,
1777
2099
  String failureEvent,
1778
2100
  boolean shouldSendStats
1779
2101
  ) {
2102
+ this.consumeOnLaunchDirectUpdateAttempt(Boolean.TRUE.equals(plannedDirectUpdate));
1780
2103
  if (error) {
1781
2104
  logger.info(
1782
2105
  "endBackGroundTaskWithNotif error: " +
@@ -1796,7 +2119,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1796
2119
  final JSObject ret = new JSObject();
1797
2120
  ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
1798
2121
  this.notifyListeners("noNeedUpdate", ret);
1799
- this.sendReadyToJs(current, msg, isDirectUpdate);
2122
+ this.sendReadyToJs(current, msg, plannedDirectUpdate);
1800
2123
  this.backgroundDownloadTask = null;
1801
2124
  this.downloadStartTimeMs = 0;
1802
2125
  logger.info("endBackGroundTaskWithNotif " + msg);
@@ -1830,7 +2153,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
1830
2153
  private Thread backgroundDownload() {
1831
2154
  final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
1832
2155
  final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
1833
- this.implementation.directUpdate = initialDirectUpdateAllowed;
1834
2156
  final String messageUpdate = initialDirectUpdateAllowed
1835
2157
  ? "Update will occur now."
1836
2158
  : "Update will occur next time app moves to background.";
@@ -1844,30 +2166,37 @@ public class CapacitorUpdaterPlugin extends Plugin {
1844
2166
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1845
2167
 
1846
2168
  // Handle network errors and other failures first
1847
- if (jsRes.has("error")) {
1848
- String error = jsRes.getString("error");
2169
+ if (jsRes.has("error") || jsRes.has("kind")) {
2170
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1849
2171
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1850
2172
  int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
1851
- boolean responseIsOk = statusCode >= 200 && statusCode < 300;
1852
-
1853
- logger.error(
1854
- "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
1855
- );
2173
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
1856
2174
  String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
2175
+ CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(kind, error, errorMessage, statusCode, latestVersion, current);
2176
+
2177
+ if ("up_to_date".equals(kind)) {
2178
+ logger.info("No new version available");
2179
+ } else if ("blocked".equals(kind)) {
2180
+ logger.info("Update check blocked with error: " + error);
2181
+ } else {
2182
+ logger.error(
2183
+ "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
2184
+ );
2185
+ }
1857
2186
 
2187
+ boolean isFailure = "failed".equals(kind);
1858
2188
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1859
2189
  errorMessage,
1860
2190
  latestVersion,
1861
2191
  current,
1862
- true,
2192
+ isFailure,
1863
2193
  plannedDirectUpdate,
1864
2194
  "download_fail",
1865
2195
  "downloadFailed",
1866
- !responseIsOk
2196
+ isFailure
1867
2197
  );
1868
2198
  return;
1869
2199
  }
1870
-
1871
2200
  try {
1872
2201
  final String latestVersionName = jsRes.getString("version");
1873
2202
 
@@ -1878,7 +2207,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1878
2207
  );
1879
2208
  if (directUpdateAllowedNow) {
1880
2209
  logger.info("Direct update to builtin version");
1881
- this._reset(false);
2210
+ this._reset(false, false);
1882
2211
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1883
2212
  "Updated to builtin version",
1884
2213
  latestVersionName,
@@ -1898,7 +2227,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1898
2227
  "Next update will be to builtin version",
1899
2228
  latestVersionName,
1900
2229
  current,
1901
- false
2230
+ false,
2231
+ plannedDirectUpdate
1902
2232
  );
1903
2233
  }
1904
2234
  return;
@@ -1934,7 +2264,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1934
2264
  );
1935
2265
  return;
1936
2266
  }
1937
- if (latest.isDownloaded()) {
2267
+ if (latest.isDownloaded() && BundleStatus.DOWNLOADING != latest.getStatus()) {
1938
2268
  logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
1939
2269
  final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
1940
2270
  plannedDirectUpdate
@@ -1955,15 +2285,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
1955
2285
  );
1956
2286
  return;
1957
2287
  }
1958
- CapacitorUpdaterPlugin.this.implementation.set(latest);
1959
- CapacitorUpdaterPlugin.this._reload();
1960
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1961
- "Update installed",
1962
- latestVersionName,
1963
- latest,
1964
- false,
1965
- true
1966
- );
2288
+ if (
2289
+ CapacitorUpdaterPlugin.this.implementation.set(latest) && CapacitorUpdaterPlugin.this._reload()
2290
+ ) {
2291
+ CapacitorUpdaterPlugin.this.notifyBundleSet(latest);
2292
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2293
+ "Update installed",
2294
+ latestVersionName,
2295
+ latest,
2296
+ false,
2297
+ true
2298
+ );
2299
+ } else {
2300
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2301
+ "Update install failed",
2302
+ latestVersionName,
2303
+ latest,
2304
+ true,
2305
+ true
2306
+ );
2307
+ }
1967
2308
  } else {
1968
2309
  if (plannedDirectUpdate && !directUpdateAllowedNow) {
1969
2310
  logger.info(
@@ -1976,7 +2317,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1976
2317
  "update downloaded, will install next background",
1977
2318
  latestVersionName,
1978
2319
  latest,
1979
- false
2320
+ false,
2321
+ plannedDirectUpdate
1980
2322
  );
1981
2323
  }
1982
2324
  return;
@@ -1993,6 +2335,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
1993
2335
  }
1994
2336
  }
1995
2337
  }
2338
+ final boolean retryingInFlightDownload =
2339
+ latest != null &&
2340
+ BundleStatus.DOWNLOADING == latest.getStatus() &&
2341
+ CapacitorUpdaterPlugin.this.isVersionDownloadInProgress(latest.getVersionName());
2342
+ CapacitorUpdaterPlugin.this.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate);
2343
+ CapacitorUpdaterPlugin.this.implementation.directUpdate = retryingInFlightDownload
2344
+ ? Boolean.TRUE.equals(CapacitorUpdaterPlugin.this.implementation.directUpdate) || initialDirectUpdateAllowed
2345
+ : initialDirectUpdateAllowed;
1996
2346
  startNewThread(() -> {
1997
2347
  try {
1998
2348
  logger.info(
@@ -2041,7 +2391,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
2041
2391
  });
2042
2392
  } else {
2043
2393
  logger.info("No need to update, " + current.getId() + " is the latest bundle.");
2044
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif("No need to update", latestVersionName, current, false);
2394
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2395
+ "No need to update",
2396
+ latestVersionName,
2397
+ current,
2398
+ false,
2399
+ plannedDirectUpdate
2400
+ );
2045
2401
  }
2046
2402
  } catch (final Exception e) {
2047
2403
  logger.error("error in update check " + e.getMessage());
@@ -2085,15 +2441,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
2085
2441
  if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
2086
2442
  // There is a next bundle waiting for activation
2087
2443
  logger.debug("Next bundle is: " + next.getVersionName());
2088
- if (this.implementation.set(next) && this._reload()) {
2089
- logger.info("Updated to bundle: " + next.getVersionName());
2090
- this.implementation.setNextBundle(null);
2091
- } else {
2092
- logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
2093
- }
2444
+ startNewThread(() -> {
2445
+ if (this.implementation.set(next) && this._reload()) {
2446
+ logger.info("Updated to bundle: " + next.getVersionName());
2447
+ this.notifyBundleSet(next);
2448
+ this.implementation.setNextBundle(null);
2449
+ } else {
2450
+ logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
2451
+ }
2452
+ });
2094
2453
  }
2095
2454
  } catch (final Exception e) {
2096
- logger.error("Error during onActivityStopped " + e.getMessage());
2455
+ logger.error("Error during installNext " + e);
2097
2456
  }
2098
2457
  }
2099
2458
 
@@ -2116,7 +2475,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2116
2475
  this.notifyListeners("updateFailed", ret);
2117
2476
  this.implementation.sendStats("update_fail", current.getVersionName());
2118
2477
  this.implementation.setError(current);
2119
- this._reset(true);
2478
+ this.performReset(true, false, true);
2120
2479
  if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
2121
2480
  logger.info("Deleting failing bundle: " + current.getVersionName());
2122
2481
  try {
@@ -2135,11 +2494,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
2135
2494
 
2136
2495
  private class DeferredNotifyAppReadyCheck implements Runnable {
2137
2496
 
2497
+ private final long waitTimeMs;
2498
+
2499
+ DeferredNotifyAppReadyCheck(final long waitTimeMs) {
2500
+ this.waitTimeMs = waitTimeMs;
2501
+ }
2502
+
2138
2503
  @Override
2139
2504
  public void run() {
2140
2505
  try {
2141
- logger.info("Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady");
2142
- Thread.sleep(CapacitorUpdaterPlugin.this.appReadyTimeout);
2506
+ logger.info("Wait for " + this.waitTimeMs + "ms, then check for notifyAppReady");
2507
+ Thread.sleep(this.waitTimeMs);
2143
2508
  CapacitorUpdaterPlugin.this.checkRevert();
2144
2509
  CapacitorUpdaterPlugin.this.appReadyCheck = null;
2145
2510
  } catch (final InterruptedException e) {
@@ -2492,9 +2857,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
2492
2857
 
2493
2858
  JSObject result = new JSObject();
2494
2859
  try {
2495
- PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0);
2860
+ PackageInfo pInfo = getCurrentPackageInfo();
2496
2861
  result.put("currentVersionName", pInfo.versionName);
2497
- result.put("currentVersionCode", String.valueOf(pInfo.versionCode));
2862
+ result.put("currentVersionCode", getVersionCode(pInfo));
2498
2863
  } catch (PackageManager.NameNotFoundException e) {
2499
2864
  result.put("currentVersionName", "0.0.0");
2500
2865
  result.put("currentVersionCode", "0");