@capgo/capacitor-updater 8.47.5 → 8.47.7

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.
@@ -115,6 +115,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
115
115
  private static final int SPLASH_SCREEN_RETRY_DELAY_MS = 100;
116
116
  private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
117
117
  private static final long PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS = 30000L;
118
+ private static final long PREVIEW_TRANSITION_LOADER_TIMEOUT_MS = 60000L;
118
119
  static final int APPLICATION_EXIT_REASON_UNKNOWN = 0;
119
120
  static final int APPLICATION_EXIT_REASON_EXIT_SELF = 1;
120
121
  static final int APPLICATION_EXIT_REASON_SIGNALED = 2;
@@ -128,7 +129,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
128
129
  static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
129
130
  static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
130
131
 
131
- private final String pluginVersion = "8.47.5";
132
+ private final String pluginVersion = "8.47.7";
132
133
  private static final String DELAY_CONDITION_PREFERENCES = "";
133
134
 
134
135
  private SharedPreferences.Editor editor;
@@ -159,9 +160,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
159
160
  private volatile boolean onLaunchDirectUpdateUsed = false;
160
161
  Boolean shakeMenuEnabled = false;
161
162
  Boolean shakeChannelSelectorEnabled = false;
162
- Boolean previewSessionEnabled = false;
163
+ volatile Boolean previewSessionEnabled = false;
163
164
  private Boolean previewSessionAlertPending = false;
164
- private Boolean isLeavingPreviewForIncomingLink = false;
165
+ private volatile Boolean isLeavingPreviewForIncomingLink = false;
165
166
  private Boolean allowManualBundleError = false;
166
167
  private Boolean allowPreview = false;
167
168
  Boolean allowSetDefaultChannel = true;
@@ -170,6 +171,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
170
171
  return this.updateUrl;
171
172
  }
172
173
 
174
+ private boolean isPreviewSessionStateActive() {
175
+ return (
176
+ Boolean.TRUE.equals(this.previewSessionEnabled) ||
177
+ Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink) ||
178
+ (this.implementation != null && this.implementation.previewSession)
179
+ );
180
+ }
181
+
182
+ private boolean shouldBlockAutoUpdateForPreviewSession() {
183
+ if (!this.isPreviewSessionStateActive()) {
184
+ return false;
185
+ }
186
+
187
+ logger.info("Preview session is active. Skipping normal auto-update work.");
188
+ return true;
189
+ }
190
+
191
+ private void clearIncomingPreviewTransition() {
192
+ this.isLeavingPreviewForIncomingLink = false;
193
+ if (!Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation != null) {
194
+ this.implementation.previewSession = false;
195
+ }
196
+ }
197
+
173
198
  // Used for activity-based foreground/background detection on Android < 14
174
199
  private Boolean isPreviousMainActivity = true;
175
200
 
@@ -198,6 +223,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
198
223
  private final Handler mainHandler = new Handler(Looper.getMainLooper());
199
224
  private FrameLayout splashscreenLoaderOverlay;
200
225
  private Runnable splashscreenTimeoutRunnable;
226
+ private FrameLayout previewTransitionLoaderOverlay;
227
+ private Runnable previewTransitionLoaderTimeoutRunnable;
228
+ private boolean previewTransitionLoaderRequested = false;
201
229
  private WebViewListener webViewStatsListener;
202
230
 
203
231
  private static final class FireAndForgetPluginCall extends PluginCall {
@@ -513,7 +541,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
513
541
  logger.info("Using preview appId " + previewAppId);
514
542
  }
515
543
  this.shakeMenuEnabled = true;
516
- this.shakeChannelSelectorEnabled = false;
544
+ this.shakeChannelSelectorEnabled = this.prefs.contains(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY)
545
+ ? this.prefs.getBoolean(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY, false)
546
+ : this.shakeChannelSelectorEnabled;
517
547
  }
518
548
  boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
519
549
 
@@ -658,6 +688,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
658
688
  if (this.autoSplashscreen) {
659
689
  this.hideSplashscreen();
660
690
  }
691
+ this.hidePreviewTransitionLoader("app-ready");
661
692
  }
662
693
 
663
694
  private void hideSplashscreen() {
@@ -769,6 +800,38 @@ public class CapacitorUpdaterPlugin extends Plugin {
769
800
  return requestToken == this.splashscreenInvocationToken;
770
801
  }
771
802
 
803
+ private FrameLayout createLoaderOverlay(final Activity activity, final boolean blocksTouches, final int backgroundColor) {
804
+ final ProgressBar progressBar = new ProgressBar(activity);
805
+ progressBar.setIndeterminate(true);
806
+
807
+ final FrameLayout overlay = new FrameLayout(activity);
808
+ overlay.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
809
+ overlay.setClickable(blocksTouches);
810
+ overlay.setFocusable(blocksTouches);
811
+ overlay.setBackgroundColor(backgroundColor);
812
+ overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
813
+
814
+ final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
815
+ ViewGroup.LayoutParams.WRAP_CONTENT,
816
+ ViewGroup.LayoutParams.WRAP_CONTENT
817
+ );
818
+ params.gravity = Gravity.CENTER;
819
+ overlay.addView(progressBar, params);
820
+ return overlay;
821
+ }
822
+
823
+ private void attachLoaderOverlay(final Activity activity, final FrameLayout overlay) {
824
+ final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
825
+ decorView.addView(overlay);
826
+ }
827
+
828
+ private void removeLoaderOverlay(final FrameLayout overlay) {
829
+ final ViewGroup parent = (ViewGroup) overlay.getParent();
830
+ if (parent != null) {
831
+ parent.removeView(overlay);
832
+ }
833
+ }
834
+
772
835
  private void addSplashscreenLoaderIfNeeded() {
773
836
  if (!Boolean.TRUE.equals(this.autoSplashscreenLoader)) {
774
837
  return;
@@ -785,26 +848,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
785
848
  return;
786
849
  }
787
850
 
788
- ProgressBar progressBar = new ProgressBar(activity);
789
- progressBar.setIndeterminate(true);
790
-
791
- FrameLayout overlay = new FrameLayout(activity);
792
- overlay.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
793
- overlay.setClickable(false);
794
- overlay.setFocusable(false);
795
- overlay.setBackgroundColor(Color.TRANSPARENT);
796
- overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
797
-
798
- FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
799
- ViewGroup.LayoutParams.WRAP_CONTENT,
800
- ViewGroup.LayoutParams.WRAP_CONTENT
801
- );
802
- params.gravity = Gravity.CENTER;
803
- overlay.addView(progressBar, params);
804
-
805
- ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
806
- decorView.addView(overlay);
807
-
851
+ FrameLayout overlay = createLoaderOverlay(activity, false, Color.TRANSPARENT);
852
+ attachLoaderOverlay(activity, overlay);
808
853
  this.splashscreenLoaderOverlay = overlay;
809
854
  };
810
855
 
@@ -818,10 +863,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
818
863
  private void removeSplashscreenLoader() {
819
864
  Runnable removeLoader = () -> {
820
865
  if (this.splashscreenLoaderOverlay != null) {
821
- ViewGroup parent = (ViewGroup) this.splashscreenLoaderOverlay.getParent();
822
- if (parent != null) {
823
- parent.removeView(this.splashscreenLoaderOverlay);
824
- }
866
+ removeLoaderOverlay(this.splashscreenLoaderOverlay);
825
867
  this.splashscreenLoaderOverlay = null;
826
868
  }
827
869
  };
@@ -833,6 +875,84 @@ public class CapacitorUpdaterPlugin extends Plugin {
833
875
  }
834
876
  }
835
877
 
878
+ private void showPreviewTransitionLoader(final String reason) {
879
+ this.previewTransitionLoaderRequested = true;
880
+ final Runnable showLoader = () -> {
881
+ if (!this.previewTransitionLoaderRequested) {
882
+ return;
883
+ }
884
+
885
+ if (this.previewTransitionLoaderOverlay != null) {
886
+ cancelPreviewTransitionLoaderTimeout();
887
+ schedulePreviewTransitionLoaderTimeout();
888
+ this.previewTransitionLoaderOverlay.bringToFront();
889
+ return;
890
+ }
891
+
892
+ final Activity activity = getActivity();
893
+ if (activity == null) {
894
+ logger.warn("Preview transition loader unavailable: activity missing for " + reason);
895
+ this.previewTransitionLoaderRequested = false;
896
+ return;
897
+ }
898
+
899
+ cancelPreviewTransitionLoaderTimeout();
900
+ schedulePreviewTransitionLoaderTimeout();
901
+
902
+ final FrameLayout overlay = createLoaderOverlay(activity, true, Color.argb(46, 0, 0, 0));
903
+ attachLoaderOverlay(activity, overlay);
904
+ this.previewTransitionLoaderOverlay = overlay;
905
+ logger.info("Preview transition loader shown: " + reason);
906
+ };
907
+
908
+ if (Looper.myLooper() == Looper.getMainLooper()) {
909
+ showLoader.run();
910
+ } else {
911
+ this.mainHandler.post(showLoader);
912
+ }
913
+ }
914
+
915
+ private void hidePreviewTransitionLoader(final String reason) {
916
+ if (
917
+ !this.previewTransitionLoaderRequested &&
918
+ this.previewTransitionLoaderOverlay == null &&
919
+ this.previewTransitionLoaderTimeoutRunnable == null
920
+ ) {
921
+ return;
922
+ }
923
+
924
+ final Runnable hideLoader = () -> {
925
+ this.previewTransitionLoaderRequested = false;
926
+ cancelPreviewTransitionLoaderTimeout();
927
+ if (this.previewTransitionLoaderOverlay == null) {
928
+ return;
929
+ }
930
+
931
+ removeLoaderOverlay(this.previewTransitionLoaderOverlay);
932
+ this.previewTransitionLoaderOverlay = null;
933
+ logger.info("Preview transition loader hidden: " + reason);
934
+ };
935
+
936
+ if (Looper.myLooper() == Looper.getMainLooper()) {
937
+ hideLoader.run();
938
+ } else {
939
+ this.mainHandler.post(hideLoader);
940
+ }
941
+ }
942
+
943
+ private void schedulePreviewTransitionLoaderTimeout() {
944
+ cancelPreviewTransitionLoaderTimeout();
945
+ this.previewTransitionLoaderTimeoutRunnable = () -> hidePreviewTransitionLoader("preview-transition-timeout");
946
+ this.mainHandler.postDelayed(this.previewTransitionLoaderTimeoutRunnable, PREVIEW_TRANSITION_LOADER_TIMEOUT_MS);
947
+ }
948
+
949
+ private void cancelPreviewTransitionLoaderTimeout() {
950
+ if (this.previewTransitionLoaderTimeoutRunnable != null) {
951
+ this.mainHandler.removeCallbacks(this.previewTransitionLoaderTimeoutRunnable);
952
+ this.previewTransitionLoaderTimeoutRunnable = null;
953
+ }
954
+ }
955
+
836
956
  private void scheduleSplashscreenTimeout() {
837
957
  if (this.autoSplashscreenTimeout == null || this.autoSplashscreenTimeout <= 0) {
838
958
  return;
@@ -1329,9 +1449,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
1329
1449
  );
1330
1450
  }
1331
1451
  } else {
1332
- final boolean enabled = configuredMode != null
1333
- ? "true".equals(configuredMode)
1334
- : Boolean.TRUE.equals(this.getConfig().getBoolean("autoUpdate", true));
1452
+ final boolean enabled =
1453
+ configuredMode != null
1454
+ ? "true".equals(configuredMode)
1455
+ : Boolean.TRUE.equals(this.getConfig().getBoolean("autoUpdate", true));
1335
1456
  this.autoUpdateMode = enabled
1336
1457
  ? autoUpdateModeForLegacyDirectUpdateMode(this.resolveLegacyDirectUpdateModeFromConfig())
1337
1458
  : AUTO_UPDATE_MODE_OFF;
@@ -1506,6 +1627,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1506
1627
  void scheduleDirectUpdateFinish(final BundleInfo latest) {
1507
1628
  startNewThread(() -> {
1508
1629
  try {
1630
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
1631
+ logger.info("Skipping direct update install while preview session state is active");
1632
+ this.implementation.directUpdate = false;
1633
+ this.clearBackgroundDownloadState();
1634
+ return;
1635
+ }
1509
1636
  Activity currentActivity = this.getActivity();
1510
1637
  if (currentActivity != null) {
1511
1638
  this.implementation.activity = currentActivity;
@@ -1520,14 +1647,52 @@ public class CapacitorUpdaterPlugin extends Plugin {
1520
1647
  }
1521
1648
 
1522
1649
  private void directUpdateFinish(final BundleInfo latest) {
1650
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
1651
+ logger.info("Skipping direct update finish while preview session state is active");
1652
+ this.implementation.directUpdate = false;
1653
+ this.clearBackgroundDownloadState();
1654
+ return;
1655
+ }
1523
1656
  if ("onLaunch".equals(this.directUpdateMode)) {
1524
1657
  this.onLaunchDirectUpdateUsed = true;
1525
1658
  this.implementation.directUpdate = false;
1526
1659
  }
1527
- if (CapacitorUpdaterPlugin.this.implementation.set(latest) && CapacitorUpdaterPlugin.this._reload()) {
1660
+ if (this.applyDownloadedBundleForDirectUpdate(latest)) {
1661
+ this.implementation.setNextBundle(null);
1528
1662
  this.notifyBundleSet(latest);
1529
1663
  sendReadyToJs(latest, "update installed", true);
1664
+ } else {
1665
+ this.implementation.setNextBundle(latest.getId());
1666
+ final JSObject ret = new JSObject();
1667
+ ret.put("bundle", InternalUtils.mapToJSObject(latest.toJSONMap()));
1668
+ this.notifyListeners("updateAvailable", ret);
1669
+ sendReadyToJs(
1670
+ this.implementation.getCurrentBundle(),
1671
+ "Direct update reload failed, update will install next background",
1672
+ false
1673
+ );
1674
+ }
1675
+ }
1676
+
1677
+ private boolean applyDownloadedBundleForDirectUpdate(final BundleInfo latest) {
1678
+ final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
1679
+ final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
1680
+
1681
+ if (!this.implementation.stagePendingReload(latest)) {
1682
+ this.implementation.restoreResetState(previousState);
1683
+ logger.error("Direct update failed to stage downloaded bundle: " + latest.toString());
1684
+ return false;
1530
1685
  }
1686
+
1687
+ if (this._reload()) {
1688
+ this.implementation.finalizePendingReload(latest, previousBundleName);
1689
+ return true;
1690
+ }
1691
+
1692
+ this.implementation.restoreResetState(previousState);
1693
+ this.restoreLiveBundleStateAfterFailedReload();
1694
+ logger.error("Direct update reload failed after staging bundle: " + latest.toString());
1695
+ return false;
1531
1696
  }
1532
1697
 
1533
1698
  private void cleanupObsoleteVersions() {
@@ -2135,6 +2300,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
2135
2300
  return this.semaphoreWait(phase, waitTimeMs);
2136
2301
  }
2137
2302
 
2303
+ protected boolean reloadWithoutWaitingForAppReady() {
2304
+ this.applyCurrentBundleToBridge();
2305
+
2306
+ final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
2307
+ this.checkAppReady(waitTimeMs);
2308
+ this.notifyListeners("appReloaded", new JSObject());
2309
+ return true;
2310
+ }
2311
+
2138
2312
  @PluginMethod
2139
2313
  public void reload(final PluginCall call) {
2140
2314
  startNewThread(() -> {
@@ -2142,7 +2316,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2142
2316
  final BundleInfo current = this.implementation.getCurrentBundle();
2143
2317
  final BundleInfo next = this.implementation.getNextBundle();
2144
2318
 
2145
- if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
2319
+ if (!this.isPreviewSessionStateActive() && next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
2146
2320
  final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
2147
2321
  final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
2148
2322
  logger.info("Applying pending bundle before reload: " + next.getVersionName());
@@ -2222,6 +2396,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
2222
2396
  if (!this.implementation.set(id)) {
2223
2397
  logger.info("No such bundle " + id);
2224
2398
  call.reject("Update failed, id " + id + " does not exist.");
2399
+ } else if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
2400
+ logger.info("Preview session set active bundle " + id + " without waiting for preview app readiness");
2401
+ this.reloadWithoutWaitingForAppReady();
2402
+ this.notifyBundleSet(this.implementation.getBundleInfo(id));
2403
+ this.showPreviewSessionNoticeIfNeeded();
2404
+ call.resolve();
2225
2405
  } else if (!this._reload()) {
2226
2406
  logger.error("Reload failed after setting bundle " + id);
2227
2407
  call.reject("Reload failed after setting bundle " + id);
@@ -2241,6 +2421,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2241
2421
  @PluginMethod
2242
2422
  public void startPreviewSession(final PluginCall call) {
2243
2423
  if (!Boolean.TRUE.equals(this.allowPreview)) {
2424
+ this.hidePreviewTransitionLoader("preview-session-not-allowed");
2244
2425
  logger.error("startPreviewSession not allowed set allowPreview in your config to true to enable it");
2245
2426
  call.reject("startPreviewSession not allowed");
2246
2427
  return;
@@ -2249,6 +2430,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2249
2430
  final String rawPayloadUrl = call.getString("payloadUrl");
2250
2431
  final String previewPayloadUrl = this.normalizePreviewPayloadUrl(rawPayloadUrl);
2251
2432
  if (this.hasPreviewPayloadUrl(rawPayloadUrl) && previewPayloadUrl == null) {
2433
+ this.hidePreviewTransitionLoader("preview-session-invalid-payload");
2252
2434
  logger.error("startPreviewSession called with invalid payloadUrl");
2253
2435
  call.reject("Invalid preview payloadUrl");
2254
2436
  return;
@@ -2258,6 +2440,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2258
2440
  if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
2259
2441
  final BundleInfo current = this.implementation.getCurrentBundle();
2260
2442
  if (!this.implementation.setPreviewFallbackBundle(current.getId())) {
2443
+ this.hidePreviewTransitionLoader("preview-session-fallback-failed");
2261
2444
  logger.error("Could not save current bundle as preview fallback");
2262
2445
  call.reject("Could not save current bundle as preview fallback");
2263
2446
  return;
@@ -2302,17 +2485,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
2302
2485
  this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
2303
2486
  }
2304
2487
 
2488
+ this.hidePreviewTransitionLoader("preview-session-started");
2305
2489
  this.previewSessionEnabled = true;
2306
2490
  this.previewSessionAlertPending = true;
2307
2491
  this.implementation.previewSession = true;
2308
2492
  this.shakeMenuEnabled = true;
2309
- this.shakeChannelSelectorEnabled = false;
2310
2493
  this.editor.putBoolean(PREVIEW_SESSION_PREF_KEY, true);
2311
2494
  this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2312
2495
  this.editor.apply();
2313
2496
  this.ensureShakeMenuStarted();
2314
2497
  call.resolve();
2315
2498
  } catch (final Exception e) {
2499
+ this.hidePreviewTransitionLoader("preview-session-failed");
2316
2500
  logger.error("Could not start preview session " + e.getMessage());
2317
2501
  call.reject("Could not start preview session", e);
2318
2502
  }
@@ -2322,8 +2506,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
2322
2506
  public boolean leavePreviewSessionFromShakeMenu() {
2323
2507
  final BundleInfo previewBundle = this.implementation.getCurrentBundle();
2324
2508
 
2509
+ this.showPreviewTransitionLoader("leave-preview-session");
2325
2510
  final boolean didReset = this.resetToPreviewFallbackBundle();
2326
2511
  if (!didReset) {
2512
+ this.hidePreviewTransitionLoader("leave-preview-session-failed");
2327
2513
  return false;
2328
2514
  }
2329
2515
 
@@ -2334,6 +2520,47 @@ public class CapacitorUpdaterPlugin extends Plugin {
2334
2520
  return true;
2335
2521
  }
2336
2522
 
2523
+ private boolean leavePreviewSessionForIncomingPreviewLink() {
2524
+ this.showPreviewTransitionLoader("incoming-preview-deeplink");
2525
+ final BundleInfo previewBundle = this.implementation.getCurrentBundle();
2526
+ final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2527
+ boolean didReload = false;
2528
+
2529
+ try {
2530
+ if (previewFallbackBundle == null || previewFallbackBundle.isErrorStatus()) {
2531
+ logger.error("No preview fallback bundle available");
2532
+ return false;
2533
+ }
2534
+ if (!this.implementation.canSet(previewFallbackBundle)) {
2535
+ logger.error("Preview fallback bundle is not installable");
2536
+ return false;
2537
+ }
2538
+
2539
+ final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
2540
+ if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
2541
+ logger.error("Could not stage preview fallback bundle");
2542
+ return false;
2543
+ }
2544
+
2545
+ if (!this._reload()) {
2546
+ this.implementation.restoreResetState(previousState);
2547
+ this.restoreLiveBundleStateAfterFailedReload();
2548
+ return false;
2549
+ }
2550
+ didReload = true;
2551
+
2552
+ this.endPreviewSession(true);
2553
+ final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
2554
+ this.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle, restoredNextBundle);
2555
+ return true;
2556
+ } finally {
2557
+ this.clearIncomingPreviewTransition();
2558
+ if (!didReload) {
2559
+ this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
2560
+ }
2561
+ }
2562
+ }
2563
+
2337
2564
  private void leavePreviewSessionForLaunchIntentIfNeeded() {
2338
2565
  final Intent intent = getActivity() == null ? null : getActivity().getIntent();
2339
2566
  if (
@@ -2348,14 +2575,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
2348
2575
  }
2349
2576
 
2350
2577
  this.isLeavingPreviewForIncomingLink = true;
2578
+ this.showPreviewTransitionLoader("preview-launch-deeplink");
2351
2579
  logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load");
2352
2580
  if (!this.leavePreviewSessionWithoutReload()) {
2353
2581
  logger.error("Could not leave preview session before initial preview deeplink routing");
2354
2582
  this.isLeavingPreviewForIncomingLink = false;
2583
+ this.hidePreviewTransitionLoader("preview-launch-deeplink-failed");
2355
2584
  }
2356
2585
  }
2357
2586
 
2358
2587
  private boolean leavePreviewSessionWithoutReload() {
2588
+ return this.leavePreviewSessionWithoutReload(false);
2589
+ }
2590
+
2591
+ private boolean leavePreviewSessionWithoutReload(final boolean keepPreviewGuard) {
2359
2592
  final BundleInfo previewBundle = this.implementation.getCurrentBundle();
2360
2593
  final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2361
2594
  if (previewFallbackBundle == null || previewFallbackBundle.isErrorStatus()) {
@@ -2371,7 +2604,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2371
2604
  return false;
2372
2605
  }
2373
2606
 
2374
- this.endPreviewSession();
2607
+ this.endPreviewSession(keepPreviewGuard);
2375
2608
  final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
2376
2609
  this.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle, restoredNextBundle);
2377
2610
  return true;
@@ -2396,12 +2629,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
2396
2629
  }
2397
2630
 
2398
2631
  public boolean reloadPreviewSessionFromShakeMenu() {
2632
+ this.showPreviewTransitionLoader("reload-preview-session");
2633
+ final boolean didReload;
2399
2634
  final String payloadUrl = this.storedPreviewPayloadUrl();
2400
2635
  if (payloadUrl != null) {
2401
- return this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
2636
+ didReload = this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
2637
+ } else {
2638
+ didReload = this.reloadWithoutWaitingForAppReady();
2402
2639
  }
2403
2640
 
2404
- return this._reload();
2641
+ if (!didReload) {
2642
+ this.hidePreviewTransitionLoader("reload-preview-session-failed");
2643
+ }
2644
+ return didReload;
2405
2645
  }
2406
2646
 
2407
2647
  public boolean hasActivePreviewSession() {
@@ -2433,6 +2673,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
2433
2673
  }
2434
2674
 
2435
2675
  private void endPreviewSession() {
2676
+ this.endPreviewSession(false);
2677
+ }
2678
+
2679
+ private void endPreviewSession(final boolean keepPreviewGuard) {
2436
2680
  final boolean previousShakeMenuEnabled = this.prefs.getBoolean(
2437
2681
  PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY,
2438
2682
  this.getConfig().getBoolean("shakeMenu", false)
@@ -2447,10 +2691,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
2447
2691
 
2448
2692
  this.previewSessionEnabled = false;
2449
2693
  this.previewSessionAlertPending = false;
2450
- this.isLeavingPreviewForIncomingLink = false;
2451
- this.implementation.previewSession = false;
2694
+ if (keepPreviewGuard) {
2695
+ this.implementation.previewSession = true;
2696
+ } else {
2697
+ this.clearIncomingPreviewTransition();
2698
+ }
2452
2699
  this.shakeMenuEnabled = previousShakeMenuEnabled;
2453
2700
  this.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled;
2701
+ this.syncShakeMenuLifecycle();
2454
2702
  this.implementation.setPreviewFallbackBundle(null);
2455
2703
  this.clearPreviewSessionPreferences();
2456
2704
  logger.info("Preview session ended");
@@ -2459,9 +2707,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2459
2707
  private void clearPreviewSessionBecauseDisabled() {
2460
2708
  logger.info("Preview session disabled by config; restoring preview fallback");
2461
2709
  final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
2462
- final BundleInfo bundleToRestore = fallback == null || fallback.isErrorStatus()
2463
- ? this.implementation.getBundleInfo(BundleInfo.ID_BUILTIN)
2464
- : fallback;
2710
+ final BundleInfo bundleToRestore =
2711
+ fallback == null || fallback.isErrorStatus() ? this.implementation.getBundleInfo(BundleInfo.ID_BUILTIN) : fallback;
2465
2712
 
2466
2713
  if (this.implementation.canSet(bundleToRestore)) {
2467
2714
  this.implementation.stagePreviewFallbackReload(bundleToRestore);
@@ -2476,8 +2723,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
2476
2723
  this.previewSessionAlertPending = false;
2477
2724
  this.isLeavingPreviewForIncomingLink = false;
2478
2725
  this.implementation.previewSession = false;
2726
+ this.hidePreviewTransitionLoader("preview-session-disabled");
2479
2727
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2480
2728
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
2729
+ this.syncShakeMenuLifecycle();
2481
2730
  this.clearPreviewSessionPreferences();
2482
2731
  }
2483
2732
 
@@ -2670,7 +2919,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2670
2919
 
2671
2920
  if (version.equals(this.implementation.getCurrentBundle().getVersionName())) {
2672
2921
  logger.info("Preview payload unchanged, reloading current bundle");
2673
- return this._reload();
2922
+ return this.reloadWithoutWaitingForAppReady();
2674
2923
  }
2675
2924
 
2676
2925
  final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
@@ -2682,7 +2931,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2682
2931
  }
2683
2932
 
2684
2933
  this.notifyBundleSet(next);
2685
- return this._reload();
2934
+ return this.reloadWithoutWaitingForAppReady();
2686
2935
  } catch (final Exception err) {
2687
2936
  logger.error("Could not refresh preview session: " + err.getMessage());
2688
2937
  return false;
@@ -2700,6 +2949,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2700
2949
  this.implementation.previewSession = false;
2701
2950
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2702
2951
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
2952
+ this.syncShakeMenuLifecycle();
2703
2953
  this.restorePreviewPreviousAppId();
2704
2954
  this.restorePreviewPreviousDefaultChannel();
2705
2955
  this.implementation.setPreviewFallbackBundle(null);
@@ -2730,6 +2980,24 @@ public class CapacitorUpdaterPlugin extends Plugin {
2730
2980
  }
2731
2981
  }
2732
2982
 
2983
+ private void syncShakeMenuLifecycle() {
2984
+ if (this.shouldListenForShake()) {
2985
+ this.ensureShakeMenuStarted();
2986
+ } else if (shakeMenu != null) {
2987
+ try {
2988
+ shakeMenu.stop();
2989
+ shakeMenu = null;
2990
+ logger.info("Shake menu stopped");
2991
+ } catch (Exception e) {
2992
+ logger.error("Failed to stop shake menu: " + e.getMessage());
2993
+ }
2994
+ }
2995
+ }
2996
+
2997
+ private boolean shouldListenForShake() {
2998
+ return Boolean.TRUE.equals(this.shakeMenuEnabled) || Boolean.TRUE.equals(this.shakeChannelSelectorEnabled);
2999
+ }
3000
+
2733
3001
  private void showPreviewSessionNoticeIfNeeded() {
2734
3002
  if (!Boolean.TRUE.equals(this.previewSessionEnabled) || !Boolean.TRUE.equals(this.previewSessionAlertPending)) {
2735
3003
  return;
@@ -2738,33 +3006,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
2738
3006
  this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false);
2739
3007
  this.editor.apply();
2740
3008
 
2741
- new Handler(Looper.getMainLooper()).postDelayed(
2742
- () -> {
2743
- try {
2744
- if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
2745
- return;
2746
- }
2747
- if (getActivity() == null || getActivity().isFinishing()) {
2748
- this.previewSessionAlertPending = true;
2749
- this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2750
- this.editor.apply();
2751
- return;
2752
- }
2753
-
2754
- new AlertDialog.Builder(getActivity())
2755
- .setTitle("Preview started")
2756
- .setMessage("Shake your device anytime to reload or leave the test app.")
2757
- .setPositiveButton("Got it", (dialog, which) -> dialog.dismiss())
2758
- .show();
2759
- } catch (final Exception e) {
3009
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
3010
+ try {
3011
+ if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
3012
+ return;
3013
+ }
3014
+ if (getActivity() == null || getActivity().isFinishing()) {
2760
3015
  this.previewSessionAlertPending = true;
2761
3016
  this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2762
3017
  this.editor.apply();
2763
- logger.warn("Could not show preview session notice: " + e.getMessage());
3018
+ return;
2764
3019
  }
2765
- },
2766
- 600
2767
- );
3020
+
3021
+ new AlertDialog.Builder(getActivity())
3022
+ .setTitle("Preview started")
3023
+ .setMessage("Shake your device anytime to reload or leave the test app.")
3024
+ .setPositiveButton("Got it", (dialog, which) -> dialog.dismiss())
3025
+ .show();
3026
+ } catch (final Exception e) {
3027
+ this.previewSessionAlertPending = true;
3028
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
3029
+ this.editor.apply();
3030
+ logger.warn("Could not show preview session notice: " + e.getMessage());
3031
+ }
3032
+ }, 600);
2768
3033
  }
2769
3034
 
2770
3035
  @PluginMethod
@@ -2970,6 +3235,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
2970
3235
  logger.error("Error no url or wrong format");
2971
3236
  return "unavailable";
2972
3237
  }
3238
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
3239
+ return "preview_session";
3240
+ }
2973
3241
  if (this.isDownloadStuckOrTimedOut()) {
2974
3242
  logger.info("Download already in progress, skipping duplicate download request");
2975
3243
  return "already_running";
@@ -3138,7 +3406,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
3138
3406
  @Override
3139
3407
  public void run() {
3140
3408
  try {
3409
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
3410
+ return;
3411
+ }
3141
3412
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
3413
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
3414
+ return;
3415
+ }
3142
3416
  JSObject jsRes = InternalUtils.mapToJSObject(res);
3143
3417
  if (jsRes.has("error") || jsRes.has("kind")) {
3144
3418
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
@@ -3201,6 +3475,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
3201
3475
  logger.info("semaphoreReady countDown");
3202
3476
  this.semaphoreDown();
3203
3477
  logger.info("semaphoreReady countDown done");
3478
+ this.clearIncomingPreviewTransition();
3479
+ this.hidePreviewTransitionLoader("notify-app-ready");
3204
3480
  final JSObject ret = new JSObject();
3205
3481
  ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
3206
3482
  call.resolve(ret);
@@ -3248,6 +3524,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
3248
3524
  }
3249
3525
 
3250
3526
  private Boolean _isAutoUpdateEnabled() {
3527
+ if (this.isPreviewSessionStateActive()) {
3528
+ return false;
3529
+ }
3251
3530
  final CapConfig config = CapConfig.loadDefault(this.getActivity());
3252
3531
  String serverUrl = config.getServerUrl();
3253
3532
  if (serverUrl != null && !serverUrl.isEmpty()) {
@@ -3446,6 +3725,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
3446
3725
  logger.info("endBackGroundTaskWithNotif " + msg);
3447
3726
  }
3448
3727
 
3728
+ private void clearBackgroundDownloadState() {
3729
+ this.backgroundDownloadTask = null;
3730
+ this.downloadStartTimeMs = 0;
3731
+ }
3732
+
3449
3733
  private boolean isDownloadStuckOrTimedOut() {
3450
3734
  if (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive()) {
3451
3735
  return false;
@@ -3472,6 +3756,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
3472
3756
  }
3473
3757
 
3474
3758
  private Thread backgroundDownload() {
3759
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
3760
+ return null;
3761
+ }
3475
3762
  final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
3476
3763
  final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
3477
3764
  final String messageUpdate = initialDirectUpdateAllowed
@@ -3482,9 +3769,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
3482
3769
  Thread newTask = startNewThread(() -> {
3483
3770
  // Wait for cleanup to complete before starting download
3484
3771
  waitForCleanupIfNeeded();
3772
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
3773
+ CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
3774
+ return;
3775
+ }
3485
3776
  logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
3486
3777
  try {
3487
3778
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
3779
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
3780
+ CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
3781
+ return;
3782
+ }
3488
3783
  JSObject jsRes = InternalUtils.mapToJSObject(res);
3489
3784
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
3490
3785
 
@@ -3709,6 +4004,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
3709
4004
  : initialDirectUpdateAllowed;
3710
4005
  startNewThread(() -> {
3711
4006
  try {
4007
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
4008
+ CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
4009
+ return;
4010
+ }
3712
4011
  logger.info(
3713
4012
  "New bundle: " +
3714
4013
  latestVersionName +
@@ -3795,6 +4094,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
3795
4094
 
3796
4095
  private void installNext() {
3797
4096
  try {
4097
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
4098
+ return;
4099
+ }
3798
4100
  String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
3799
4101
  ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(delayUpdatePreferences);
3800
4102
  if (!delayConditionList.isEmpty()) {
@@ -3830,6 +4132,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
3830
4132
  logger.info("Built-in bundle is active. We skip the check for notifyAppReady.");
3831
4133
  return;
3832
4134
  }
4135
+ if (this.isPreviewSessionStateActive()) {
4136
+ logger.info("Preview session is active. We skip the check for notifyAppReady.");
4137
+ return;
4138
+ }
3833
4139
  logger.debug("Current bundle is: " + current);
3834
4140
 
3835
4141
  if (BundleStatus.SUCCESS != current.getStatus()) {
@@ -4019,15 +4325,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
4019
4325
  }
4020
4326
 
4021
4327
  this.isLeavingPreviewForIncomingLink = true;
4328
+ this.showPreviewTransitionLoader("incoming-preview-deeplink");
4022
4329
  if (getActivity() != null) {
4023
4330
  getActivity().setIntent(intent);
4024
4331
  }
4025
4332
  logger.info("Preview deeplink received while preview session is active; restoring fallback before routing");
4026
4333
  startNewThread(() -> {
4027
- final boolean didLeave = this.leavePreviewSessionFromShakeMenu();
4334
+ final boolean didLeave = this.leavePreviewSessionForIncomingPreviewLink();
4028
4335
  if (!didLeave) {
4029
4336
  logger.error("Could not leave preview session before routing incoming preview deeplink");
4030
4337
  this.isLeavingPreviewForIncomingLink = false;
4338
+ this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
4031
4339
  }
4032
4340
  });
4033
4341
  }
@@ -4048,13 +4356,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
4048
4356
  }
4049
4357
 
4050
4358
  // Initialize shake menu if enabled and activity is BridgeActivity
4051
- if (shakeMenuEnabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
4052
- try {
4053
- shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
4054
- logger.info("Shake menu initialized");
4055
- } catch (Exception e) {
4056
- logger.error("Failed to initialize shake menu: " + e.getMessage());
4057
- }
4359
+ if (this.shouldListenForShake()) {
4360
+ this.ensureShakeMenuStarted();
4058
4361
  }
4059
4362
  } catch (Exception e) {
4060
4363
  logger.error("Failed to run handleOnStart: " + e.getMessage());
@@ -4113,23 +4416,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
4113
4416
  this.shakeMenuEnabled = enabled;
4114
4417
  logger.info("Shake menu " + (enabled ? "enabled" : "disabled"));
4115
4418
 
4116
- // Manage shake menu instance based on enabled state
4117
- if (enabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
4118
- try {
4119
- shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
4120
- logger.info("Shake menu initialized");
4121
- } catch (Exception e) {
4122
- logger.error("Failed to initialize shake menu: " + e.getMessage());
4123
- }
4124
- } else if (!enabled && shakeMenu != null) {
4125
- try {
4126
- shakeMenu.stop();
4127
- shakeMenu = null;
4128
- logger.info("Shake menu stopped");
4129
- } catch (Exception e) {
4130
- logger.error("Failed to stop shake menu: " + e.getMessage());
4131
- }
4132
- }
4419
+ this.syncShakeMenuLifecycle();
4133
4420
 
4134
4421
  call.resolve();
4135
4422
  }
@@ -4157,6 +4444,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
4157
4444
 
4158
4445
  this.shakeChannelSelectorEnabled = enabled;
4159
4446
  logger.info("Shake channel selector " + (enabled ? "enabled" : "disabled"));
4447
+ this.syncShakeMenuLifecycle();
4160
4448
  call.resolve();
4161
4449
  }
4162
4450