@capgo/capacitor-updater 8.51.13 → 8.51.15
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/Package.swift +2 -1
- package/README.md +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +62 -29
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +89 -35
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +8 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +65 -39
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +3 -0
- package/dist/docs.json +1 -1
- package/dist/esm/definitions.d.ts +4 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +5 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +32 -11
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +55 -8
- package/package.json +1 -1
package/Package.swift
CHANGED
|
@@ -32,7 +32,8 @@ let package = Package(
|
|
|
32
32
|
name: "CapacitorUpdaterPluginTests",
|
|
33
33
|
dependencies: [
|
|
34
34
|
"CapacitorUpdaterPlugin",
|
|
35
|
-
.product(name: "Version", package: "Version")
|
|
35
|
+
.product(name: "Version", package: "Version"),
|
|
36
|
+
.product(name: "ZIPFoundation", package: "ZIPFoundation")
|
|
36
37
|
],
|
|
37
38
|
path: "ios/Tests/CapacitorUpdaterPluginTests")
|
|
38
39
|
],
|
package/README.md
CHANGED
|
@@ -303,7 +303,7 @@ CapacitorUpdater can be configured with these options:
|
|
|
303
303
|
| Prop | Type | Description | Default | Since |
|
|
304
304
|
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------- |
|
|
305
305
|
| **`appReadyTimeout`** | <code>number</code> | Configure the number of milliseconds the native plugin should wait before considering an update 'failed'. Only available for Android and iOS. | <code>10000 // (10 seconds)</code> | |
|
|
306
|
-
| **`responseTimeout`** | <code>number</code> | Configure the number of seconds the native plugin should wait before considering
|
|
306
|
+
| **`responseTimeout`** | <code>number</code> | Configure the number of seconds the native plugin should wait before considering an HTTP timeout. Applies to update checks and file downloads. On Android these are idle connect/read/write timeouts and do not cap total download time; on iOS the request timeout also bounds the total download duration. Only available for Android and iOS. | <code>20 // (20 second)</code> | |
|
|
307
307
|
| **`autoDeleteFailed`** | <code>boolean</code> | Configure whether the plugin should use automatically delete failed bundles. Only available for Android and iOS. | <code>true</code> | |
|
|
308
308
|
| **`autoDeletePrevious`** | <code>boolean</code> | Configure whether the plugin should use automatically delete previous bundles after a successful update. Only available for Android and iOS. | <code>true</code> | |
|
|
309
309
|
| **`autoUpdate`** | <code>boolean \| 'always' \| 'off' \| 'atBackground' \| 'atInstall' \| 'onLaunch' \| 'onlyDownload'</code> | Configure how the plugin checks for, downloads, and applies live updates. The plugin checks for updates when the app moves to the foreground. When {@link periodCheckDelay} is greater than 0, it also checks on a repeating timer while the app stays open. Boolean values keep their existing behavior: - `true`: Same as `"atBackground"`. - `false`: Same as `"off"`. String values merge the previous Auto Update and Direct Update configuration: - `"off"`: Disable automatic update checks. - `"atBackground"`: Check and download automatically on each foreground check, then apply the update the next time the app moves to background. - `"atInstall"`: Apply immediately only after a fresh install or native app store update; otherwise use `"atBackground"` behavior. - `"onLaunch"`: Apply immediately only when the app is brought to the foreground from a killed state (cold start). After that first check, fall back to `"atBackground"` behavior. - `"always"`: Check on every foreground transition and apply immediately whenever an update is available. - `"onlyDownload"`: Check and download automatically, emit `updateAvailable`, and never set the next bundle or apply an update automatically. Only available for Android and iOS. | <code>true</code> | |
|
|
@@ -99,7 +99,7 @@ public class BundleInfo {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
public boolean isDownloaded() {
|
|
102
|
-
return
|
|
102
|
+
return !this.isBuiltin() && this.downloaded != null && !this.downloaded.isEmpty() && !this.isDeleted() && !this.isDeleting();
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
public String getDownloaded() {
|
|
@@ -146,7 +146,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
146
146
|
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
147
147
|
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
148
148
|
|
|
149
|
-
private final String pluginVersion = "8.51.
|
|
149
|
+
private final String pluginVersion = "8.51.15";
|
|
150
150
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
151
151
|
|
|
152
152
|
private SharedPreferences.Editor editor;
|
|
@@ -250,6 +250,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
250
250
|
private volatile long webViewPageStartedAtMs = 0;
|
|
251
251
|
private volatile boolean launchStartReported = false;
|
|
252
252
|
private volatile boolean launchReadyReported = false;
|
|
253
|
+
private volatile boolean launchTimeoutReported = false;
|
|
254
|
+
private final Object launchReportLock = new Object();
|
|
253
255
|
private FrameLayout splashscreenLoaderOverlay;
|
|
254
256
|
private Runnable splashscreenTimeoutRunnable;
|
|
255
257
|
private FrameLayout previewTransitionLoaderOverlay;
|
|
@@ -879,7 +881,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
879
881
|
this.autoSplashscreenLoader = this.getConfig().getBoolean("autoSplashscreenLoader", false);
|
|
880
882
|
int splashscreenTimeoutValue = this.getConfig().getInt("autoSplashscreenTimeout", 10000);
|
|
881
883
|
this.autoSplashscreenTimeout = Math.max(0, splashscreenTimeoutValue);
|
|
882
|
-
|
|
884
|
+
int responseTimeoutSeconds = this.getConfig().getInt("responseTimeout", 20);
|
|
885
|
+
long responseTimeoutMillis = responseTimeoutSeconds > 0 ? (long) responseTimeoutSeconds * 1000L : 20_000L;
|
|
886
|
+
this.implementation.timeout = (int) Math.min(Integer.MAX_VALUE, responseTimeoutMillis);
|
|
887
|
+
DownloadService.applyHttpTimeouts(this.implementation.timeout);
|
|
883
888
|
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
884
889
|
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
885
890
|
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
@@ -1751,16 +1756,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1751
1756
|
}
|
|
1752
1757
|
|
|
1753
1758
|
private void reportAppLaunchReady(final BundleInfo bundle) {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1759
|
+
synchronized (this.launchReportLock) {
|
|
1760
|
+
if (
|
|
1761
|
+
this.implementation == null ||
|
|
1762
|
+
this.implementation.statsUrl == null ||
|
|
1763
|
+
this.implementation.statsUrl.isEmpty() ||
|
|
1764
|
+
this.launchReadyReported ||
|
|
1765
|
+
this.launchTimeoutReported
|
|
1766
|
+
) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
this.launchReadyReported = true;
|
|
1761
1770
|
}
|
|
1762
|
-
|
|
1763
|
-
this.launchReadyReported = true;
|
|
1764
1771
|
final Map<String, String> metadata = new HashMap<>();
|
|
1765
1772
|
metadata.put("duration_ms", Long.toString(Math.max(0, System.currentTimeMillis() - this.launchStartedAtMs)));
|
|
1766
1773
|
metadata.put("launch_started_at", Long.toString(this.launchStartedAtMs));
|
|
@@ -1769,14 +1776,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1769
1776
|
}
|
|
1770
1777
|
|
|
1771
1778
|
private void reportAppLaunchTimeout(final BundleInfo bundle) {
|
|
1772
|
-
|
|
1773
|
-
|
|
1779
|
+
synchronized (this.launchReportLock) {
|
|
1780
|
+
if (
|
|
1781
|
+
this.implementation == null ||
|
|
1782
|
+
this.implementation.statsUrl == null ||
|
|
1783
|
+
this.implementation.statsUrl.isEmpty() ||
|
|
1784
|
+
this.launchReadyReported ||
|
|
1785
|
+
this.launchTimeoutReported
|
|
1786
|
+
) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
this.launchTimeoutReported = true;
|
|
1774
1790
|
}
|
|
1775
|
-
|
|
1776
1791
|
final Map<String, String> metadata = new HashMap<>();
|
|
1777
1792
|
metadata.put("duration_ms", Long.toString(Math.max(0, System.currentTimeMillis() - this.launchStartedAtMs)));
|
|
1778
1793
|
metadata.put("launch_started_at", Long.toString(this.launchStartedAtMs));
|
|
1779
|
-
metadata.put("timeout_ms", Long.toString(this.
|
|
1794
|
+
metadata.put("timeout_ms", Long.toString(this.resolveAppReadyCheckTimeoutMs()));
|
|
1780
1795
|
metadata.put("source", "app_ready_timeout");
|
|
1781
1796
|
this.implementation.sendStats("app_launch_timeout", bundle == null ? "" : bundle.getVersionName(), "", metadata);
|
|
1782
1797
|
}
|
|
@@ -4218,12 +4233,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4218
4233
|
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4219
4234
|
return "preview_session";
|
|
4220
4235
|
}
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4236
|
+
synchronized (this) {
|
|
4237
|
+
final Thread previousTask = this.backgroundDownloadTask;
|
|
4238
|
+
final Thread task = this.backgroundDownload();
|
|
4239
|
+
if (task == null) {
|
|
4240
|
+
return "unavailable";
|
|
4241
|
+
}
|
|
4242
|
+
if (previousTask != null && previousTask == task) {
|
|
4243
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
4244
|
+
return "already_running";
|
|
4245
|
+
}
|
|
4246
|
+
return "queued";
|
|
4224
4247
|
}
|
|
4225
|
-
this.backgroundDownload();
|
|
4226
|
-
return "queued";
|
|
4227
4248
|
}
|
|
4228
4249
|
|
|
4229
4250
|
@PluginMethod
|
|
@@ -4736,10 +4757,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4736
4757
|
return true;
|
|
4737
4758
|
}
|
|
4738
4759
|
|
|
4739
|
-
private Thread backgroundDownload() {
|
|
4760
|
+
private synchronized Thread backgroundDownload() {
|
|
4740
4761
|
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4741
4762
|
return null;
|
|
4742
4763
|
}
|
|
4764
|
+
if (this.isDownloadStuckOrTimedOut()) {
|
|
4765
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
4766
|
+
return this.backgroundDownloadTask;
|
|
4767
|
+
}
|
|
4743
4768
|
final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
|
|
4744
4769
|
final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
|
|
4745
4770
|
final String messageUpdate = initialDirectUpdateAllowed
|
|
@@ -4748,8 +4773,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4748
4773
|
? "Update will occur next time app moves to background."
|
|
4749
4774
|
: "Update will be downloaded and made available.";
|
|
4750
4775
|
Thread newTask = startNewThread(() -> {
|
|
4751
|
-
// Wait for cleanup to complete before starting download
|
|
4752
|
-
waitForCleanupIfNeeded();
|
|
4753
4776
|
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4754
4777
|
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4755
4778
|
return;
|
|
@@ -4762,7 +4785,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4762
4785
|
return;
|
|
4763
4786
|
}
|
|
4764
4787
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
4765
|
-
final BundleInfo
|
|
4788
|
+
final BundleInfo currentBeforeCleanup = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
4766
4789
|
|
|
4767
4790
|
// Handle network errors and other failures first
|
|
4768
4791
|
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
@@ -4770,8 +4793,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4770
4793
|
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
|
|
4771
4794
|
int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
|
|
4772
4795
|
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
|
|
4773
|
-
String latestVersion = jsRes.has("version") ? jsRes.getString("version") :
|
|
4774
|
-
CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
|
|
4796
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : currentBeforeCleanup.getVersionName();
|
|
4797
|
+
CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
|
|
4798
|
+
kind,
|
|
4799
|
+
error,
|
|
4800
|
+
errorMessage,
|
|
4801
|
+
statusCode,
|
|
4802
|
+
latestVersion,
|
|
4803
|
+
currentBeforeCleanup
|
|
4804
|
+
);
|
|
4775
4805
|
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(
|
|
4776
4806
|
jsRes,
|
|
4777
4807
|
jsRes.has("version") ? jsRes.getString("version") : ""
|
|
@@ -4791,7 +4821,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4791
4821
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4792
4822
|
errorMessage,
|
|
4793
4823
|
latestVersion,
|
|
4794
|
-
|
|
4824
|
+
currentBeforeCleanup,
|
|
4795
4825
|
isFailure,
|
|
4796
4826
|
plannedDirectUpdate,
|
|
4797
4827
|
"download_fail",
|
|
@@ -4801,6 +4831,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4801
4831
|
return;
|
|
4802
4832
|
}
|
|
4803
4833
|
try {
|
|
4834
|
+
// File mutations wait here. getLatest already ran in parallel with cleanup.
|
|
4835
|
+
waitForCleanupIfNeeded();
|
|
4836
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
4804
4837
|
final String latestVersionName = jsRes.getString("version");
|
|
4805
4838
|
|
|
4806
4839
|
if ("builtin".equals(latestVersionName)) {
|
|
@@ -5049,8 +5082,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
5049
5082
|
logger.error("error in update check " + e.getMessage());
|
|
5050
5083
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
5051
5084
|
"Error in update check",
|
|
5052
|
-
|
|
5053
|
-
|
|
5085
|
+
currentBeforeCleanup.getVersionName(),
|
|
5086
|
+
currentBeforeCleanup,
|
|
5054
5087
|
true,
|
|
5055
5088
|
plannedDirectUpdate
|
|
5056
5089
|
);
|
|
@@ -315,14 +315,17 @@ public class CapgoUpdater {
|
|
|
315
315
|
return this.cachedKeyId;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
File unzip(final String id, final File zipFile, final String dest) throws IOException {
|
|
319
|
+
return unzip(id, zipFile, dest, CryptoCipher.ioBufferBytes());
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
File unzip(final String id, final File zipFile, final String dest, final int bufferSize) throws IOException {
|
|
319
323
|
final File targetDirectory = new File(this.documentsDir, dest);
|
|
320
324
|
try (
|
|
321
325
|
final BufferedInputStream bis = new BufferedInputStream(new FileInputStream(zipFile));
|
|
322
326
|
final ZipInputStream zis = new ZipInputStream(bis)
|
|
323
327
|
) {
|
|
324
328
|
int count;
|
|
325
|
-
final int bufferSize = 8192;
|
|
326
329
|
final byte[] buffer = new byte[bufferSize];
|
|
327
330
|
final long lengthTotal = zipFile.length();
|
|
328
331
|
long lengthRead = bufferSize;
|
|
@@ -760,6 +763,13 @@ public class CapgoUpdater {
|
|
|
760
763
|
}
|
|
761
764
|
});
|
|
762
765
|
break;
|
|
766
|
+
case CANCELLED:
|
|
767
|
+
DataManager.getInstance().clearManifest(id);
|
|
768
|
+
CompletableFuture<BundleInfo> cancelledFuture = downloadFutures.remove(id);
|
|
769
|
+
if (cancelledFuture != null) {
|
|
770
|
+
cancelledFuture.cancel(true);
|
|
771
|
+
}
|
|
772
|
+
break;
|
|
763
773
|
}
|
|
764
774
|
});
|
|
765
775
|
});
|
|
@@ -781,6 +791,10 @@ public class CapgoUpdater {
|
|
|
781
791
|
}
|
|
782
792
|
observeWorkProgress(this.activity, id, setNext);
|
|
783
793
|
|
|
794
|
+
if (manifest != null) {
|
|
795
|
+
DataManager.getInstance().setManifest(id, manifest);
|
|
796
|
+
}
|
|
797
|
+
|
|
784
798
|
DownloadWorkerManager.enqueueDownload(
|
|
785
799
|
this.activity,
|
|
786
800
|
url,
|
|
@@ -805,10 +819,6 @@ public class CapgoUpdater {
|
|
|
805
819
|
this.customId,
|
|
806
820
|
this.defaultChannel
|
|
807
821
|
);
|
|
808
|
-
|
|
809
|
-
if (manifest != null) {
|
|
810
|
-
DataManager.getInstance().setManifest(manifest);
|
|
811
|
-
}
|
|
812
822
|
}
|
|
813
823
|
|
|
814
824
|
public Boolean finishDownload(
|
|
@@ -1398,7 +1408,10 @@ public class CapgoUpdater {
|
|
|
1398
1408
|
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
1399
1409
|
if (existingBundle != null && existingBundle.isErrorStatus()) {
|
|
1400
1410
|
// Cancel the failed download and allow retry
|
|
1401
|
-
DownloadWorkerManager.
|
|
1411
|
+
if (!DownloadWorkerManager.cancelVersionDownloadAndAwait(this.activity, version)) {
|
|
1412
|
+
logger.error("Failed to cancel previous download before retry");
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1402
1415
|
logger.info("Retrying failed download for version: " + version);
|
|
1403
1416
|
} else {
|
|
1404
1417
|
logger.info("Version already downloading: " + version);
|
|
@@ -1419,7 +1432,9 @@ public class CapgoUpdater {
|
|
|
1419
1432
|
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
1420
1433
|
if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted() || existingBundle.isDeleting())) {
|
|
1421
1434
|
logger.info("Found existing failed bundle for version " + version + ", deleting before retry");
|
|
1422
|
-
this.delete(existingBundle.getId(), true)
|
|
1435
|
+
if (!Boolean.TRUE.equals(this.delete(existingBundle.getId(), true))) {
|
|
1436
|
+
throw new IOException("Failed to delete existing bundle before retry");
|
|
1437
|
+
}
|
|
1423
1438
|
}
|
|
1424
1439
|
|
|
1425
1440
|
final String id = this.randomString();
|
|
@@ -1472,7 +1487,9 @@ public class CapgoUpdater {
|
|
|
1472
1487
|
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
1473
1488
|
if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted() || existingBundle.isDeleting())) {
|
|
1474
1489
|
logger.info("Found existing failed bundle for version " + version + ", deleting before retry");
|
|
1475
|
-
this.delete(existingBundle.getId(), true)
|
|
1490
|
+
if (!Boolean.TRUE.equals(this.delete(existingBundle.getId(), true))) {
|
|
1491
|
+
throw new IOException("Failed to delete existing bundle before retry");
|
|
1492
|
+
}
|
|
1476
1493
|
}
|
|
1477
1494
|
|
|
1478
1495
|
final String id = this.randomString();
|
|
@@ -1587,7 +1604,10 @@ public class CapgoUpdater {
|
|
|
1587
1604
|
|
|
1588
1605
|
// Cancel download for this version if active
|
|
1589
1606
|
if (cancelActiveDownload && this.activity != null) {
|
|
1590
|
-
DownloadWorkerManager.
|
|
1607
|
+
if (!DownloadWorkerManager.cancelVersionDownloadAndAwait(this.activity, deleted.getVersionName())) {
|
|
1608
|
+
logger.error("Failed to cancel active download before delete");
|
|
1609
|
+
return false;
|
|
1610
|
+
}
|
|
1591
1611
|
}
|
|
1592
1612
|
|
|
1593
1613
|
if (bundle.exists()) {
|
|
@@ -1922,32 +1942,35 @@ public class CapgoUpdater {
|
|
|
1922
1942
|
!fallbackIsPreviewFallback &&
|
|
1923
1943
|
!previousIsNext;
|
|
1924
1944
|
if (shouldDeletePrevious) {
|
|
1925
|
-
// Cancel any in-flight download for the old version before the async boundary
|
|
1926
|
-
// so a later download of the same version name is not cancelled mid-flight.
|
|
1927
|
-
if (this.activity != null) {
|
|
1928
|
-
DownloadWorkerManager.cancelVersionDownload(this.activity, previousFallbackVersion);
|
|
1929
|
-
}
|
|
1930
|
-
// Mark durable intent before fallback switch so a kill mid-flight still retries.
|
|
1931
1945
|
if (!this.saveBundleInfo(previousFallbackId, fallback.setStatus(BundleStatus.DELETING))) {
|
|
1932
1946
|
logger.error("Failed to persist DELETING for previous bundle; queueing durable retry");
|
|
1933
1947
|
logger.debug("Bundle ID: " + previousFallbackId);
|
|
1934
1948
|
this.enqueuePendingDelete(previousFallbackId);
|
|
1935
1949
|
}
|
|
1936
1950
|
}
|
|
1951
|
+
boolean deletePreviousAsync = shouldDeletePrevious;
|
|
1937
1952
|
this.setFallbackBundle(bundle);
|
|
1938
|
-
if (
|
|
1939
|
-
|
|
1953
|
+
if (deletePreviousAsync) {
|
|
1954
|
+
final String asyncPreviousFallbackId = previousFallbackId;
|
|
1955
|
+
final String asyncPreviousFallbackVersion = previousFallbackVersion;
|
|
1956
|
+
io.execute(() -> {
|
|
1957
|
+
if (this.activity != null) {
|
|
1958
|
+
if (!DownloadWorkerManager.cancelVersionDownloadAndAwait(this.activity, asyncPreviousFallbackVersion)) {
|
|
1959
|
+
logger.error("Failed to cancel previous version download before delete");
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1940
1963
|
try {
|
|
1941
|
-
final Boolean res = this.delete(
|
|
1964
|
+
final Boolean res = this.delete(asyncPreviousFallbackId, true, false);
|
|
1942
1965
|
if (Boolean.TRUE.equals(res)) {
|
|
1943
|
-
logger.info("Deleted previous bundle: " +
|
|
1966
|
+
logger.info("Deleted previous bundle: " + asyncPreviousFallbackVersion);
|
|
1944
1967
|
} else {
|
|
1945
|
-
logger.debug("Previous bundle delete incomplete, will retry: " +
|
|
1968
|
+
logger.debug("Previous bundle delete incomplete, will retry: " + asyncPreviousFallbackId);
|
|
1946
1969
|
}
|
|
1947
1970
|
} catch (final IOException e) {
|
|
1948
|
-
logger.error("Failed to delete previous bundle: " +
|
|
1971
|
+
logger.error("Failed to delete previous bundle: " + asyncPreviousFallbackId + " " + e.getMessage());
|
|
1949
1972
|
}
|
|
1950
|
-
}
|
|
1973
|
+
});
|
|
1951
1974
|
}
|
|
1952
1975
|
}
|
|
1953
1976
|
|
|
@@ -2854,7 +2877,17 @@ public class CapgoUpdater {
|
|
|
2854
2877
|
|
|
2855
2878
|
public void restorePendingStats() {
|
|
2856
2879
|
File file = pendingStatsFile();
|
|
2857
|
-
if (file == null
|
|
2880
|
+
if (file == null) {
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
File backup = new File(file.getAbsolutePath() + ".bak");
|
|
2884
|
+
if (!file.exists() && backup.exists() && !backup.renameTo(file)) {
|
|
2885
|
+
if (logger != null) {
|
|
2886
|
+
logger.error("Failed to restore stats backup");
|
|
2887
|
+
}
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
if (!file.exists()) {
|
|
2858
2891
|
return;
|
|
2859
2892
|
}
|
|
2860
2893
|
try {
|
|
@@ -2958,18 +2991,39 @@ public class CapgoUpdater {
|
|
|
2958
2991
|
|
|
2959
2992
|
private static void writeFileAtomically(final File file, final byte[] bytes) throws IOException {
|
|
2960
2993
|
final File tmp = new File(file.getAbsolutePath() + ".tmp");
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
out
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2994
|
+
File backup = null;
|
|
2995
|
+
try {
|
|
2996
|
+
try (FileOutputStream out = new FileOutputStream(tmp)) {
|
|
2997
|
+
out.write(bytes);
|
|
2998
|
+
out.flush();
|
|
2999
|
+
}
|
|
3000
|
+
if (tmp.renameTo(file)) {
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
if (file.exists()) {
|
|
3004
|
+
backup = new File(file.getAbsolutePath() + ".bak");
|
|
3005
|
+
if (backup.exists() && !backup.delete()) {
|
|
3006
|
+
throw new IOException("Failed to replace " + file.getAbsolutePath());
|
|
3007
|
+
}
|
|
3008
|
+
if (!file.renameTo(backup)) {
|
|
3009
|
+
throw new IOException("Failed to replace " + file.getAbsolutePath());
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
if (tmp.renameTo(file)) {
|
|
3013
|
+
if (backup != null && backup.exists() && !backup.delete()) {
|
|
3014
|
+
backup.deleteOnExit();
|
|
3015
|
+
}
|
|
3016
|
+
backup = null;
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
2972
3019
|
throw new IOException("Failed to persist " + file.getAbsolutePath());
|
|
3020
|
+
} finally {
|
|
3021
|
+
if (backup != null && !file.exists()) {
|
|
3022
|
+
backup.renameTo(file);
|
|
3023
|
+
}
|
|
3024
|
+
if (tmp.exists() && !tmp.delete()) {
|
|
3025
|
+
tmp.deleteOnExit();
|
|
3026
|
+
}
|
|
2973
3027
|
}
|
|
2974
3028
|
}
|
|
2975
3029
|
|
|
@@ -169,7 +169,11 @@ public class CryptoCipher {
|
|
|
169
169
|
}
|
|
170
170
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
171
171
|
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getEncoded(), "AES"), new IvParameterSpec(iv));
|
|
172
|
-
File
|
|
172
|
+
File parent = file.getAbsoluteFile().getParentFile();
|
|
173
|
+
if (parent == null) {
|
|
174
|
+
throw new IOException("Cannot create temp file for " + file.getAbsolutePath());
|
|
175
|
+
}
|
|
176
|
+
File tempFile = File.createTempFile("capgo-aes-", ".tmp", parent);
|
|
173
177
|
try {
|
|
174
178
|
byte[] inBuf = new byte[ioBufferBytes()];
|
|
175
179
|
// Reuse one output buffer. cipher.update(in) allocates a new byte[] per chunk.
|
|
@@ -187,6 +191,9 @@ public class CryptoCipher {
|
|
|
187
191
|
fos.write(outBuf, 0, last);
|
|
188
192
|
}
|
|
189
193
|
}
|
|
194
|
+
if (tempFile.length() == 0) {
|
|
195
|
+
throw new IOException("Empty decrypted data");
|
|
196
|
+
}
|
|
190
197
|
replaceFile(tempFile, file);
|
|
191
198
|
tempFile = null;
|
|
192
199
|
} finally {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
package ee.forgr.capacitor_updater;
|
|
2
2
|
|
|
3
|
+
import java.util.HashMap;
|
|
4
|
+
import java.util.Map;
|
|
3
5
|
import org.json.JSONArray;
|
|
4
6
|
|
|
5
7
|
public class DataManager {
|
|
6
8
|
|
|
7
9
|
private static DataManager instance;
|
|
8
|
-
private JSONArray
|
|
10
|
+
private final Map<String, JSONArray> manifestsById = new HashMap<>();
|
|
9
11
|
|
|
10
12
|
private DataManager() {}
|
|
11
13
|
|
|
@@ -16,13 +18,27 @@ public class DataManager {
|
|
|
16
18
|
return instance;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
public void setManifest(JSONArray manifest) {
|
|
20
|
-
|
|
21
|
+
public synchronized void setManifest(String downloadId, JSONArray manifest) {
|
|
22
|
+
if (downloadId == null || manifest == null) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.manifestsById.put(downloadId, manifest);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public synchronized JSONArray getAndClearManifest(String downloadId) {
|
|
29
|
+
if (downloadId == null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return this.manifestsById.remove(downloadId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public synchronized void clearManifest(String downloadId) {
|
|
36
|
+
if (downloadId != null) {
|
|
37
|
+
this.manifestsById.remove(downloadId);
|
|
38
|
+
}
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
public
|
|
24
|
-
|
|
25
|
-
this.currentManifest = null;
|
|
26
|
-
return manifest;
|
|
41
|
+
public synchronized void clearAllManifests() {
|
|
42
|
+
this.manifestsById.clear();
|
|
27
43
|
}
|
|
28
44
|
}
|