@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 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 API timeout. Only available for Android and iOS. | <code>20 // (20 second)</code> | |
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 (!this.isBuiltin() && this.downloaded != null && !this.downloaded.isEmpty() && !this.isDeleted() && !this.isDeleting());
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.13";
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
- this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
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
- if (
1755
- this.implementation == null ||
1756
- this.implementation.statsUrl == null ||
1757
- this.implementation.statsUrl.isEmpty() ||
1758
- this.launchReadyReported
1759
- ) {
1760
- return;
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
- if (this.implementation == null || this.implementation.statsUrl == null || this.implementation.statsUrl.isEmpty()) {
1773
- return;
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.appReadyTimeout));
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
- if (this.isDownloadStuckOrTimedOut()) {
4222
- logger.info("Download already in progress, skipping duplicate download request");
4223
- return "already_running";
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 current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
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") : current.getVersionName();
4774
- CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(kind, error, errorMessage, statusCode, latestVersion, current);
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
- current,
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
- current.getVersionName(),
5053
- current,
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
- private File unzip(final String id, final File zipFile, final String dest) throws IOException {
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.cancelVersionDownload(this.activity, version);
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.cancelVersionDownload(this.activity, deleted.getVersionName());
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 (shouldDeletePrevious) {
1939
- new Thread(() -> {
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(previousFallbackId, true, false);
1964
+ final Boolean res = this.delete(asyncPreviousFallbackId, true, false);
1942
1965
  if (Boolean.TRUE.equals(res)) {
1943
- logger.info("Deleted previous bundle: " + previousFallbackVersion);
1966
+ logger.info("Deleted previous bundle: " + asyncPreviousFallbackVersion);
1944
1967
  } else {
1945
- logger.debug("Previous bundle delete incomplete, will retry: " + previousFallbackId);
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: " + previousFallbackId + " " + e.getMessage());
1971
+ logger.error("Failed to delete previous bundle: " + asyncPreviousFallbackId + " " + e.getMessage());
1949
1972
  }
1950
- }, "CapgoUpdater-autoDeletePrevious").start();
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 || !file.exists()) {
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
- try (FileOutputStream out = new FileOutputStream(tmp)) {
2962
- out.write(bytes);
2963
- out.flush();
2964
- }
2965
- if (tmp.renameTo(file)) {
2966
- return;
2967
- }
2968
- if (file.exists() && !file.delete()) {
2969
- throw new IOException("Failed to replace " + file.getAbsolutePath());
2970
- }
2971
- if (!tmp.renameTo(file)) {
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 tempFile = File.createTempFile("capgo-aes-", ".tmp", file.getParentFile());
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 currentManifest;
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
- this.currentManifest = manifest;
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 JSONArray getAndClearManifest() {
24
- JSONArray manifest = this.currentManifest;
25
- this.currentManifest = null;
26
- return manifest;
41
+ public synchronized void clearAllManifests() {
42
+ this.manifestsById.clear();
27
43
  }
28
44
  }