@capgo/capacitor-updater 8.51.14 → 8.51.16

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.
@@ -16,7 +16,5 @@ Pod::Spec.new do |s|
16
16
  s.dependency 'ZIPFoundation', '~> 0.9'
17
17
  s.dependency 'Alamofire', '5.10.2'
18
18
  s.dependency 'Version', '0.8.0'
19
- # We cannot update to latest because it not published anymore in cocoa https://github.com/attaswift/BigInt/issues/94
20
- s.dependency 'BigInt', '5.2.0'
21
19
  s.swift_version = '5.1'
22
20
  end
package/Package.swift CHANGED
@@ -13,8 +13,7 @@ let package = Package(
13
13
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
14
14
  .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.12.0")),
15
15
  .package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.20"),
16
- .package(url: "https://github.com/mrackwitz/Version.git", exact: "0.8.0"),
17
- .package(url: "https://github.com/attaswift/BigInt.git", from: "5.7.0")
16
+ .package(url: "https://github.com/mrackwitz/Version.git", exact: "0.8.0")
18
17
  ],
19
18
  targets: [
20
19
  .target(
@@ -24,15 +23,15 @@ let package = Package(
24
23
  .product(name: "Cordova", package: "capacitor-swift-pm"),
25
24
  .product(name: "ZIPFoundation", package: "ZIPFoundation"),
26
25
  .product(name: "Alamofire", package: "Alamofire"),
27
- .product(name: "Version", package: "Version"),
28
- .product(name: "BigInt", package: "BigInt")
26
+ .product(name: "Version", package: "Version")
29
27
  ],
30
28
  path: "ios/Sources/CapacitorUpdaterPlugin"),
31
29
  .testTarget(
32
30
  name: "CapacitorUpdaterPluginTests",
33
31
  dependencies: [
34
32
  "CapacitorUpdaterPlugin",
35
- .product(name: "Version", package: "Version")
33
+ .product(name: "Version", package: "Version"),
34
+ .product(name: "ZIPFoundation", package: "ZIPFoundation")
36
35
  ],
37
36
  path: "ios/Tests/CapacitorUpdaterPluginTests")
38
37
  ],
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> | |
@@ -3,6 +3,7 @@ ext {
3
3
  androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
4
4
  androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
5
5
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
6
+ okhttpVersion = project.hasProperty('okhttpVersion') ? rootProject.ext.okhttpVersion : '4.12.0'
6
7
  }
7
8
 
8
9
  buildscript {
@@ -54,7 +55,6 @@ dependencies {
54
55
  implementation "androidx.work:work-runtime:$work_version"
55
56
  implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
56
57
  implementation "com.google.android.gms:play-services-tasks:18.4.1"
57
- implementation "com.google.guava:guava:33.6.0-android"
58
58
  implementation fileTree(dir: 'libs', include: ['*.jar'])
59
59
  implementation project(':capacitor-android')
60
60
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
@@ -65,11 +65,11 @@ dependencies {
65
65
  testImplementation "junit:junit:$junitVersion"
66
66
  testImplementation 'org.mockito:mockito-core:5.23.0'
67
67
  testImplementation 'org.json:json:20250517'
68
- testImplementation 'org.robolectric:robolectric:4.16.1'
68
+ testImplementation 'org.robolectric:robolectric:4.17'
69
69
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
70
70
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
71
71
  implementation 'org.brotli:dec:0.1.2'
72
- implementation 'com.squareup.okhttp3:okhttp:5.4.0'
72
+ implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
73
73
  }
74
74
 
75
75
  tasks.withType(Test) {
@@ -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.14";
149
+ private final String pluginVersion = "8.51.16";
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
- 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);
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));
@@ -2529,11 +2532,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
2529
2532
  call.reject("setUpdateUrl called without url");
2530
2533
  return;
2531
2534
  }
2532
- this.updateUrl = url;
2533
2535
  if (Boolean.TRUE.equals(this.persistModifyUrl)) {
2534
2536
  this.editor.putString(UPDATE_URL_PREF_KEY, url);
2535
- this.editor.apply();
2537
+ if (!this.editor.commit()) {
2538
+ logger.error("Failed to persist updateUrl");
2539
+ call.reject("Failed to persist updateUrl");
2540
+ return;
2541
+ }
2536
2542
  }
2543
+ this.updateUrl = url;
2537
2544
  call.resolve();
2538
2545
  }
2539
2546
 
@@ -2550,11 +2557,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
2550
2557
  call.reject("setStatsUrl called without url");
2551
2558
  return;
2552
2559
  }
2553
- this.implementation.statsUrl = url;
2554
2560
  if (Boolean.TRUE.equals(this.persistModifyUrl)) {
2555
2561
  this.editor.putString(STATS_URL_PREF_KEY, url);
2556
- this.editor.apply();
2562
+ if (!this.editor.commit()) {
2563
+ logger.error("Failed to persist statsUrl");
2564
+ call.reject("Failed to persist statsUrl");
2565
+ return;
2566
+ }
2557
2567
  }
2568
+ this.implementation.statsUrl = url;
2558
2569
  call.resolve();
2559
2570
  }
2560
2571
 
@@ -2571,11 +2582,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
2571
2582
  call.reject("setChannelUrl called without url");
2572
2583
  return;
2573
2584
  }
2574
- this.implementation.channelUrl = url;
2575
2585
  if (Boolean.TRUE.equals(this.persistModifyUrl)) {
2576
2586
  this.editor.putString(CHANNEL_URL_PREF_KEY, url);
2577
- this.editor.apply();
2587
+ if (!this.editor.commit()) {
2588
+ logger.error("Failed to persist channelUrl");
2589
+ call.reject("Failed to persist channelUrl");
2590
+ return;
2591
+ }
2578
2592
  }
2593
+ this.implementation.channelUrl = url;
2579
2594
  call.resolve();
2580
2595
  }
2581
2596
 
@@ -4230,12 +4245,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
4230
4245
  if (this.shouldBlockAutoUpdateForPreviewSession()) {
4231
4246
  return "preview_session";
4232
4247
  }
4233
- if (this.isDownloadStuckOrTimedOut()) {
4234
- logger.info("Download already in progress, skipping duplicate download request");
4235
- return "already_running";
4248
+ synchronized (this) {
4249
+ final Thread previousTask = this.backgroundDownloadTask;
4250
+ final Thread task = this.backgroundDownload();
4251
+ if (task == null) {
4252
+ return "unavailable";
4253
+ }
4254
+ if (previousTask != null && previousTask == task) {
4255
+ logger.info("Download already in progress, skipping duplicate download request");
4256
+ return "already_running";
4257
+ }
4258
+ return "queued";
4236
4259
  }
4237
- this.backgroundDownload();
4238
- return "queued";
4239
4260
  }
4240
4261
 
4241
4262
  @PluginMethod
@@ -4748,10 +4769,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
4748
4769
  return true;
4749
4770
  }
4750
4771
 
4751
- private Thread backgroundDownload() {
4772
+ private synchronized Thread backgroundDownload() {
4752
4773
  if (this.shouldBlockAutoUpdateForPreviewSession()) {
4753
4774
  return null;
4754
4775
  }
4776
+ if (this.isDownloadStuckOrTimedOut()) {
4777
+ logger.info("Download already in progress, skipping duplicate download request");
4778
+ return this.backgroundDownloadTask;
4779
+ }
4755
4780
  final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
4756
4781
  final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
4757
4782
  final String messageUpdate = initialDirectUpdateAllowed
@@ -4760,8 +4785,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
4760
4785
  ? "Update will occur next time app moves to background."
4761
4786
  : "Update will be downloaded and made available.";
4762
4787
  Thread newTask = startNewThread(() -> {
4763
- // Wait for cleanup to complete before starting download
4764
- waitForCleanupIfNeeded();
4765
4788
  if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
4766
4789
  CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
4767
4790
  return;
@@ -4774,7 +4797,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
4774
4797
  return;
4775
4798
  }
4776
4799
  JSObject jsRes = InternalUtils.mapToJSObject(res);
4777
- final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
4800
+ final BundleInfo currentBeforeCleanup = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
4778
4801
 
4779
4802
  // Handle network errors and other failures first
4780
4803
  if (jsRes.has("error") || jsRes.has("kind")) {
@@ -4782,8 +4805,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
4782
4805
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
4783
4806
  int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
4784
4807
  String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
4785
- String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
4786
- CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(kind, error, errorMessage, statusCode, latestVersion, current);
4808
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : currentBeforeCleanup.getVersionName();
4809
+ CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
4810
+ kind,
4811
+ error,
4812
+ errorMessage,
4813
+ statusCode,
4814
+ latestVersion,
4815
+ currentBeforeCleanup
4816
+ );
4787
4817
  CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(
4788
4818
  jsRes,
4789
4819
  jsRes.has("version") ? jsRes.getString("version") : ""
@@ -4803,7 +4833,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
4803
4833
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
4804
4834
  errorMessage,
4805
4835
  latestVersion,
4806
- current,
4836
+ currentBeforeCleanup,
4807
4837
  isFailure,
4808
4838
  plannedDirectUpdate,
4809
4839
  "download_fail",
@@ -4813,6 +4843,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
4813
4843
  return;
4814
4844
  }
4815
4845
  try {
4846
+ // File mutations wait here. getLatest already ran in parallel with cleanup.
4847
+ waitForCleanupIfNeeded();
4848
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
4816
4849
  final String latestVersionName = jsRes.getString("version");
4817
4850
 
4818
4851
  if ("builtin".equals(latestVersionName)) {
@@ -5061,8 +5094,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
5061
5094
  logger.error("error in update check " + e.getMessage());
5062
5095
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
5063
5096
  "Error in update check",
5064
- current.getVersionName(),
5065
- current,
5097
+ currentBeforeCleanup.getVersionName(),
5098
+ currentBeforeCleanup,
5066
5099
  true,
5067
5100
  plannedDirectUpdate
5068
5101
  );
@@ -23,8 +23,6 @@ import androidx.work.OneTimeWorkRequest;
23
23
  import androidx.work.PeriodicWorkRequest;
24
24
  import androidx.work.WorkInfo;
25
25
  import androidx.work.WorkManager;
26
- import com.google.common.util.concurrent.Futures;
27
- import com.google.common.util.concurrent.ListenableFuture;
28
26
  import java.io.BufferedInputStream;
29
27
  import java.io.BufferedReader;
30
28
  import java.io.File;
@@ -315,14 +313,17 @@ public class CapgoUpdater {
315
313
  return this.cachedKeyId;
316
314
  }
317
315
 
318
- private File unzip(final String id, final File zipFile, final String dest) throws IOException {
316
+ File unzip(final String id, final File zipFile, final String dest) throws IOException {
317
+ return unzip(id, zipFile, dest, CryptoCipher.ioBufferBytes());
318
+ }
319
+
320
+ File unzip(final String id, final File zipFile, final String dest, final int bufferSize) throws IOException {
319
321
  final File targetDirectory = new File(this.documentsDir, dest);
320
322
  try (
321
323
  final BufferedInputStream bis = new BufferedInputStream(new FileInputStream(zipFile));
322
324
  final ZipInputStream zis = new ZipInputStream(bis)
323
325
  ) {
324
326
  int count;
325
- final int bufferSize = 8192;
326
327
  final byte[] buffer = new byte[bufferSize];
327
328
  final long lengthTotal = zipFile.length();
328
329
  long lengthRead = bufferSize;
@@ -760,6 +761,13 @@ public class CapgoUpdater {
760
761
  }
761
762
  });
762
763
  break;
764
+ case CANCELLED:
765
+ DataManager.getInstance().clearManifest(id);
766
+ CompletableFuture<BundleInfo> cancelledFuture = downloadFutures.remove(id);
767
+ if (cancelledFuture != null) {
768
+ cancelledFuture.cancel(true);
769
+ }
770
+ break;
763
771
  }
764
772
  });
765
773
  });
@@ -781,6 +789,10 @@ public class CapgoUpdater {
781
789
  }
782
790
  observeWorkProgress(this.activity, id, setNext);
783
791
 
792
+ if (manifest != null) {
793
+ DataManager.getInstance().setManifest(id, manifest);
794
+ }
795
+
784
796
  DownloadWorkerManager.enqueueDownload(
785
797
  this.activity,
786
798
  url,
@@ -805,10 +817,6 @@ public class CapgoUpdater {
805
817
  this.customId,
806
818
  this.defaultChannel
807
819
  );
808
-
809
- if (manifest != null) {
810
- DataManager.getInstance().setManifest(manifest);
811
- }
812
820
  }
813
821
 
814
822
  public Boolean finishDownload(
@@ -1398,7 +1406,10 @@ public class CapgoUpdater {
1398
1406
  BundleInfo existingBundle = this.getBundleInfoByName(version);
1399
1407
  if (existingBundle != null && existingBundle.isErrorStatus()) {
1400
1408
  // Cancel the failed download and allow retry
1401
- DownloadWorkerManager.cancelVersionDownload(this.activity, version);
1409
+ if (!DownloadWorkerManager.cancelVersionDownloadAndAwait(this.activity, version)) {
1410
+ logger.error("Failed to cancel previous download before retry");
1411
+ return;
1412
+ }
1402
1413
  logger.info("Retrying failed download for version: " + version);
1403
1414
  } else {
1404
1415
  logger.info("Version already downloading: " + version);
@@ -1419,7 +1430,9 @@ public class CapgoUpdater {
1419
1430
  BundleInfo existingBundle = this.getBundleInfoByName(version);
1420
1431
  if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted() || existingBundle.isDeleting())) {
1421
1432
  logger.info("Found existing failed bundle for version " + version + ", deleting before retry");
1422
- this.delete(existingBundle.getId(), true);
1433
+ if (!Boolean.TRUE.equals(this.delete(existingBundle.getId(), true))) {
1434
+ throw new IOException("Failed to delete existing bundle before retry");
1435
+ }
1423
1436
  }
1424
1437
 
1425
1438
  final String id = this.randomString();
@@ -1472,7 +1485,9 @@ public class CapgoUpdater {
1472
1485
  BundleInfo existingBundle = this.getBundleInfoByName(version);
1473
1486
  if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted() || existingBundle.isDeleting())) {
1474
1487
  logger.info("Found existing failed bundle for version " + version + ", deleting before retry");
1475
- this.delete(existingBundle.getId(), true);
1488
+ if (!Boolean.TRUE.equals(this.delete(existingBundle.getId(), true))) {
1489
+ throw new IOException("Failed to delete existing bundle before retry");
1490
+ }
1476
1491
  }
1477
1492
 
1478
1493
  final String id = this.randomString();
@@ -1587,7 +1602,10 @@ public class CapgoUpdater {
1587
1602
 
1588
1603
  // Cancel download for this version if active
1589
1604
  if (cancelActiveDownload && this.activity != null) {
1590
- DownloadWorkerManager.cancelVersionDownload(this.activity, deleted.getVersionName());
1605
+ if (!DownloadWorkerManager.cancelVersionDownloadAndAwait(this.activity, deleted.getVersionName())) {
1606
+ logger.error("Failed to cancel active download before delete");
1607
+ return false;
1608
+ }
1591
1609
  }
1592
1610
 
1593
1611
  if (bundle.exists()) {
@@ -1922,32 +1940,35 @@ public class CapgoUpdater {
1922
1940
  !fallbackIsPreviewFallback &&
1923
1941
  !previousIsNext;
1924
1942
  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
1943
  if (!this.saveBundleInfo(previousFallbackId, fallback.setStatus(BundleStatus.DELETING))) {
1932
1944
  logger.error("Failed to persist DELETING for previous bundle; queueing durable retry");
1933
1945
  logger.debug("Bundle ID: " + previousFallbackId);
1934
1946
  this.enqueuePendingDelete(previousFallbackId);
1935
1947
  }
1936
1948
  }
1949
+ boolean deletePreviousAsync = shouldDeletePrevious;
1937
1950
  this.setFallbackBundle(bundle);
1938
- if (shouldDeletePrevious) {
1939
- new Thread(() -> {
1951
+ if (deletePreviousAsync) {
1952
+ final String asyncPreviousFallbackId = previousFallbackId;
1953
+ final String asyncPreviousFallbackVersion = previousFallbackVersion;
1954
+ io.execute(() -> {
1955
+ if (this.activity != null) {
1956
+ if (!DownloadWorkerManager.cancelVersionDownloadAndAwait(this.activity, asyncPreviousFallbackVersion)) {
1957
+ logger.error("Failed to cancel previous version download before delete");
1958
+ return;
1959
+ }
1960
+ }
1940
1961
  try {
1941
- final Boolean res = this.delete(previousFallbackId, true, false);
1962
+ final Boolean res = this.delete(asyncPreviousFallbackId, true, false);
1942
1963
  if (Boolean.TRUE.equals(res)) {
1943
- logger.info("Deleted previous bundle: " + previousFallbackVersion);
1964
+ logger.info("Deleted previous bundle: " + asyncPreviousFallbackVersion);
1944
1965
  } else {
1945
- logger.debug("Previous bundle delete incomplete, will retry: " + previousFallbackId);
1966
+ logger.debug("Previous bundle delete incomplete, will retry: " + asyncPreviousFallbackId);
1946
1967
  }
1947
1968
  } catch (final IOException e) {
1948
- logger.error("Failed to delete previous bundle: " + previousFallbackId + " " + e.getMessage());
1969
+ logger.error("Failed to delete previous bundle: " + asyncPreviousFallbackId + " " + e.getMessage());
1949
1970
  }
1950
- }, "CapgoUpdater-autoDeletePrevious").start();
1971
+ });
1951
1972
  }
1952
1973
  }
1953
1974
 
@@ -2878,6 +2899,11 @@ public class CapgoUpdater {
2878
2899
  statsQueue.add(new QueuedStatsEvent(arr.getJSONObject(i), null));
2879
2900
  }
2880
2901
  }
2902
+ if (backup.exists() && !backup.delete()) {
2903
+ if (logger != null) {
2904
+ logger.error("Failed to delete stats backup");
2905
+ }
2906
+ }
2881
2907
  if (!statsQueue.isEmpty()) {
2882
2908
  if (logger != null) {
2883
2909
  logger.info("Restored " + statsQueue.size() + " pending stats events");
@@ -2933,9 +2959,11 @@ public class CapgoUpdater {
2933
2959
  }
2934
2960
  try {
2935
2961
  if (arr.length() == 0) {
2936
- if (file.exists() && !file.delete()) {
2962
+ writeFileAtomically(file, "[]".getBytes(StandardCharsets.UTF_8));
2963
+ File backup = new File(file.getAbsolutePath() + ".bak");
2964
+ if (backup.exists() && !backup.delete()) {
2937
2965
  if (logger != null) {
2938
- logger.error("Failed to delete empty stats queue file");
2966
+ logger.error("Failed to delete empty stats backup");
2939
2967
  }
2940
2968
  }
2941
2969
  return;
@@ -2966,7 +2994,7 @@ public class CapgoUpdater {
2966
2994
  }
2967
2995
  }
2968
2996
 
2969
- private static void writeFileAtomically(final File file, final byte[] bytes) throws IOException {
2997
+ private void writeFileAtomically(final File file, final byte[] bytes) throws IOException {
2970
2998
  final File tmp = new File(file.getAbsolutePath() + ".tmp");
2971
2999
  File backup = null;
2972
3000
  try {
@@ -2988,7 +3016,9 @@ public class CapgoUpdater {
2988
3016
  }
2989
3017
  if (tmp.renameTo(file)) {
2990
3018
  if (backup != null && backup.exists() && !backup.delete()) {
2991
- backup.deleteOnExit();
3019
+ if (logger != null) {
3020
+ logger.error("Failed to delete stats backup");
3021
+ }
2992
3022
  }
2993
3023
  backup = null;
2994
3024
  return;
@@ -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
  }
@@ -75,6 +75,17 @@ public class DelayUpdateUtils {
75
75
  ", longValue: " +
76
76
  longValue
77
77
  );
78
+ } else {
79
+ delayConditionListToKeep.add(condition);
80
+ logger.info(
81
+ "Background delay (value: " +
82
+ value +
83
+ ") condition kept at index " +
84
+ index +
85
+ " (source: " +
86
+ source.toString() +
87
+ ")"
88
+ );
78
89
  }
79
90
  } else {
80
91
  delayConditionListToKeep.add(condition);