@capgo/capacitor-updater 6.43.5 → 6.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 = "8.43.5";
92
+ private final String pluginVersion = "6.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() {
@@ -1230,12 +1387,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1230
1387
  this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
1231
1388
  }
1232
1389
 
1233
- protected boolean _reload() {
1390
+ private void applyCurrentBundleToBridge() {
1234
1391
  final String path = this.implementation.getCurrentBundlePath();
1392
+ final boolean usingBuiltin = this.implementation.isUsingBuiltin();
1235
1393
  if (this.keepUrlPathAfterReload) {
1236
1394
  this.syncKeepUrlPathFlag(true);
1237
1395
  }
1238
- this.semaphoreUp();
1239
1396
  logger.info("Reloading: " + path);
1240
1397
 
1241
1398
  AtomicReference<URL> url = new AtomicReference<>();
@@ -1280,7 +1437,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1280
1437
  }
1281
1438
 
1282
1439
  if (url.get() != null) {
1283
- if (this.implementation.isUsingBuiltin()) {
1440
+ if (usingBuiltin) {
1284
1441
  this.bridge.getLocalServer().hostAssets(path);
1285
1442
  } else {
1286
1443
  this.bridge.getLocalServer().hostFiles(path);
@@ -1300,14 +1457,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
1300
1457
  } catch (MalformedURLException e) {
1301
1458
  logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
1302
1459
 
1303
- if (this.implementation.isUsingBuiltin()) {
1460
+ if (usingBuiltin) {
1304
1461
  this.bridge.setServerAssetPath(path);
1305
1462
  } else {
1306
1463
  this.bridge.setServerBasePath(path);
1307
1464
  }
1308
1465
  }
1309
1466
  } else {
1310
- if (this.implementation.isUsingBuiltin()) {
1467
+ if (usingBuiltin) {
1311
1468
  this.bridge.setServerAssetPath(path);
1312
1469
  } else {
1313
1470
  this.bridge.setServerBasePath(path);
@@ -1323,34 +1480,75 @@ public class CapacitorUpdaterPlugin extends Plugin {
1323
1480
  });
1324
1481
  }
1325
1482
  }
1483
+ }
1326
1484
 
1327
- this.checkAppReady();
1328
- this.notifyListeners("appReloaded", new JSObject());
1329
-
1330
- // Wait for the reload to complete (until notifyAppReady is called)
1485
+ protected void restoreLiveBundleStateAfterFailedReload() {
1331
1486
  try {
1332
- this.semaphoreWait(this.appReadyTimeout);
1333
- } catch (Exception e) {
1334
- logger.error("Error waiting for app ready: " + e.getMessage());
1335
- return false;
1487
+ this.applyCurrentBundleToBridge();
1488
+ } catch (final Exception e) {
1489
+ logger.warn("Failed to restore live bundle after rejected reload: " + e.getMessage());
1336
1490
  }
1491
+ }
1337
1492
 
1338
- 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);
1339
1503
  }
1340
1504
 
1341
1505
  @PluginMethod
1342
1506
  public void reload(final PluginCall call) {
1343
- try {
1344
- if (this._reload()) {
1345
- call.resolve();
1346
- } else {
1347
- logger.error("Reload failed");
1348
- 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);
1349
1550
  }
1350
- } catch (final Exception e) {
1351
- logger.error("Could not reload " + e.getMessage());
1352
- call.reject("Could not reload", e);
1353
- }
1551
+ });
1354
1552
  }
1355
1553
 
1356
1554
  @PluginMethod
@@ -1388,9 +1586,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
1388
1586
  if (!this.implementation.set(id)) {
1389
1587
  logger.info("No such bundle " + id);
1390
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);
1391
1592
  } else {
1392
1593
  logger.info("Bundle successfully set to " + id);
1393
- this.reload(call);
1594
+ this.notifyBundleSet(this.implementation.getBundleInfo(id));
1595
+ call.resolve();
1394
1596
  }
1395
1597
  } catch (final Exception e) {
1396
1598
  logger.error("Could not set id " + id + " " + e.getMessage());
@@ -1482,11 +1684,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
1482
1684
  startNewThread(() ->
1483
1685
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
1484
1686
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1485
- if (jsRes.has("error")) {
1486
- String error = jsRes.getString("error");
1687
+ if (jsRes.has("error") || jsRes.has("kind")) {
1688
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1487
1689
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1488
- logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1489
- 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
+ }
1490
1702
  return;
1491
1703
  } else if (jsRes.has("message")) {
1492
1704
  call.reject(jsRes.getString("message"));
@@ -1498,24 +1710,83 @@ public class CapacitorUpdaterPlugin extends Plugin {
1498
1710
  );
1499
1711
  }
1500
1712
 
1501
- 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) {
1502
1718
  final BundleInfo fallback = this.implementation.getFallbackBundle();
1503
- 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();
1504
1722
 
1505
- if (toLastSuccessful && !fallback.isBuiltin()) {
1506
- logger.info("Resetting to: " + fallback);
1507
- 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;
1508
1749
  }
1509
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();
1510
1772
  logger.info("Resetting to native.");
1511
- 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;
1512
1782
  }
1513
1783
 
1514
1784
  @PluginMethod
1515
1785
  public void reset(final PluginCall call) {
1516
1786
  try {
1517
1787
  final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
1518
- if (this._reset(toLastSuccessful)) {
1788
+ final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
1789
+ if (this._reset(toLastSuccessful, usePendingBundle)) {
1519
1790
  call.resolve();
1520
1791
  return;
1521
1792
  }
@@ -1590,12 +1861,33 @@ public class CapacitorUpdaterPlugin extends Plugin {
1590
1861
  try {
1591
1862
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1592
1863
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1593
- if (jsRes.has("error")) {
1594
- 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") : "";
1595
1867
  String errorMessage = jsRes.has("message")
1596
1868
  ? jsRes.getString("message")
1597
1869
  : "server did not provide a message";
1598
- 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
+ }
1599
1891
  } else if (jsRes.has("version")) {
1600
1892
  String newVersion = jsRes.getString("version");
1601
1893
  String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
@@ -1717,11 +2009,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
1717
2009
  }
1718
2010
 
1719
2011
  private void checkAppReady() {
2012
+ this.checkAppReady(this.resolveAppReadyCheckTimeoutMs());
2013
+ }
2014
+
2015
+ private void checkAppReady(final long waitTimeMs) {
1720
2016
  try {
1721
2017
  if (this.appReadyCheck != null) {
1722
2018
  this.appReadyCheck.interrupt();
1723
2019
  }
1724
- this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck());
2020
+ this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck(waitTimeMs));
1725
2021
  } catch (final Exception e) {
1726
2022
  logger.error("Failed to start " + DeferredNotifyAppReadyCheck.class.getName() + " " + e.getMessage());
1727
2023
  }
@@ -1736,6 +2032,31 @@ public class CapacitorUpdaterPlugin extends Plugin {
1736
2032
  }
1737
2033
  }
1738
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
+
1739
2060
  private void ensureBridgeSet() {
1740
2061
  if (this.bridge != null && this.bridge.getWebView() != null) {
1741
2062
  logger.setBridge(this.bridge);
@@ -1773,11 +2094,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1773
2094
  String latestVersionName,
1774
2095
  BundleInfo current,
1775
2096
  Boolean error,
1776
- Boolean isDirectUpdate,
2097
+ Boolean plannedDirectUpdate,
1777
2098
  String failureAction,
1778
2099
  String failureEvent,
1779
2100
  boolean shouldSendStats
1780
2101
  ) {
2102
+ this.consumeOnLaunchDirectUpdateAttempt(Boolean.TRUE.equals(plannedDirectUpdate));
1781
2103
  if (error) {
1782
2104
  logger.info(
1783
2105
  "endBackGroundTaskWithNotif error: " +
@@ -1797,7 +2119,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1797
2119
  final JSObject ret = new JSObject();
1798
2120
  ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
1799
2121
  this.notifyListeners("noNeedUpdate", ret);
1800
- this.sendReadyToJs(current, msg, isDirectUpdate);
2122
+ this.sendReadyToJs(current, msg, plannedDirectUpdate);
1801
2123
  this.backgroundDownloadTask = null;
1802
2124
  this.downloadStartTimeMs = 0;
1803
2125
  logger.info("endBackGroundTaskWithNotif " + msg);
@@ -1831,7 +2153,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
1831
2153
  private Thread backgroundDownload() {
1832
2154
  final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
1833
2155
  final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
1834
- this.implementation.directUpdate = initialDirectUpdateAllowed;
1835
2156
  final String messageUpdate = initialDirectUpdateAllowed
1836
2157
  ? "Update will occur now."
1837
2158
  : "Update will occur next time app moves to background.";
@@ -1845,30 +2166,37 @@ public class CapacitorUpdaterPlugin extends Plugin {
1845
2166
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1846
2167
 
1847
2168
  // Handle network errors and other failures first
1848
- if (jsRes.has("error")) {
1849
- String error = jsRes.getString("error");
2169
+ if (jsRes.has("error") || jsRes.has("kind")) {
2170
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1850
2171
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1851
2172
  int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
1852
- boolean responseIsOk = statusCode >= 200 && statusCode < 300;
1853
-
1854
- logger.error(
1855
- "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
1856
- );
2173
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
1857
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
+ }
1858
2186
 
2187
+ boolean isFailure = "failed".equals(kind);
1859
2188
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1860
2189
  errorMessage,
1861
2190
  latestVersion,
1862
2191
  current,
1863
- true,
2192
+ isFailure,
1864
2193
  plannedDirectUpdate,
1865
2194
  "download_fail",
1866
2195
  "downloadFailed",
1867
- !responseIsOk
2196
+ isFailure
1868
2197
  );
1869
2198
  return;
1870
2199
  }
1871
-
1872
2200
  try {
1873
2201
  final String latestVersionName = jsRes.getString("version");
1874
2202
 
@@ -1879,7 +2207,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1879
2207
  );
1880
2208
  if (directUpdateAllowedNow) {
1881
2209
  logger.info("Direct update to builtin version");
1882
- this._reset(false);
2210
+ this._reset(false, false);
1883
2211
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1884
2212
  "Updated to builtin version",
1885
2213
  latestVersionName,
@@ -1899,7 +2227,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1899
2227
  "Next update will be to builtin version",
1900
2228
  latestVersionName,
1901
2229
  current,
1902
- false
2230
+ false,
2231
+ plannedDirectUpdate
1903
2232
  );
1904
2233
  }
1905
2234
  return;
@@ -1935,7 +2264,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1935
2264
  );
1936
2265
  return;
1937
2266
  }
1938
- if (latest.isDownloaded()) {
2267
+ if (latest.isDownloaded() && BundleStatus.DOWNLOADING != latest.getStatus()) {
1939
2268
  logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
1940
2269
  final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
1941
2270
  plannedDirectUpdate
@@ -1956,15 +2285,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
1956
2285
  );
1957
2286
  return;
1958
2287
  }
1959
- CapacitorUpdaterPlugin.this.implementation.set(latest);
1960
- CapacitorUpdaterPlugin.this._reload();
1961
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1962
- "Update installed",
1963
- latestVersionName,
1964
- latest,
1965
- false,
1966
- true
1967
- );
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
+ }
1968
2308
  } else {
1969
2309
  if (plannedDirectUpdate && !directUpdateAllowedNow) {
1970
2310
  logger.info(
@@ -1977,7 +2317,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1977
2317
  "update downloaded, will install next background",
1978
2318
  latestVersionName,
1979
2319
  latest,
1980
- false
2320
+ false,
2321
+ plannedDirectUpdate
1981
2322
  );
1982
2323
  }
1983
2324
  return;
@@ -1994,6 +2335,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
1994
2335
  }
1995
2336
  }
1996
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;
1997
2346
  startNewThread(() -> {
1998
2347
  try {
1999
2348
  logger.info(
@@ -2042,7 +2391,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
2042
2391
  });
2043
2392
  } else {
2044
2393
  logger.info("No need to update, " + current.getId() + " is the latest bundle.");
2045
- 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
+ );
2046
2401
  }
2047
2402
  } catch (final Exception e) {
2048
2403
  logger.error("error in update check " + e.getMessage());
@@ -2086,15 +2441,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
2086
2441
  if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
2087
2442
  // There is a next bundle waiting for activation
2088
2443
  logger.debug("Next bundle is: " + next.getVersionName());
2089
- if (this.implementation.set(next) && this._reload()) {
2090
- logger.info("Updated to bundle: " + next.getVersionName());
2091
- this.implementation.setNextBundle(null);
2092
- } else {
2093
- logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
2094
- }
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
+ });
2095
2453
  }
2096
2454
  } catch (final Exception e) {
2097
- logger.error("Error during onActivityStopped " + e.getMessage());
2455
+ logger.error("Error during installNext " + e);
2098
2456
  }
2099
2457
  }
2100
2458
 
@@ -2117,7 +2475,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2117
2475
  this.notifyListeners("updateFailed", ret);
2118
2476
  this.implementation.sendStats("update_fail", current.getVersionName());
2119
2477
  this.implementation.setError(current);
2120
- this._reset(true);
2478
+ this.performReset(true, false, true);
2121
2479
  if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
2122
2480
  logger.info("Deleting failing bundle: " + current.getVersionName());
2123
2481
  try {
@@ -2136,11 +2494,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
2136
2494
 
2137
2495
  private class DeferredNotifyAppReadyCheck implements Runnable {
2138
2496
 
2497
+ private final long waitTimeMs;
2498
+
2499
+ DeferredNotifyAppReadyCheck(final long waitTimeMs) {
2500
+ this.waitTimeMs = waitTimeMs;
2501
+ }
2502
+
2139
2503
  @Override
2140
2504
  public void run() {
2141
2505
  try {
2142
- logger.info("Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady");
2143
- Thread.sleep(CapacitorUpdaterPlugin.this.appReadyTimeout);
2506
+ logger.info("Wait for " + this.waitTimeMs + "ms, then check for notifyAppReady");
2507
+ Thread.sleep(this.waitTimeMs);
2144
2508
  CapacitorUpdaterPlugin.this.checkRevert();
2145
2509
  CapacitorUpdaterPlugin.this.appReadyCheck = null;
2146
2510
  } catch (final InterruptedException e) {
@@ -2493,9 +2857,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
2493
2857
 
2494
2858
  JSObject result = new JSObject();
2495
2859
  try {
2496
- PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0);
2860
+ PackageInfo pInfo = getCurrentPackageInfo();
2497
2861
  result.put("currentVersionName", pInfo.versionName);
2498
- result.put("currentVersionCode", String.valueOf(pInfo.versionCode));
2862
+ result.put("currentVersionCode", getVersionCode(pInfo));
2499
2863
  } catch (PackageManager.NameNotFoundException e) {
2500
2864
  result.put("currentVersionName", "0.0.0");
2501
2865
  result.put("currentVersionCode", "0");