@capgo/capacitor-updater 8.51.14 → 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/CapacitorUpdaterPlugin.java +37 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +46 -23
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +57 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- 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/CapacitorUpdaterPlugin.swift +6 -5
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +37 -5
- 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> | |
|
|
@@ -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;
|
|
@@ -881,7 +881,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
881
881
|
this.autoSplashscreenLoader = this.getConfig().getBoolean("autoSplashscreenLoader", false);
|
|
882
882
|
int splashscreenTimeoutValue = this.getConfig().getInt("autoSplashscreenTimeout", 10000);
|
|
883
883
|
this.autoSplashscreenTimeout = Math.max(0, splashscreenTimeoutValue);
|
|
884
|
-
|
|
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);
|
|
885
888
|
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
886
889
|
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
887
890
|
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
@@ -4230,12 +4233,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4230
4233
|
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4231
4234
|
return "preview_session";
|
|
4232
4235
|
}
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
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";
|
|
4236
4247
|
}
|
|
4237
|
-
this.backgroundDownload();
|
|
4238
|
-
return "queued";
|
|
4239
4248
|
}
|
|
4240
4249
|
|
|
4241
4250
|
@PluginMethod
|
|
@@ -4748,10 +4757,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4748
4757
|
return true;
|
|
4749
4758
|
}
|
|
4750
4759
|
|
|
4751
|
-
private Thread backgroundDownload() {
|
|
4760
|
+
private synchronized Thread backgroundDownload() {
|
|
4752
4761
|
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4753
4762
|
return null;
|
|
4754
4763
|
}
|
|
4764
|
+
if (this.isDownloadStuckOrTimedOut()) {
|
|
4765
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
4766
|
+
return this.backgroundDownloadTask;
|
|
4767
|
+
}
|
|
4755
4768
|
final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
|
|
4756
4769
|
final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
|
|
4757
4770
|
final String messageUpdate = initialDirectUpdateAllowed
|
|
@@ -4760,8 +4773,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4760
4773
|
? "Update will occur next time app moves to background."
|
|
4761
4774
|
: "Update will be downloaded and made available.";
|
|
4762
4775
|
Thread newTask = startNewThread(() -> {
|
|
4763
|
-
// Wait for cleanup to complete before starting download
|
|
4764
|
-
waitForCleanupIfNeeded();
|
|
4765
4776
|
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4766
4777
|
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4767
4778
|
return;
|
|
@@ -4774,7 +4785,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4774
4785
|
return;
|
|
4775
4786
|
}
|
|
4776
4787
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
4777
|
-
final BundleInfo
|
|
4788
|
+
final BundleInfo currentBeforeCleanup = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
4778
4789
|
|
|
4779
4790
|
// Handle network errors and other failures first
|
|
4780
4791
|
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
@@ -4782,8 +4793,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4782
4793
|
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
|
|
4783
4794
|
int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
|
|
4784
4795
|
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
|
|
4785
|
-
String latestVersion = jsRes.has("version") ? jsRes.getString("version") :
|
|
4786
|
-
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
|
+
);
|
|
4787
4805
|
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(
|
|
4788
4806
|
jsRes,
|
|
4789
4807
|
jsRes.has("version") ? jsRes.getString("version") : ""
|
|
@@ -4803,7 +4821,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4803
4821
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4804
4822
|
errorMessage,
|
|
4805
4823
|
latestVersion,
|
|
4806
|
-
|
|
4824
|
+
currentBeforeCleanup,
|
|
4807
4825
|
isFailure,
|
|
4808
4826
|
plannedDirectUpdate,
|
|
4809
4827
|
"download_fail",
|
|
@@ -4813,6 +4831,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4813
4831
|
return;
|
|
4814
4832
|
}
|
|
4815
4833
|
try {
|
|
4834
|
+
// File mutations wait here. getLatest already ran in parallel with cleanup.
|
|
4835
|
+
waitForCleanupIfNeeded();
|
|
4836
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
4816
4837
|
final String latestVersionName = jsRes.getString("version");
|
|
4817
4838
|
|
|
4818
4839
|
if ("builtin".equals(latestVersionName)) {
|
|
@@ -5061,8 +5082,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
5061
5082
|
logger.error("error in update check " + e.getMessage());
|
|
5062
5083
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
5063
5084
|
"Error in update check",
|
|
5064
|
-
|
|
5065
|
-
|
|
5085
|
+
currentBeforeCleanup.getVersionName(),
|
|
5086
|
+
currentBeforeCleanup,
|
|
5066
5087
|
true,
|
|
5067
5088
|
plannedDirectUpdate
|
|
5068
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
|
|
|
@@ -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
|
}
|
|
@@ -83,7 +83,11 @@ public class DownloadService extends Worker {
|
|
|
83
83
|
private static final String UPDATE_FILE = "update.dat";
|
|
84
84
|
|
|
85
85
|
// Shared OkHttpClient to prevent resource leaks
|
|
86
|
-
protected static OkHttpClient sharedClient;
|
|
86
|
+
protected static volatile OkHttpClient sharedClient;
|
|
87
|
+
private static final Object HTTP_CLIENT_LOCK = new Object();
|
|
88
|
+
// Match CapgoUpdater.timeout / responseTimeout default (20s). OkHttp's 10s
|
|
89
|
+
// defaults were unused by the plugin config and aborted slow manifest GETs.
|
|
90
|
+
private static volatile int httpTimeoutMs = 20_000;
|
|
87
91
|
private static String currentAppId = "unknown";
|
|
88
92
|
private static String currentPluginVersion = "unknown";
|
|
89
93
|
private static String currentVersionOs = "unknown";
|
|
@@ -96,6 +100,9 @@ public class DownloadService extends Worker {
|
|
|
96
100
|
sharedClient = new OkHttpClient.Builder()
|
|
97
101
|
.dispatcher(dispatcher)
|
|
98
102
|
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
|
|
103
|
+
.connectTimeout(httpTimeoutMs, TimeUnit.MILLISECONDS)
|
|
104
|
+
.readTimeout(httpTimeoutMs, TimeUnit.MILLISECONDS)
|
|
105
|
+
.writeTimeout(httpTimeoutMs, TimeUnit.MILLISECONDS)
|
|
99
106
|
.addInterceptor((chain) -> {
|
|
100
107
|
Request originalRequest = chain.request();
|
|
101
108
|
String userAgent = buildUserAgent(currentAppId, currentPluginVersion, currentVersionOs);
|
|
@@ -105,6 +112,32 @@ public class DownloadService extends Worker {
|
|
|
105
112
|
.build();
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
static int httpTimeoutMs() {
|
|
116
|
+
return httpTimeoutMs;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static void applyHttpTimeouts(int timeoutMs) {
|
|
120
|
+
// OkHttp treats 0 as infinite; keep plugin responseTimeout floor (20s default).
|
|
121
|
+
int ms = timeoutMs > 0 ? timeoutMs : 20_000;
|
|
122
|
+
synchronized (HTTP_CLIENT_LOCK) {
|
|
123
|
+
if (
|
|
124
|
+
sharedClient.connectTimeoutMillis() == ms &&
|
|
125
|
+
sharedClient.readTimeoutMillis() == ms &&
|
|
126
|
+
sharedClient.writeTimeoutMillis() == ms
|
|
127
|
+
) {
|
|
128
|
+
httpTimeoutMs = ms;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
sharedClient = sharedClient
|
|
132
|
+
.newBuilder()
|
|
133
|
+
.connectTimeout(ms, TimeUnit.MILLISECONDS)
|
|
134
|
+
.readTimeout(ms, TimeUnit.MILLISECONDS)
|
|
135
|
+
.writeTimeout(ms, TimeUnit.MILLISECONDS)
|
|
136
|
+
.build();
|
|
137
|
+
httpTimeoutMs = ms;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
108
141
|
static int manifestMaxConcurrentFiles() {
|
|
109
142
|
return manifestMaxConcurrentFiles(Runtime.getRuntime().availableProcessors());
|
|
110
143
|
}
|
|
@@ -342,10 +375,12 @@ public class DownloadService extends Worker {
|
|
|
342
375
|
logger.debug("doWork isManifest: " + isManifest);
|
|
343
376
|
|
|
344
377
|
if (isManifest) {
|
|
345
|
-
JSONArray manifest = DataManager.getInstance().getAndClearManifest();
|
|
378
|
+
JSONArray manifest = DataManager.getInstance().getAndClearManifest(id);
|
|
346
379
|
if (manifest != null) {
|
|
347
380
|
handleManifestDownload(id, documentsDir, dest, version, sessionKey, publicKey, manifest);
|
|
348
381
|
return createSuccessResult(dest, version, sessionKey, checksum, true);
|
|
382
|
+
} else if (isStopped()) {
|
|
383
|
+
return createFailureResult("download_cancelled");
|
|
349
384
|
} else {
|
|
350
385
|
logger.error("Manifest is null");
|
|
351
386
|
return createFailureResult("Manifest is null");
|
|
@@ -526,8 +561,7 @@ public class DownloadService extends Worker {
|
|
|
526
561
|
try {
|
|
527
562
|
if (tryCopyBuiltinAsset(assets, fileName, targetFile, finalFileHash)) {
|
|
528
563
|
logger.debug("using builtin asset " + fileName);
|
|
529
|
-
} else if (
|
|
530
|
-
copyFile(builtinFile, targetFile);
|
|
564
|
+
} else if (tryCopyBuiltinFile(builtinFile, targetFile, finalFileHash)) {
|
|
531
565
|
logger.debug("using builtin file " + fileName);
|
|
532
566
|
} else if (
|
|
533
567
|
tryCopyFromCache(cacheFile, targetFile, finalFileHash) ||
|
|
@@ -626,9 +660,11 @@ public class DownloadService extends Worker {
|
|
|
626
660
|
URL u = new URL(url);
|
|
627
661
|
httpConn = (HttpURLConnection) u.openConnection();
|
|
628
662
|
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
663
|
+
// Zip can stall longer than a JSON API call; keep a floor so
|
|
664
|
+
// responseTimeout cannot shrink large-bundle downloads.
|
|
665
|
+
int zipTimeoutMs = Math.max(httpTimeoutMs, 60_000);
|
|
666
|
+
httpConn.setConnectTimeout(zipTimeoutMs);
|
|
667
|
+
httpConn.setReadTimeout(zipTimeoutMs);
|
|
632
668
|
|
|
633
669
|
// Reading progress file (if exist)
|
|
634
670
|
long downloadedBytes = 0;
|
|
@@ -678,7 +714,7 @@ public class DownloadService extends Worker {
|
|
|
678
714
|
writer = null;
|
|
679
715
|
}
|
|
680
716
|
|
|
681
|
-
byte[] buffer = new byte[
|
|
717
|
+
byte[] buffer = new byte[CryptoCipher.ioBufferBytes()];
|
|
682
718
|
int lastNotifiedPercent = 0;
|
|
683
719
|
int bytesRead;
|
|
684
720
|
|
|
@@ -801,6 +837,10 @@ public class DownloadService extends Worker {
|
|
|
801
837
|
}
|
|
802
838
|
|
|
803
839
|
private void copyFile(File source, File dest) throws IOException {
|
|
840
|
+
copyFileChannel(source, dest);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
static void copyFileChannel(final File source, final File dest) throws IOException {
|
|
804
844
|
final File parent = dest.getParentFile();
|
|
805
845
|
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
|
806
846
|
throw new IOException("Failed to create parent directory: " + parent.getAbsolutePath());
|
|
@@ -971,33 +1011,19 @@ public class DownloadService extends Worker {
|
|
|
971
1011
|
}
|
|
972
1012
|
}
|
|
973
1013
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
String actualHash = calculateFileHash(file);
|
|
977
|
-
return actualHash.equalsIgnoreCase(expectedHash);
|
|
978
|
-
} catch (Exception e) {
|
|
979
|
-
e.printStackTrace();
|
|
1014
|
+
static boolean tryCopyBuiltinFile(final File builtinFile, final File dest, final String expectedHash) {
|
|
1015
|
+
if (builtinFile == null || dest == null || expectedHash == null || expectedHash.isEmpty() || !builtinFile.isFile()) {
|
|
980
1016
|
return false;
|
|
981
1017
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
986
|
-
byte[] byteArray = new byte[1024];
|
|
987
|
-
int bytesCount = 0;
|
|
988
|
-
|
|
989
|
-
try (FileInputStream fis = new FileInputStream(file)) {
|
|
990
|
-
while ((bytesCount = fis.read(byteArray)) != -1) {
|
|
991
|
-
digest.update(byteArray, 0, bytesCount);
|
|
1018
|
+
try {
|
|
1019
|
+
if (!expectedHash.equalsIgnoreCase(CryptoCipher.calcChecksum(builtinFile))) {
|
|
1020
|
+
return false;
|
|
992
1021
|
}
|
|
1022
|
+
copyFileChannel(builtinFile, dest);
|
|
1023
|
+
return true;
|
|
1024
|
+
} catch (IOException e) {
|
|
1025
|
+
return false;
|
|
993
1026
|
}
|
|
994
|
-
|
|
995
|
-
byte[] bytes = digest.digest();
|
|
996
|
-
StringBuilder sb = new StringBuilder();
|
|
997
|
-
for (byte aByte : bytes) {
|
|
998
|
-
sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
|
|
999
|
-
}
|
|
1000
|
-
return sb.toString();
|
|
1001
1027
|
}
|
|
1002
1028
|
|
|
1003
1029
|
static void decompressBrotli(File input, File output, String fileName) throws IOException {
|