@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 +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +143 -81
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +289 -59
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +1 -1
- package/dist/docs.json +8 -0
- package/dist/esm/definitions.d.ts +1 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +5 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +105 -49
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +186 -32
- package/package.json +1 -1
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 :
|
|
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() ? "" :
|
|
114
|
+
return this.isBuiltin() ? "" : this.checksum != null ? this.checksum : "";
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
public BundleInfo setChecksum(String checksum) {
|
|
@@ -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.
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
|
993
|
-
|
|
994
|
-
ret.put("status", msg);
|
|
1029
|
+
final int[] pendingPhase = new int[] { -1 };
|
|
1030
|
+
final boolean shouldWait = this.consumePendingNotifyAppReadyWait(pendingPhase);
|
|
995
1031
|
|
|
996
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
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
|
-
//
|
|
2258
|
-
|
|
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
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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
|
-
}
|
|
2285
|
-
|
|
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
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
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
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
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
|
-
}
|
|
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());
|