@capgo/capacitor-updater 8.47.4 → 8.47.6

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.
@@ -106,6 +106,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
106
106
  private static final String PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY = "CapacitorUpdater.previewPreviousDefaultChannelWasSet";
107
107
  private static final String PREVIEW_APP_ID_PREF_KEY = "CapacitorUpdater.previewAppId";
108
108
  private static final String PREVIEW_PAYLOAD_URL_PREF_KEY = "CapacitorUpdater.previewPayloadUrl";
109
+ private static final String PREVIEW_SESSION_ALERT_PENDING_PREF_KEY = "CapacitorUpdater.previewSessionAlertPending";
109
110
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
110
111
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
111
112
  private static final String LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY = "CapacitorUpdater.lastReportedAppExitTimestamp";
@@ -127,7 +128,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
127
128
  static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
128
129
  static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
129
130
 
130
- private final String pluginVersion = "8.47.4";
131
+ private final String pluginVersion = "8.47.6";
131
132
  private static final String DELAY_CONDITION_PREFERENCES = "";
132
133
 
133
134
  private SharedPreferences.Editor editor;
@@ -158,8 +159,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
158
159
  private volatile boolean onLaunchDirectUpdateUsed = false;
159
160
  Boolean shakeMenuEnabled = false;
160
161
  Boolean shakeChannelSelectorEnabled = false;
161
- Boolean previewSessionEnabled = false;
162
+ volatile Boolean previewSessionEnabled = false;
162
163
  private Boolean previewSessionAlertPending = false;
164
+ private volatile Boolean isLeavingPreviewForIncomingLink = false;
163
165
  private Boolean allowManualBundleError = false;
164
166
  private Boolean allowPreview = false;
165
167
  Boolean allowSetDefaultChannel = true;
@@ -168,6 +170,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
168
170
  return this.updateUrl;
169
171
  }
170
172
 
173
+ private boolean isPreviewSessionStateActive() {
174
+ return (
175
+ Boolean.TRUE.equals(this.previewSessionEnabled) ||
176
+ Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink) ||
177
+ (this.implementation != null && this.implementation.previewSession)
178
+ );
179
+ }
180
+
181
+ private boolean shouldBlockAutoUpdateForPreviewSession() {
182
+ if (!this.isPreviewSessionStateActive()) {
183
+ return false;
184
+ }
185
+
186
+ logger.info("Preview session is active. Skipping normal auto-update work.");
187
+ return true;
188
+ }
189
+
190
+ private void clearIncomingPreviewTransition() {
191
+ this.isLeavingPreviewForIncomingLink = false;
192
+ if (!Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation != null) {
193
+ this.implementation.previewSession = false;
194
+ }
195
+ }
196
+
171
197
  // Used for activity-based foreground/background detection on Android < 14
172
198
  private Boolean isPreviousMainActivity = true;
173
199
 
@@ -502,6 +528,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
502
528
  }
503
529
  this.implementation.previewSession = Boolean.TRUE.equals(this.previewSessionEnabled);
504
530
  if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
531
+ this.previewSessionAlertPending = this.prefs.contains(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY)
532
+ ? this.prefs.getBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false)
533
+ : true;
505
534
  final String previewAppId = this.prefs.getString(PREVIEW_APP_ID_PREF_KEY, "");
506
535
  if (previewAppId != null && !previewAppId.isEmpty()) {
507
536
  this.setActiveAppId(previewAppId);
@@ -520,6 +549,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
520
549
  if (nativeBuildVersionChanged) {
521
550
  this.clearPreviewSessionForNativeBuildChange();
522
551
  }
552
+ this.leavePreviewSessionForLaunchIntentIfNeeded();
523
553
  this.reportPreviousAppExitReasons();
524
554
  this.reportPreviousWebViewRenderProcessGone();
525
555
  this.installWebViewStatsReporter();
@@ -534,6 +564,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
534
564
  this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
535
565
 
536
566
  this.checkForUpdateAfterDelay();
567
+ this.showPreviewSessionNoticeIfNeeded();
537
568
 
538
569
  // On Android 14+ (API 34+), topActivity in RecentTaskInfo returns null due to
539
570
  // security restrictions (StrandHogg task hijacking mitigations). Use ProcessLifecycleOwner
@@ -1322,9 +1353,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
1322
1353
  );
1323
1354
  }
1324
1355
  } else {
1325
- final boolean enabled = configuredMode != null
1326
- ? "true".equals(configuredMode)
1327
- : Boolean.TRUE.equals(this.getConfig().getBoolean("autoUpdate", true));
1356
+ final boolean enabled =
1357
+ configuredMode != null
1358
+ ? "true".equals(configuredMode)
1359
+ : Boolean.TRUE.equals(this.getConfig().getBoolean("autoUpdate", true));
1328
1360
  this.autoUpdateMode = enabled
1329
1361
  ? autoUpdateModeForLegacyDirectUpdateMode(this.resolveLegacyDirectUpdateModeFromConfig())
1330
1362
  : AUTO_UPDATE_MODE_OFF;
@@ -1499,6 +1531,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1499
1531
  void scheduleDirectUpdateFinish(final BundleInfo latest) {
1500
1532
  startNewThread(() -> {
1501
1533
  try {
1534
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
1535
+ logger.info("Skipping direct update install while preview session state is active");
1536
+ this.implementation.directUpdate = false;
1537
+ this.clearBackgroundDownloadState();
1538
+ return;
1539
+ }
1502
1540
  Activity currentActivity = this.getActivity();
1503
1541
  if (currentActivity != null) {
1504
1542
  this.implementation.activity = currentActivity;
@@ -1513,16 +1551,54 @@ public class CapacitorUpdaterPlugin extends Plugin {
1513
1551
  }
1514
1552
 
1515
1553
  private void directUpdateFinish(final BundleInfo latest) {
1554
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
1555
+ logger.info("Skipping direct update finish while preview session state is active");
1556
+ this.implementation.directUpdate = false;
1557
+ this.clearBackgroundDownloadState();
1558
+ return;
1559
+ }
1516
1560
  if ("onLaunch".equals(this.directUpdateMode)) {
1517
1561
  this.onLaunchDirectUpdateUsed = true;
1518
1562
  this.implementation.directUpdate = false;
1519
1563
  }
1520
- if (CapacitorUpdaterPlugin.this.implementation.set(latest) && CapacitorUpdaterPlugin.this._reload()) {
1564
+ if (this.applyDownloadedBundleForDirectUpdate(latest)) {
1565
+ this.implementation.setNextBundle(null);
1521
1566
  this.notifyBundleSet(latest);
1522
1567
  sendReadyToJs(latest, "update installed", true);
1568
+ } else {
1569
+ this.implementation.setNextBundle(latest.getId());
1570
+ final JSObject ret = new JSObject();
1571
+ ret.put("bundle", InternalUtils.mapToJSObject(latest.toJSONMap()));
1572
+ this.notifyListeners("updateAvailable", ret);
1573
+ sendReadyToJs(
1574
+ this.implementation.getCurrentBundle(),
1575
+ "Direct update reload failed, update will install next background",
1576
+ false
1577
+ );
1523
1578
  }
1524
1579
  }
1525
1580
 
1581
+ private boolean applyDownloadedBundleForDirectUpdate(final BundleInfo latest) {
1582
+ final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
1583
+ final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
1584
+
1585
+ if (!this.implementation.stagePendingReload(latest)) {
1586
+ this.implementation.restoreResetState(previousState);
1587
+ logger.error("Direct update failed to stage downloaded bundle: " + latest.toString());
1588
+ return false;
1589
+ }
1590
+
1591
+ if (this._reload()) {
1592
+ this.implementation.finalizePendingReload(latest, previousBundleName);
1593
+ return true;
1594
+ }
1595
+
1596
+ this.implementation.restoreResetState(previousState);
1597
+ this.restoreLiveBundleStateAfterFailedReload();
1598
+ logger.error("Direct update reload failed after staging bundle: " + latest.toString());
1599
+ return false;
1600
+ }
1601
+
1526
1602
  private void cleanupObsoleteVersions() {
1527
1603
  cleanupThread = startNewThread(() -> {
1528
1604
  synchronized (cleanupLock) {
@@ -2128,6 +2204,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
2128
2204
  return this.semaphoreWait(phase, waitTimeMs);
2129
2205
  }
2130
2206
 
2207
+ protected boolean reloadWithoutWaitingForAppReady() {
2208
+ this.applyCurrentBundleToBridge();
2209
+
2210
+ final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
2211
+ this.checkAppReady(waitTimeMs);
2212
+ this.notifyListeners("appReloaded", new JSObject());
2213
+ return true;
2214
+ }
2215
+
2131
2216
  @PluginMethod
2132
2217
  public void reload(final PluginCall call) {
2133
2218
  startNewThread(() -> {
@@ -2135,7 +2220,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2135
2220
  final BundleInfo current = this.implementation.getCurrentBundle();
2136
2221
  final BundleInfo next = this.implementation.getNextBundle();
2137
2222
 
2138
- if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
2223
+ if (!this.isPreviewSessionStateActive() && next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
2139
2224
  final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
2140
2225
  final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
2141
2226
  logger.info("Applying pending bundle before reload: " + next.getVersionName());
@@ -2215,6 +2300,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
2215
2300
  if (!this.implementation.set(id)) {
2216
2301
  logger.info("No such bundle " + id);
2217
2302
  call.reject("Update failed, id " + id + " does not exist.");
2303
+ } else if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
2304
+ logger.info("Preview session set active bundle " + id + " without waiting for preview app readiness");
2305
+ this.reloadWithoutWaitingForAppReady();
2306
+ this.notifyBundleSet(this.implementation.getBundleInfo(id));
2307
+ this.showPreviewSessionNoticeIfNeeded();
2308
+ call.resolve();
2218
2309
  } else if (!this._reload()) {
2219
2310
  logger.error("Reload failed after setting bundle " + id);
2220
2311
  call.reject("Reload failed after setting bundle " + id);
@@ -2301,6 +2392,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2301
2392
  this.shakeMenuEnabled = true;
2302
2393
  this.shakeChannelSelectorEnabled = false;
2303
2394
  this.editor.putBoolean(PREVIEW_SESSION_PREF_KEY, true);
2395
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2304
2396
  this.editor.apply();
2305
2397
  this.ensureShakeMenuStarted();
2306
2398
  call.resolve();
@@ -2322,6 +2414,97 @@ public class CapacitorUpdaterPlugin extends Plugin {
2322
2414
  final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2323
2415
  this.endPreviewSession();
2324
2416
  final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
2417
+ this.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle, restoredNextBundle);
2418
+ return true;
2419
+ }
2420
+
2421
+ private boolean leavePreviewSessionForIncomingPreviewLink() {
2422
+ final BundleInfo previewBundle = this.implementation.getCurrentBundle();
2423
+ final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2424
+
2425
+ try {
2426
+ if (previewFallbackBundle == null || previewFallbackBundle.isErrorStatus()) {
2427
+ logger.error("No preview fallback bundle available");
2428
+ return false;
2429
+ }
2430
+ if (!this.implementation.canSet(previewFallbackBundle)) {
2431
+ logger.error("Preview fallback bundle is not installable");
2432
+ return false;
2433
+ }
2434
+
2435
+ final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
2436
+ if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
2437
+ logger.error("Could not stage preview fallback bundle");
2438
+ return false;
2439
+ }
2440
+
2441
+ if (!this._reload()) {
2442
+ this.implementation.restoreResetState(previousState);
2443
+ this.restoreLiveBundleStateAfterFailedReload();
2444
+ return false;
2445
+ }
2446
+
2447
+ this.endPreviewSession(true);
2448
+ final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
2449
+ this.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle, restoredNextBundle);
2450
+ return true;
2451
+ } finally {
2452
+ this.clearIncomingPreviewTransition();
2453
+ }
2454
+ }
2455
+
2456
+ private void leavePreviewSessionForLaunchIntentIfNeeded() {
2457
+ final Intent intent = getActivity() == null ? null : getActivity().getIntent();
2458
+ if (
2459
+ intent == null ||
2460
+ !Intent.ACTION_VIEW.equals(intent.getAction()) ||
2461
+ intent.getData() == null ||
2462
+ !Boolean.TRUE.equals(this.previewSessionEnabled) ||
2463
+ !isPreviewDeepLink(intent.getData()) ||
2464
+ Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
2465
+ ) {
2466
+ return;
2467
+ }
2468
+
2469
+ this.isLeavingPreviewForIncomingLink = true;
2470
+ logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load");
2471
+ if (!this.leavePreviewSessionWithoutReload()) {
2472
+ logger.error("Could not leave preview session before initial preview deeplink routing");
2473
+ this.isLeavingPreviewForIncomingLink = false;
2474
+ }
2475
+ }
2476
+
2477
+ private boolean leavePreviewSessionWithoutReload() {
2478
+ return this.leavePreviewSessionWithoutReload(false);
2479
+ }
2480
+
2481
+ private boolean leavePreviewSessionWithoutReload(final boolean keepPreviewGuard) {
2482
+ final BundleInfo previewBundle = this.implementation.getCurrentBundle();
2483
+ final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2484
+ if (previewFallbackBundle == null || previewFallbackBundle.isErrorStatus()) {
2485
+ logger.error("No preview fallback bundle available");
2486
+ return false;
2487
+ }
2488
+ if (!this.implementation.canSet(previewFallbackBundle)) {
2489
+ logger.error("Preview fallback bundle is not installable");
2490
+ return false;
2491
+ }
2492
+ if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
2493
+ logger.error("Could not stage preview fallback bundle");
2494
+ return false;
2495
+ }
2496
+
2497
+ this.endPreviewSession(keepPreviewGuard);
2498
+ final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
2499
+ this.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle, restoredNextBundle);
2500
+ return true;
2501
+ }
2502
+
2503
+ private void deletePreviewBundleIfUnused(
2504
+ final BundleInfo previewBundle,
2505
+ final BundleInfo previewFallbackBundle,
2506
+ final BundleInfo restoredNextBundle
2507
+ ) {
2325
2508
  if (
2326
2509
  !previewBundle.isBuiltin() &&
2327
2510
  (previewFallbackBundle == null || !previewBundle.getId().equals(previewFallbackBundle.getId())) &&
@@ -2333,7 +2516,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
2333
2516
  logger.warn("Cannot delete preview bundle " + previewBundle.getId() + ": " + err.getMessage());
2334
2517
  }
2335
2518
  }
2336
- return true;
2337
2519
  }
2338
2520
 
2339
2521
  public boolean reloadPreviewSessionFromShakeMenu() {
@@ -2342,7 +2524,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2342
2524
  return this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
2343
2525
  }
2344
2526
 
2345
- return this._reload();
2527
+ return this.reloadWithoutWaitingForAppReady();
2346
2528
  }
2347
2529
 
2348
2530
  public boolean hasActivePreviewSession() {
@@ -2374,6 +2556,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
2374
2556
  }
2375
2557
 
2376
2558
  private void endPreviewSession() {
2559
+ this.endPreviewSession(false);
2560
+ }
2561
+
2562
+ private void endPreviewSession(final boolean keepPreviewGuard) {
2377
2563
  final boolean previousShakeMenuEnabled = this.prefs.getBoolean(
2378
2564
  PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY,
2379
2565
  this.getConfig().getBoolean("shakeMenu", false)
@@ -2388,7 +2574,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
2388
2574
 
2389
2575
  this.previewSessionEnabled = false;
2390
2576
  this.previewSessionAlertPending = false;
2391
- this.implementation.previewSession = false;
2577
+ if (keepPreviewGuard) {
2578
+ this.implementation.previewSession = true;
2579
+ } else {
2580
+ this.clearIncomingPreviewTransition();
2581
+ }
2392
2582
  this.shakeMenuEnabled = previousShakeMenuEnabled;
2393
2583
  this.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled;
2394
2584
  this.implementation.setPreviewFallbackBundle(null);
@@ -2399,9 +2589,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2399
2589
  private void clearPreviewSessionBecauseDisabled() {
2400
2590
  logger.info("Preview session disabled by config; restoring preview fallback");
2401
2591
  final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
2402
- final BundleInfo bundleToRestore = fallback == null || fallback.isErrorStatus()
2403
- ? this.implementation.getBundleInfo(BundleInfo.ID_BUILTIN)
2404
- : fallback;
2592
+ final BundleInfo bundleToRestore =
2593
+ fallback == null || fallback.isErrorStatus() ? this.implementation.getBundleInfo(BundleInfo.ID_BUILTIN) : fallback;
2405
2594
 
2406
2595
  if (this.implementation.canSet(bundleToRestore)) {
2407
2596
  this.implementation.stagePreviewFallbackReload(bundleToRestore);
@@ -2414,6 +2603,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2414
2603
  this.restorePreviewPreviousDefaultChannel();
2415
2604
  this.previewSessionEnabled = false;
2416
2605
  this.previewSessionAlertPending = false;
2606
+ this.isLeavingPreviewForIncomingLink = false;
2417
2607
  this.implementation.previewSession = false;
2418
2608
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2419
2609
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
@@ -2433,6 +2623,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2433
2623
  this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY);
2434
2624
  this.editor.remove(PREVIEW_APP_ID_PREF_KEY);
2435
2625
  this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
2626
+ this.editor.remove(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY);
2436
2627
  this.editor.apply();
2437
2628
  }
2438
2629
 
@@ -2523,6 +2714,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
2523
2714
  return this.normalizePreviewPayloadUrl(this.prefs.getString(PREVIEW_PAYLOAD_URL_PREF_KEY, null));
2524
2715
  }
2525
2716
 
2717
+ private String previewPathFromUri(final Uri uri) {
2718
+ if ("capgo".equals(uri.getScheme())) {
2719
+ final String host = uri.getHost();
2720
+ final String path = uri.getPath();
2721
+ return ("/" + (host == null ? "" : host) + (path == null ? "" : path)).replaceAll("/+", "/");
2722
+ }
2723
+
2724
+ return uri.getPath();
2725
+ }
2726
+
2727
+ private boolean isPreviewDeepLink(final Uri uri) {
2728
+ final String path = this.previewPathFromUri(uri);
2729
+ return "/preview/channel".equals(path) || "/preview/bundle".equals(path);
2730
+ }
2731
+
2526
2732
  private String readResponseBody(final InputStream stream) throws IOException {
2527
2733
  if (stream == null) {
2528
2734
  return "";
@@ -2593,7 +2799,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2593
2799
 
2594
2800
  if (version.equals(this.implementation.getCurrentBundle().getVersionName())) {
2595
2801
  logger.info("Preview payload unchanged, reloading current bundle");
2596
- return this._reload();
2802
+ return this.reloadWithoutWaitingForAppReady();
2597
2803
  }
2598
2804
 
2599
2805
  final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
@@ -2605,7 +2811,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2605
2811
  }
2606
2812
 
2607
2813
  this.notifyBundleSet(next);
2608
- return this._reload();
2814
+ return this.reloadWithoutWaitingForAppReady();
2609
2815
  } catch (final Exception err) {
2610
2816
  logger.error("Could not refresh preview session: " + err.getMessage());
2611
2817
  return false;
@@ -2619,6 +2825,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2619
2825
  logger.info("Native build changed; clearing preview session state");
2620
2826
  this.previewSessionEnabled = false;
2621
2827
  this.previewSessionAlertPending = false;
2828
+ this.isLeavingPreviewForIncomingLink = false;
2622
2829
  this.implementation.previewSession = false;
2623
2830
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2624
2831
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
@@ -2657,30 +2864,33 @@ public class CapacitorUpdaterPlugin extends Plugin {
2657
2864
  return;
2658
2865
  }
2659
2866
  this.previewSessionAlertPending = false;
2867
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false);
2868
+ this.editor.apply();
2660
2869
 
2661
- new Handler(Looper.getMainLooper()).postDelayed(
2662
- () -> {
2663
- try {
2664
- if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
2665
- return;
2666
- }
2667
- if (getActivity() == null || getActivity().isFinishing()) {
2668
- this.previewSessionAlertPending = true;
2669
- return;
2670
- }
2671
-
2672
- new AlertDialog.Builder(getActivity())
2673
- .setTitle("Preview started")
2674
- .setMessage("Shake your device anytime to reload or leave the test app.")
2675
- .setPositiveButton("Got it", (dialog, which) -> dialog.dismiss())
2676
- .show();
2677
- } catch (final Exception e) {
2870
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
2871
+ try {
2872
+ if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
2873
+ return;
2874
+ }
2875
+ if (getActivity() == null || getActivity().isFinishing()) {
2678
2876
  this.previewSessionAlertPending = true;
2679
- logger.warn("Could not show preview session notice: " + e.getMessage());
2877
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2878
+ this.editor.apply();
2879
+ return;
2680
2880
  }
2681
- },
2682
- 600
2683
- );
2881
+
2882
+ new AlertDialog.Builder(getActivity())
2883
+ .setTitle("Preview started")
2884
+ .setMessage("Shake your device anytime to reload or leave the test app.")
2885
+ .setPositiveButton("Got it", (dialog, which) -> dialog.dismiss())
2886
+ .show();
2887
+ } catch (final Exception e) {
2888
+ this.previewSessionAlertPending = true;
2889
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2890
+ this.editor.apply();
2891
+ logger.warn("Could not show preview session notice: " + e.getMessage());
2892
+ }
2893
+ }, 600);
2684
2894
  }
2685
2895
 
2686
2896
  @PluginMethod
@@ -2886,6 +3096,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
2886
3096
  logger.error("Error no url or wrong format");
2887
3097
  return "unavailable";
2888
3098
  }
3099
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
3100
+ return "preview_session";
3101
+ }
2889
3102
  if (this.isDownloadStuckOrTimedOut()) {
2890
3103
  logger.info("Download already in progress, skipping duplicate download request");
2891
3104
  return "already_running";
@@ -3054,7 +3267,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
3054
3267
  @Override
3055
3268
  public void run() {
3056
3269
  try {
3270
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
3271
+ return;
3272
+ }
3057
3273
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
3274
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
3275
+ return;
3276
+ }
3058
3277
  JSObject jsRes = InternalUtils.mapToJSObject(res);
3059
3278
  if (jsRes.has("error") || jsRes.has("kind")) {
3060
3279
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
@@ -3117,6 +3336,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
3117
3336
  logger.info("semaphoreReady countDown");
3118
3337
  this.semaphoreDown();
3119
3338
  logger.info("semaphoreReady countDown done");
3339
+ this.clearIncomingPreviewTransition();
3120
3340
  final JSObject ret = new JSObject();
3121
3341
  ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
3122
3342
  call.resolve(ret);
@@ -3164,6 +3384,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
3164
3384
  }
3165
3385
 
3166
3386
  private Boolean _isAutoUpdateEnabled() {
3387
+ if (this.isPreviewSessionStateActive()) {
3388
+ return false;
3389
+ }
3167
3390
  final CapConfig config = CapConfig.loadDefault(this.getActivity());
3168
3391
  String serverUrl = config.getServerUrl();
3169
3392
  if (serverUrl != null && !serverUrl.isEmpty()) {
@@ -3362,6 +3585,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
3362
3585
  logger.info("endBackGroundTaskWithNotif " + msg);
3363
3586
  }
3364
3587
 
3588
+ private void clearBackgroundDownloadState() {
3589
+ this.backgroundDownloadTask = null;
3590
+ this.downloadStartTimeMs = 0;
3591
+ }
3592
+
3365
3593
  private boolean isDownloadStuckOrTimedOut() {
3366
3594
  if (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive()) {
3367
3595
  return false;
@@ -3388,6 +3616,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
3388
3616
  }
3389
3617
 
3390
3618
  private Thread backgroundDownload() {
3619
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
3620
+ return null;
3621
+ }
3391
3622
  final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
3392
3623
  final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
3393
3624
  final String messageUpdate = initialDirectUpdateAllowed
@@ -3398,9 +3629,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
3398
3629
  Thread newTask = startNewThread(() -> {
3399
3630
  // Wait for cleanup to complete before starting download
3400
3631
  waitForCleanupIfNeeded();
3632
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
3633
+ CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
3634
+ return;
3635
+ }
3401
3636
  logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
3402
3637
  try {
3403
3638
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
3639
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
3640
+ CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
3641
+ return;
3642
+ }
3404
3643
  JSObject jsRes = InternalUtils.mapToJSObject(res);
3405
3644
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
3406
3645
 
@@ -3625,6 +3864,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
3625
3864
  : initialDirectUpdateAllowed;
3626
3865
  startNewThread(() -> {
3627
3866
  try {
3867
+ if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
3868
+ CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
3869
+ return;
3870
+ }
3628
3871
  logger.info(
3629
3872
  "New bundle: " +
3630
3873
  latestVersionName +
@@ -3711,6 +3954,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
3711
3954
 
3712
3955
  private void installNext() {
3713
3956
  try {
3957
+ if (this.shouldBlockAutoUpdateForPreviewSession()) {
3958
+ return;
3959
+ }
3714
3960
  String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
3715
3961
  ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(delayUpdatePreferences);
3716
3962
  if (!delayConditionList.isEmpty()) {
@@ -3746,6 +3992,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
3746
3992
  logger.info("Built-in bundle is active. We skip the check for notifyAppReady.");
3747
3993
  return;
3748
3994
  }
3995
+ if (this.isPreviewSessionStateActive()) {
3996
+ logger.info("Preview session is active. We skip the check for notifyAppReady.");
3997
+ return;
3998
+ }
3749
3999
  logger.debug("Current bundle is: " + current);
3750
4000
 
3751
4001
  if (BundleStatus.SUCCESS != current.getStatus()) {
@@ -3920,6 +4170,34 @@ public class CapacitorUpdaterPlugin extends Plugin {
3920
4170
  }
3921
4171
  }
3922
4172
 
4173
+ @Override
4174
+ protected void handleOnNewIntent(Intent intent) {
4175
+ super.handleOnNewIntent(intent);
4176
+ if (
4177
+ intent == null ||
4178
+ !Intent.ACTION_VIEW.equals(intent.getAction()) ||
4179
+ intent.getData() == null ||
4180
+ !Boolean.TRUE.equals(this.previewSessionEnabled) ||
4181
+ !isPreviewDeepLink(intent.getData()) ||
4182
+ Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
4183
+ ) {
4184
+ return;
4185
+ }
4186
+
4187
+ this.isLeavingPreviewForIncomingLink = true;
4188
+ if (getActivity() != null) {
4189
+ getActivity().setIntent(intent);
4190
+ }
4191
+ logger.info("Preview deeplink received while preview session is active; restoring fallback before routing");
4192
+ startNewThread(() -> {
4193
+ final boolean didLeave = this.leavePreviewSessionForIncomingPreviewLink();
4194
+ if (!didLeave) {
4195
+ logger.error("Could not leave preview session before routing incoming preview deeplink");
4196
+ this.isLeavingPreviewForIncomingLink = false;
4197
+ }
4198
+ });
4199
+ }
4200
+
3923
4201
  @Override
3924
4202
  public void handleOnStart() {
3925
4203
  try {