@capgo/capacitor-updater 8.51.0 → 8.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2857,7 +2857,7 @@ downloading: The bundle is being downloaded.
2857
2857
  success: The bundle has been downloaded and is ready to be **SET** as the next bundle.
2858
2858
  error: The bundle has failed to download.
2859
2859
 
2860
- <code>'success' | 'error' | 'pending' | 'downloading'</code>
2860
+ <code>'success' | 'error' | 'pending' | 'downloading' | 'deleted' | 'deleting'</code>
2861
2861
 
2862
2862
 
2863
2863
  ##### DelayUntilNext
@@ -94,12 +94,16 @@ public class BundleInfo {
94
94
  return BundleStatus.DELETED == this.status;
95
95
  }
96
96
 
97
+ public Boolean isDeleting() {
98
+ return BundleStatus.DELETING == this.status;
99
+ }
100
+
97
101
  public boolean isDownloaded() {
98
- return (!this.isBuiltin() && this.downloaded != null && !this.downloaded.isEmpty() && !this.isDeleted());
102
+ return (!this.isBuiltin() && this.downloaded != null && !this.downloaded.isEmpty() && !this.isDeleted() && !this.isDeleting());
99
103
  }
100
104
 
101
105
  public String getDownloaded() {
102
- return this.isBuiltin() ? DOWNLOADED_BUILTIN : (this.downloaded != null ? this.downloaded : "");
106
+ return this.isBuiltin() ? DOWNLOADED_BUILTIN : this.downloaded != null ? this.downloaded : "";
103
107
  }
104
108
 
105
109
  public BundleInfo setDownloaded(Date downloaded) {
@@ -107,7 +111,7 @@ public class BundleInfo {
107
111
  }
108
112
 
109
113
  public String getChecksum() {
110
- return this.isBuiltin() ? "" : (this.checksum != null ? this.checksum : "");
114
+ return this.isBuiltin() ? "" : this.checksum != null ? this.checksum : "";
111
115
  }
112
116
 
113
117
  public BundleInfo setChecksum(String checksum) {
@@ -14,6 +14,7 @@ public enum BundleStatus {
14
14
  ERROR("error"),
15
15
  PENDING("pending"),
16
16
  DELETED("deleted"),
17
+ DELETING("deleting"),
17
18
  DOWNLOADING("downloading");
18
19
 
19
20
  public final String label;
@@ -68,6 +68,7 @@ import java.util.Set;
68
68
  import java.util.TimeZone;
69
69
  import java.util.Timer;
70
70
  import java.util.TimerTask;
71
+ import java.util.concurrent.CountDownLatch;
71
72
  import java.util.concurrent.Phaser;
72
73
  import java.util.concurrent.Semaphore;
73
74
  import java.util.concurrent.TimeUnit;
@@ -142,7 +143,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
142
143
  static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
143
144
  static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
144
145
 
145
- private final String pluginVersion = "8.51.0";
146
+ private final String pluginVersion = "8.51.1";
146
147
  private static final String DELAY_CONDITION_PREFERENCES = "";
147
148
 
148
149
  private SharedPreferences.Editor editor;
@@ -215,6 +216,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
215
216
 
216
217
  private volatile Thread backgroundDownloadTask;
217
218
  private volatile Thread appReadyCheck;
219
+ // When true, sendReadyToJs should wait for notifyAppReady before hiding splash.
220
+ private volatile boolean pendingNotifyAppReadyWait = false;
221
+ private volatile int pendingNotifyAppReadyPhase = -1;
218
222
  private volatile long downloadStartTimeMs = 0;
219
223
  private static final long DOWNLOAD_TIMEOUT_MS = 600000; // 10 minute timeout
220
224
 
@@ -227,6 +231,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
227
231
 
228
232
  // Lock to ensure cleanup completes before downloads start
229
233
  private final Object cleanupLock = new Object();
234
+ private volatile CountDownLatch cleanupLatch = new CountDownLatch(0);
230
235
  private volatile boolean cleanupComplete = false;
231
236
  private volatile Thread cleanupThread = null;
232
237
 
@@ -871,11 +876,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
871
876
  this.reportPreviousAppExitReasons();
872
877
  this.reportPreviousWebViewRenderProcessGone();
873
878
  this.installWebViewStatsReporter();
874
- if (resetWhenUpdate) {
875
- this.cleanupObsoleteVersions();
876
- } else {
879
+ // Downloads (including shake-menu / CapgoUpdater entry points) wait on this gate.
880
+ this.implementation.downloadGate = this::waitForCleanupIfNeeded;
881
+ // Always run async cleanup: delete obsolete bundles on native update (when enabled)
882
+ // and sweep orphan folders every launch. Must not block app startup.
883
+ if (!resetWhenUpdate) {
877
884
  this.persistCurrentNativeBuildVersion();
878
885
  }
886
+ this.cleanupObsoleteVersions(resetWhenUpdate);
879
887
 
880
888
  // Check for 'kill' delay condition on app launch
881
889
  // This handles cases where the app was killed by the system (onDestroy is not reliable)
@@ -909,6 +917,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
909
917
  } else {
910
918
  logger.info("Using activity lifecycle callbacks for foreground/background detection (Android <14)");
911
919
  }
920
+
921
+ // Expect notifyAppReady before the first appReady/splash hide (same idea as iOS).
922
+ this.armPendingNotifyAppReadyWait();
912
923
  }
913
924
 
914
925
  private boolean semaphoreWait(final int phase, Number waitTime) {
@@ -951,6 +962,32 @@ public class CapacitorUpdaterPlugin extends Plugin {
951
962
  semaphoreReady.arriveAndDeregister();
952
963
  }
953
964
 
965
+ private void armPendingNotifyAppReadyWait() {
966
+ this.clearPendingNotifyAppReadyWait();
967
+ this.pendingNotifyAppReadyPhase = this.semaphoreUp();
968
+ this.pendingNotifyAppReadyWait = true;
969
+ }
970
+
971
+ private void clearPendingNotifyAppReadyWait() {
972
+ if (!this.pendingNotifyAppReadyWait) {
973
+ return;
974
+ }
975
+ final int phase = this.pendingNotifyAppReadyPhase;
976
+ this.pendingNotifyAppReadyWait = false;
977
+ this.pendingNotifyAppReadyPhase = -1;
978
+ this.cleanupTimedOutSemaphoreWait(phase);
979
+ }
980
+
981
+ private boolean consumePendingNotifyAppReadyWait(final int[] phaseOut) {
982
+ if (!this.pendingNotifyAppReadyWait) {
983
+ return false;
984
+ }
985
+ phaseOut[0] = this.pendingNotifyAppReadyPhase;
986
+ this.pendingNotifyAppReadyWait = false;
987
+ this.pendingNotifyAppReadyPhase = -1;
988
+ return true;
989
+ }
990
+
954
991
  protected long getMinimumPendingBundleAppReadyTimeoutMs() {
955
992
  return PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS;
956
993
  }
@@ -989,19 +1026,40 @@ public class CapacitorUpdaterPlugin extends Plugin {
989
1026
 
990
1027
  private void sendReadyToJs(final BundleInfo current, final String msg, final boolean isDirectUpdate) {
991
1028
  logger.info("sendReadyToJs: " + msg);
992
- final JSObject ret = new JSObject();
993
- ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
994
- ret.put("status", msg);
1029
+ final int[] pendingPhase = new int[] { -1 };
1030
+ final boolean shouldWait = this.consumePendingNotifyAppReadyWait(pendingPhase);
995
1031
 
996
- // No need to wait for semaphore anymore since _reload() has already waited
997
- this.notifyListeners("appReady", ret, true);
1032
+ final Runnable emitReady = () -> {
1033
+ final JSObject ret = new JSObject();
1034
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
1035
+ ret.put("status", msg);
998
1036
 
999
- // Auto hide splashscreen if enabled
1000
- // We show it on background when conditions are met, so we should hide it on foreground regardless of update outcome
1001
- if (this.autoSplashscreen) {
1002
- this.hideSplashscreen();
1037
+ this.notifyListeners("appReady", ret, true);
1038
+
1039
+ // Auto hide splashscreen if enabled
1040
+ // We show it on background when conditions are met, so we should hide it on foreground regardless of update outcome
1041
+ if (this.autoSplashscreen) {
1042
+ this.hideSplashscreen();
1043
+ }
1044
+ this.hidePreviewTransitionLoader("app-ready");
1045
+ };
1046
+
1047
+ if (!shouldWait) {
1048
+ emitReady.run();
1049
+ return;
1050
+ }
1051
+
1052
+ // Never block the UI thread waiting for notifyAppReady (JS needs it).
1053
+ if (Looper.myLooper() == Looper.getMainLooper()) {
1054
+ startNewThread(() -> {
1055
+ this.semaphoreWait(pendingPhase[0], this.appReadyTimeout);
1056
+ emitReady.run();
1057
+ });
1058
+ return;
1003
1059
  }
1004
- this.hidePreviewTransitionLoader("app-ready");
1060
+
1061
+ this.semaphoreWait(pendingPhase[0], this.appReadyTimeout);
1062
+ emitReady.run();
1005
1063
  }
1006
1064
 
1007
1065
  private void hideSplashscreen() {
@@ -2222,67 +2280,58 @@ public class CapacitorUpdaterPlugin extends Plugin {
2222
2280
  return false;
2223
2281
  }
2224
2282
 
2225
- private void cleanupObsoleteVersions() {
2283
+ private void cleanupObsoleteVersions(final boolean resetWhenUpdate) {
2284
+ // Latch created before start so waiters never race past an unstarted cleanup thread.
2285
+ final CountDownLatch latch = new CountDownLatch(1);
2286
+ this.cleanupComplete = false;
2287
+ this.cleanupLatch = latch;
2226
2288
  cleanupThread = startNewThread(() -> {
2227
- synchronized (cleanupLock) {
2228
- try {
2229
- final String previous = this.getStoredNativeBuildVersion();
2230
- if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
2231
- logger.info("New native build version detected: " + this.currentBuildVersion);
2232
- this.implementation.reset(true);
2233
- final List<BundleInfo> installed = this.implementation.list(false);
2234
- for (final BundleInfo bundle : installed) {
2235
- // Check if thread was interrupted (cancelled)
2236
- if (Thread.currentThread().isInterrupted()) {
2237
- logger.warn("Cleanup was cancelled, stopping");
2238
- return;
2239
- }
2240
- try {
2241
- logger.info("Deleting obsolete bundle: " + bundle.getId());
2242
- this.implementation.delete(bundle.getId());
2243
- } catch (final Exception e) {
2244
- logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
2245
- }
2246
- }
2247
- final List<BundleInfo> storedBundles = this.implementation.list(true);
2248
- final Set<String> allowedIds = new HashSet<>();
2249
- for (final BundleInfo info : storedBundles) {
2250
- if (info != null && info.getId() != null && !info.getId().isEmpty()) {
2251
- allowedIds.add(info.getId());
2289
+ try {
2290
+ synchronized (cleanupLock) {
2291
+ try {
2292
+ final String previous = this.getStoredNativeBuildVersion();
2293
+ final boolean nativeVersionChanged = !"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous);
2294
+ if (resetWhenUpdate && nativeVersionChanged) {
2295
+ logger.info("New native build version detected: " + this.currentBuildVersion);
2296
+ this.implementation.reset(true);
2297
+ final List<BundleInfo> installed = this.implementation.list(false);
2298
+ for (final BundleInfo bundle : installed) {
2299
+ try {
2300
+ logger.info("Deleting obsolete bundle: " + bundle.getId());
2301
+ this.implementation.delete(bundle.getId());
2302
+ } catch (final Exception e) {
2303
+ logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
2304
+ }
2305
+ try {
2306
+ Thread.sleep(75L);
2307
+ } catch (final InterruptedException ie) {
2308
+ Thread.currentThread().interrupt();
2309
+ return;
2310
+ }
2252
2311
  }
2312
+ this.implementation.cleanupDeltaCache();
2253
2313
  }
2254
- this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
2255
- this.implementation.cleanupOrphanedTempFolders(Thread.currentThread());
2256
2314
 
2257
- // Check again before the expensive delta cache cleanup
2258
- if (Thread.currentThread().isInterrupted()) {
2259
- logger.warn("Cleanup was cancelled before delta cache cleanup");
2260
- return;
2261
- }
2262
- this.implementation.cleanupDeltaCache(Thread.currentThread());
2263
- }
2264
- this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
2265
- this.editor.apply();
2266
- } catch (Exception e) {
2267
- logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
2268
- } finally {
2269
- cleanupComplete = true;
2270
- logger.info("Cleanup complete");
2271
- }
2272
- }
2273
- });
2315
+ // Resume any DELETING leftovers from prior kills, one-by-one.
2316
+ this.implementation.drainPendingDeletes();
2274
2317
 
2275
- // Start a timeout watchdog thread to cancel cleanup if it takes too long
2276
- final long timeout = this.appReadyTimeout / 2;
2277
- startNewThread(() -> {
2278
- try {
2279
- Thread.sleep(timeout);
2280
- if (cleanupThread != null && cleanupThread.isAlive() && !cleanupComplete) {
2281
- logger.warn("Cleanup timeout exceeded (" + timeout + "ms), interrupting cleanup thread");
2282
- cleanupThread.interrupt();
2318
+ // Always sweep orphan folders so incomplete prior cleanups (or failed deletes)
2319
+ // cannot leave hundreds of MB behind across launches.
2320
+ final Set<String> allowedIds = this.implementation.allowedBundleIdsForCleanup();
2321
+ this.implementation.cleanupDownloadDirectories(allowedIds);
2322
+ this.implementation.cleanupOrphanedTempFolders(null);
2323
+
2324
+ this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
2325
+ this.editor.apply();
2326
+ } catch (Exception e) {
2327
+ logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
2328
+ } finally {
2329
+ cleanupComplete = true;
2330
+ logger.info("Cleanup complete");
2331
+ }
2283
2332
  }
2284
- } catch (InterruptedException e) {
2285
- // Watchdog thread was interrupted, that's fine
2333
+ } finally {
2334
+ latch.countDown();
2286
2335
  }
2287
2336
  });
2288
2337
  }
@@ -2306,11 +2355,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
2306
2355
  }
2307
2356
 
2308
2357
  logger.info("Waiting for cleanup to complete before starting download...");
2309
-
2310
- // Wait for cleanup to complete - blocks until lock is released
2311
- synchronized (cleanupLock) {
2312
- logger.info("Cleanup finished, proceeding with download");
2358
+ try {
2359
+ this.cleanupLatch.await();
2360
+ } catch (final InterruptedException e) {
2361
+ Thread.currentThread().interrupt();
2362
+ logger.warn("Interrupted while waiting for cleanup");
2363
+ throw new IllegalStateException("Interrupted while waiting for cleanup");
2313
2364
  }
2365
+ logger.info("Cleanup finished, proceeding with download");
2314
2366
  }
2315
2367
 
2316
2368
  public void notifyDownload(final String id, final int percent) {
@@ -2648,6 +2700,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2648
2700
  final String checksum,
2649
2701
  final JSONArray manifest
2650
2702
  ) throws IOException {
2703
+ // Manual/preview downloads must wait too — launch orphan sweep can delete their temps.
2704
+ waitForCleanupIfNeeded();
2651
2705
  if (manifest != null) {
2652
2706
  return this.implementation.downloadManifest(url, version, sessionKey, checksum, manifest);
2653
2707
  }
@@ -2821,6 +2875,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2821
2875
  }
2822
2876
 
2823
2877
  protected boolean _reload() {
2878
+ // Drop any launch pending wait; this reload owns notifyAppReady synchronization.
2879
+ this.clearPendingNotifyAppReadyWait();
2824
2880
  final int phase = this.semaphoreUp();
2825
2881
  this.applyCurrentBundleToBridge();
2826
2882
 
@@ -4961,15 +5017,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
4961
5017
  this.implementation.setError(current);
4962
5018
  this.performReset(true, false, true);
4963
5019
  if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
4964
- logger.info("Deleting failing bundle: " + current.getVersionName());
4965
- try {
4966
- final Boolean res = this.implementation.delete(current.getId(), false);
4967
- if (res) {
4968
- logger.info("Failed bundle deleted: " + current.getVersionName());
5020
+ final String failedId = current.getId();
5021
+ final String failedVersion = current.getVersionName();
5022
+ logger.info("Deleting failing bundle: " + failedVersion);
5023
+ // Mark before async work so kill/OOM still resumes via drainPendingDeletes.
5024
+ CapacitorUpdaterPlugin.this.implementation.saveBundleInfo(failedId, current.setStatus(BundleStatus.DELETING));
5025
+ startNewThread(() -> {
5026
+ try {
5027
+ final Boolean res = CapacitorUpdaterPlugin.this.implementation.delete(failedId, false, false);
5028
+ if (Boolean.TRUE.equals(res)) {
5029
+ logger.info("Failed bundle deleted: " + failedVersion);
5030
+ }
5031
+ } catch (final IOException e) {
5032
+ logger.error("Failed to delete failed bundle: " + failedVersion + " " + e.getMessage());
4969
5033
  }
4970
- } catch (final IOException e) {
4971
- logger.error("Failed to delete failed bundle: " + current.getVersionName() + " " + e.getMessage());
4972
- }
5034
+ });
4973
5035
  }
4974
5036
  } else {
4975
5037
  logger.info("notifyAppReady was called. This is fine: " + current.getId());