@capgo/capacitor-updater 7.20.1 → 7.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -255,6 +255,7 @@ CapacitorUpdater can be configured with these options:
255
255
  | **`localApiFiles`** | <code>string</code> | Configure the CLI to use a local file api for testing. | <code>undefined</code> | 6.3.3 |
256
256
  | **`allowModifyUrl`** | <code>boolean</code> | Allow the plugin to modify the updateUrl, statsUrl and channelUrl dynamically from the JavaScript side. | <code>false</code> | 5.4.0 |
257
257
  | **`allowModifyAppId`** | <code>boolean</code> | Allow the plugin to modify the appId dynamically from the JavaScript side. | <code>false</code> | 7.14.0 |
258
+ | **`allowManualBundleError`** | <code>boolean</code> | Allow marking bundles as errored from JavaScript while using manual update flows. When enabled, {@link CapacitorUpdaterPlugin.setBundleError} can change a bundle status to `error`. | <code>false</code> | 7.20.0 |
258
259
  | **`persistCustomId`** | <code>boolean</code> | Persist the customId set through {@link CapacitorUpdaterPlugin.setCustomId} across app restarts. Only available for Android and iOS. | <code>false (will be true by default in a future major release v8.x.x)</code> | 7.17.3 |
259
260
  | **`persistModifyUrl`** | <code>boolean</code> | Persist the updateUrl, statsUrl and channelUrl set through {@link CapacitorUpdaterPlugin.setUpdateUrl}, {@link CapacitorUpdaterPlugin.setStatsUrl} and {@link CapacitorUpdaterPlugin.setChannelUrl} across app restarts. Only available for Android and iOS. | <code>false</code> | 7.20.0 |
260
261
  | **`defaultChannel`** | <code>string</code> | Set the default channel for the app in the config. Case sensitive. This will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud. This requires the channel to allow devices to self dissociate/associate in the channel settings. https://capgo.app/docs/public-api/channels/#channel-configuration-options | <code>undefined</code> | 5.5.0 |
@@ -296,6 +297,7 @@ In `capacitor.config.json`:
296
297
  "localApiFiles": undefined,
297
298
  "allowModifyUrl": undefined,
298
299
  "allowModifyAppId": undefined,
300
+ "allowManualBundleError": undefined,
299
301
  "persistCustomId": undefined,
300
302
  "persistModifyUrl": undefined,
301
303
  "defaultChannel": undefined,
@@ -343,6 +345,7 @@ const config: CapacitorConfig = {
343
345
  localApiFiles: undefined,
344
346
  allowModifyUrl: undefined,
345
347
  allowModifyAppId: undefined,
348
+ allowManualBundleError: undefined,
346
349
  persistCustomId: undefined,
347
350
  persistModifyUrl: undefined,
348
351
  defaultChannel: undefined,
@@ -371,6 +374,7 @@ export default config;
371
374
  * [`next(...)`](#next)
372
375
  * [`set(...)`](#set)
373
376
  * [`delete(...)`](#delete)
377
+ * [`setBundleError(...)`](#setbundleerror)
374
378
  * [`list(...)`](#list)
375
379
  * [`reset(...)`](#reset)
376
380
  * [`current()`](#current)
@@ -392,6 +396,7 @@ export default config;
392
396
  * [`addListener('noNeedUpdate', ...)`](#addlistenernoneedupdate-)
393
397
  * [`addListener('updateAvailable', ...)`](#addlistenerupdateavailable-)
394
398
  * [`addListener('downloadComplete', ...)`](#addlistenerdownloadcomplete-)
399
+ * [`addListener('breakingAvailable', ...)`](#addlistenerbreakingavailable-)
395
400
  * [`addListener('majorAvailable', ...)`](#addlistenermajoravailable-)
396
401
  * [`addListener('updateFailed', ...)`](#addlistenerupdatefailed-)
397
402
  * [`addListener('downloadFailed', ...)`](#addlistenerdownloadfailed-)
@@ -399,6 +404,7 @@ export default config;
399
404
  * [`addListener('appReady', ...)`](#addlistenerappready-)
400
405
  * [`isAutoUpdateAvailable()`](#isautoupdateavailable)
401
406
  * [`getNextBundle()`](#getnextbundle)
407
+ * [`getFailedUpdate()`](#getfailedupdate)
402
408
  * [`setShakeMenu(...)`](#setshakemenu)
403
409
  * [`isShakeMenuEnabled()`](#isshakemenuenabled)
404
410
  * [`getAppId()`](#getappid)
@@ -541,6 +547,25 @@ Deletes the specified bundle from the native app storage. Use with {@link list}
541
547
  --------------------
542
548
 
543
549
 
550
+ ### setBundleError(...)
551
+
552
+ ```typescript
553
+ setBundleError(options: BundleId) => Promise<BundleInfo>
554
+ ```
555
+
556
+ Mark an installed bundle as errored. Only available when {@link PluginsConfig.CapacitorUpdater.allowManualBundleError} is true.
557
+
558
+ | Param | Type | Description |
559
+ | ------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------- |
560
+ | **`options`** | <code><a href="#bundleid">BundleId</a></code> | A {@link <a href="#bundleid">BundleId</a>} object containing the bundle id to mark as errored. |
561
+
562
+ **Returns:** <code>Promise&lt;<a href="#bundleinfo">BundleInfo</a>&gt;</code>
563
+
564
+ **Since:** 7.20.0
565
+
566
+ --------------------
567
+
568
+
544
569
  ### list(...)
545
570
 
546
571
  ```typescript
@@ -890,6 +915,27 @@ Listen for downloadComplete events.
890
915
  --------------------
891
916
 
892
917
 
918
+ ### addListener('breakingAvailable', ...)
919
+
920
+ ```typescript
921
+ addListener(eventName: 'breakingAvailable', listenerFunc: (state: BreakingAvailableEvent) => void) => Promise<PluginListenerHandle>
922
+ ```
923
+
924
+ Listen for breaking update events when the backend flags an update as incompatible with the current app.
925
+ Emits the same payload as the legacy `majorAvailable` listener.
926
+
927
+ | Param | Type |
928
+ | ------------------ | --------------------------------------------------------------------------------------- |
929
+ | **`eventName`** | <code>'breakingAvailable'</code> |
930
+ | **`listenerFunc`** | <code>(state: <a href="#majoravailableevent">MajorAvailableEvent</a>) =&gt; void</code> |
931
+
932
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
933
+
934
+ **Since:** 7.22.0
935
+
936
+ --------------------
937
+
938
+
893
939
  ### addListener('majorAvailable', ...)
894
940
 
895
941
  ```typescript
@@ -1019,6 +1065,21 @@ Returns null if no next bundle is set.
1019
1065
  --------------------
1020
1066
 
1021
1067
 
1068
+ ### getFailedUpdate()
1069
+
1070
+ ```typescript
1071
+ getFailedUpdate() => Promise<UpdateFailedEvent | null>
1072
+ ```
1073
+
1074
+ Get the most recent update that failed to install, if any. The stored value is cleared after it is retrieved once.
1075
+
1076
+ **Returns:** <code>Promise&lt;<a href="#updatefailedevent">UpdateFailedEvent</a> | null&gt;</code>
1077
+
1078
+ **Since:** 7.22.0
1079
+
1080
+ --------------------
1081
+
1082
+
1022
1083
  ### setShakeMenu(...)
1023
1084
 
1024
1085
  ```typescript
@@ -1201,17 +1262,18 @@ If you don't use backend, you need to provide the URL and version of the bundle.
1201
1262
 
1202
1263
  #### LatestVersion
1203
1264
 
1204
- | Prop | Type | Description | Since |
1205
- | ---------------- | ---------------------------- | -------------------------- | ----- |
1206
- | **`version`** | <code>string</code> | Result of getLatest method | 4.0.0 |
1207
- | **`checksum`** | <code>string</code> | | 6 |
1208
- | **`major`** | <code>boolean</code> | | |
1209
- | **`message`** | <code>string</code> | | |
1210
- | **`sessionKey`** | <code>string</code> | | |
1211
- | **`error`** | <code>string</code> | | |
1212
- | **`old`** | <code>string</code> | | |
1213
- | **`url`** | <code>string</code> | | |
1214
- | **`manifest`** | <code>ManifestEntry[]</code> | | 6.1 |
1265
+ | Prop | Type | Description | Since |
1266
+ | ---------------- | ---------------------------- | -------------------------------------------------------------------- | ------ |
1267
+ | **`version`** | <code>string</code> | Result of getLatest method | 4.0.0 |
1268
+ | **`checksum`** | <code>string</code> | | 6 |
1269
+ | **`breaking`** | <code>boolean</code> | Indicates whether the update was flagged as breaking by the backend. | 7.22.0 |
1270
+ | **`major`** | <code>boolean</code> | | |
1271
+ | **`message`** | <code>string</code> | | |
1272
+ | **`sessionKey`** | <code>string</code> | | |
1273
+ | **`error`** | <code>string</code> | | |
1274
+ | **`old`** | <code>string</code> | | |
1275
+ | **`url`** | <code>string</code> | | |
1276
+ | **`manifest`** | <code>ManifestEntry[]</code> | | 6.1 |
1215
1277
 
1216
1278
 
1217
1279
  #### GetLatestOptions
@@ -1346,9 +1408,9 @@ If you don't use backend, you need to provide the URL and version of the bundle.
1346
1408
 
1347
1409
  #### MajorAvailableEvent
1348
1410
 
1349
- | Prop | Type | Description | Since |
1350
- | ------------- | ------------------- | ------------------------------------------ | ----- |
1351
- | **`version`** | <code>string</code> | Emit when a new major bundle is available. | 4.0.0 |
1411
+ | Prop | Type | Description | Since |
1412
+ | ------------- | ------------------- | ----------------------------------------- | ----- |
1413
+ | **`version`** | <code>string</code> | Emit when a breaking update is available. | 4.0.0 |
1352
1414
 
1353
1415
 
1354
1416
  #### UpdateFailedEvent
@@ -1425,6 +1487,13 @@ error: The bundle has failed to download.
1425
1487
 
1426
1488
  <code>'background' | 'kill' | 'nativeVersion' | 'date'</code>
1427
1489
 
1490
+
1491
+ #### BreakingAvailableEvent
1492
+
1493
+ Payload emitted by {@link CapacitorUpdaterPlugin.addListener} with `breakingAvailable`.
1494
+
1495
+ <code><a href="#majoravailableevent">MajorAvailableEvent</a></code>
1496
+
1428
1497
  </docgen-api>
1429
1498
 
1430
1499
  ### Listen to download events
@@ -68,8 +68,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
68
68
  private static final String UPDATE_URL_PREF_KEY = "CapacitorUpdater.updateUrl";
69
69
  private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
70
70
  private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
71
+ private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
72
+ private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
71
73
 
72
- private final String PLUGIN_VERSION = "7.20.1";
74
+ private final String PLUGIN_VERSION = "7.22.0";
73
75
  private static final String DELAY_CONDITION_PREFERENCES = "";
74
76
 
75
77
  private SharedPreferences.Editor editor;
@@ -97,6 +99,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
97
99
  private String directUpdateMode = "false";
98
100
  private Boolean wasRecentlyInstalledOrUpdated = false;
99
101
  Boolean shakeMenuEnabled = false;
102
+ private Boolean allowManualBundleError = false;
100
103
 
101
104
  private Boolean isPreviousMainActivity = true;
102
105
 
@@ -115,6 +118,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
115
118
  private FrameLayout splashscreenLoaderOverlay;
116
119
  private Runnable splashscreenTimeoutRunnable;
117
120
 
121
+ private void notifyBreakingEvents(final String version) {
122
+ if (version == null || version.isEmpty()) {
123
+ return;
124
+ }
125
+ for (final String eventName : BREAKING_EVENT_NAMES) {
126
+ final JSObject payload = new JSObject();
127
+ payload.put("version", version);
128
+ CapacitorUpdaterPlugin.this.notifyListeners(eventName, payload);
129
+ }
130
+ }
131
+
118
132
  private JSObject mapToJSObject(Map<String, Object> map) {
119
133
  JSObject jsObject = new JSObject();
120
134
  for (Map.Entry<String, Object> entry : map.entrySet()) {
@@ -123,6 +137,37 @@ public class CapacitorUpdaterPlugin extends Plugin {
123
137
  return jsObject;
124
138
  }
125
139
 
140
+ private void persistLastFailedBundle(BundleInfo bundle) {
141
+ if (this.prefs == null) {
142
+ return;
143
+ }
144
+ final SharedPreferences.Editor localEditor = this.prefs.edit();
145
+ if (bundle == null) {
146
+ localEditor.remove(LAST_FAILED_BUNDLE_PREF_KEY);
147
+ } else {
148
+ final JSONObject json = new JSONObject(bundle.toJSONMap());
149
+ localEditor.putString(LAST_FAILED_BUNDLE_PREF_KEY, json.toString());
150
+ }
151
+ localEditor.apply();
152
+ }
153
+
154
+ private BundleInfo readLastFailedBundle() {
155
+ if (this.prefs == null) {
156
+ return null;
157
+ }
158
+ final String raw = this.prefs.getString(LAST_FAILED_BUNDLE_PREF_KEY, null);
159
+ if (raw == null || raw.trim().isEmpty()) {
160
+ return null;
161
+ }
162
+ try {
163
+ return BundleInfo.fromJSON(raw);
164
+ } catch (final JSONException e) {
165
+ logger.error("Failed to parse failed bundle info: " + e.getMessage());
166
+ this.persistLastFailedBundle(null);
167
+ return null;
168
+ }
169
+ }
170
+
126
171
  public Thread startNewThread(final Runnable function, Number waitTime) {
127
172
  Thread bgTask = new Thread(() -> {
128
173
  try {
@@ -153,17 +198,23 @@ public class CapacitorUpdaterPlugin extends Plugin {
153
198
  this.implementation = new CapgoUpdater(logger) {
154
199
  @Override
155
200
  public void notifyDownload(final String id, final int percent) {
156
- CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
201
+ activity.runOnUiThread(() -> {
202
+ CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
203
+ });
157
204
  }
158
205
 
159
206
  @Override
160
207
  public void directUpdateFinish(final BundleInfo latest) {
161
- CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
208
+ activity.runOnUiThread(() -> {
209
+ CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
210
+ });
162
211
  }
163
212
 
164
213
  @Override
165
214
  public void notifyListeners(final String id, final Map<String, Object> res) {
166
- CapacitorUpdaterPlugin.this.notifyListeners(id, CapacitorUpdaterPlugin.this.mapToJSObject(res));
215
+ activity.runOnUiThread(() -> {
216
+ CapacitorUpdaterPlugin.this.notifyListeners(id, CapacitorUpdaterPlugin.this.mapToJSObject(res));
217
+ });
167
218
  }
168
219
  };
169
220
  final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
@@ -290,6 +341,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
290
341
  this.appReadyTimeout = this.getConfig().getInt("appReadyTimeout", 10000);
291
342
  this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
292
343
  this.syncKeepUrlPathFlag(this.keepUrlPathAfterReload);
344
+ this.allowManualBundleError = this.getConfig().getBoolean("allowManualBundleError", false);
293
345
  this.autoSplashscreen = this.getConfig().getBoolean("autoSplashscreen", false);
294
346
  this.autoSplashscreenLoader = this.getConfig().getBoolean("autoSplashscreenLoader", false);
295
347
  int splashscreenTimeoutValue = this.getConfig().getInt("autoSplashscreenTimeout", 10000);
@@ -586,30 +638,36 @@ public class CapacitorUpdaterPlugin extends Plugin {
586
638
  }
587
639
 
588
640
  private void cleanupObsoleteVersions() {
589
- final String previous = this.prefs.getString("LatestNativeBuildVersion", "");
590
- if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
591
- logger.info("New native build version detected: " + this.currentBuildVersion);
592
- this.implementation.reset(true);
593
- final List<BundleInfo> installed = this.implementation.list(false);
594
- for (final BundleInfo bundle : installed) {
595
- try {
596
- logger.info("Deleting obsolete bundle: " + bundle.getId());
597
- this.implementation.delete(bundle.getId());
598
- } catch (final Exception e) {
599
- logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
600
- }
601
- }
602
- final List<BundleInfo> storedBundles = this.implementation.list(true);
603
- final Set<String> allowedIds = new HashSet<>();
604
- for (final BundleInfo info : storedBundles) {
605
- if (info != null && info.getId() != null && !info.getId().isEmpty()) {
606
- allowedIds.add(info.getId());
641
+ startNewThread(() -> {
642
+ try {
643
+ final String previous = this.prefs.getString("LatestNativeBuildVersion", "");
644
+ if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
645
+ logger.info("New native build version detected: " + this.currentBuildVersion);
646
+ this.implementation.reset(true);
647
+ final List<BundleInfo> installed = this.implementation.list(false);
648
+ for (final BundleInfo bundle : installed) {
649
+ try {
650
+ logger.info("Deleting obsolete bundle: " + bundle.getId());
651
+ this.implementation.delete(bundle.getId());
652
+ } catch (final Exception e) {
653
+ logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
654
+ }
655
+ }
656
+ final List<BundleInfo> storedBundles = this.implementation.list(true);
657
+ final Set<String> allowedIds = new HashSet<>();
658
+ for (final BundleInfo info : storedBundles) {
659
+ if (info != null && info.getId() != null && !info.getId().isEmpty()) {
660
+ allowedIds.add(info.getId());
661
+ }
662
+ }
663
+ this.implementation.cleanupDownloadDirectories(allowedIds);
607
664
  }
665
+ this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
666
+ this.editor.apply();
667
+ } catch (Exception e) {
668
+ logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
608
669
  }
609
- this.implementation.cleanupDownloadDirectories(allowedIds);
610
- }
611
- this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
612
- this.editor.apply();
670
+ });
613
671
  }
614
672
 
615
673
  public void notifyDownload(final String id, final int percent) {
@@ -1041,6 +1099,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
1041
1099
  } else {
1042
1100
  this.bridge.setServerBasePath(path);
1043
1101
  }
1102
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1103
+ this.bridge.getWebView().post(() -> {
1104
+ if (this.bridge.getWebView() != null) {
1105
+ this.bridge.getWebView().loadUrl(this.bridge.getAppUrl());
1106
+ if (!this.keepUrlPathAfterReload) {
1107
+ this.bridge.getWebView().clearHistory();
1108
+ }
1109
+ }
1110
+ });
1111
+ }
1044
1112
  }
1045
1113
 
1046
1114
  this.checkAppReady();
@@ -1140,6 +1208,44 @@ public class CapacitorUpdaterPlugin extends Plugin {
1140
1208
  }
1141
1209
  }
1142
1210
 
1211
+ @PluginMethod
1212
+ public void setBundleError(final PluginCall call) {
1213
+ if (!Boolean.TRUE.equals(this.allowManualBundleError)) {
1214
+ logger.error("setBundleError called without allowManualBundleError");
1215
+ call.reject("setBundleError not allowed. Set allowManualBundleError to true in your config to enable it.");
1216
+ return;
1217
+ }
1218
+ final String id = call.getString("id");
1219
+ if (id == null) {
1220
+ logger.error("setBundleError called without id");
1221
+ call.reject("setBundleError called without id");
1222
+ return;
1223
+ }
1224
+ try {
1225
+ final BundleInfo bundle = this.implementation.getBundleInfo(id);
1226
+ if (bundle == null || bundle.isUnknown()) {
1227
+ logger.error("setBundleError called with unknown bundle " + id);
1228
+ call.reject("Bundle " + id + " does not exist");
1229
+ return;
1230
+ }
1231
+ if (bundle.isBuiltin()) {
1232
+ logger.error("setBundleError called on builtin bundle");
1233
+ call.reject("Cannot set builtin bundle to error state");
1234
+ return;
1235
+ }
1236
+ if (Boolean.TRUE.equals(this.autoUpdate)) {
1237
+ logger.warn("setBundleError used while autoUpdate is enabled; this method is intended for manual mode");
1238
+ }
1239
+ this.implementation.setError(bundle);
1240
+ final JSObject ret = new JSObject();
1241
+ ret.put("bundle", mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1242
+ call.resolve(ret);
1243
+ } catch (final Exception e) {
1244
+ logger.error("Could not set bundle error for id " + id + " " + e.getMessage());
1245
+ call.reject("Could not set bundle error for id " + id, e);
1246
+ }
1247
+ }
1248
+
1143
1249
  @PluginMethod
1144
1250
  public void list(final PluginCall call) {
1145
1251
  try {
@@ -1236,6 +1342,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
1236
1342
  }
1237
1343
  }
1238
1344
 
1345
+ @PluginMethod
1346
+ public void getFailedUpdate(final PluginCall call) {
1347
+ try {
1348
+ final BundleInfo bundle = this.readLastFailedBundle();
1349
+ if (bundle == null || bundle.isUnknown()) {
1350
+ call.resolve(null);
1351
+ return;
1352
+ }
1353
+
1354
+ this.persistLastFailedBundle(null);
1355
+
1356
+ final JSObject ret = new JSObject();
1357
+ ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
1358
+ call.resolve(ret);
1359
+ } catch (final Exception e) {
1360
+ logger.error("Could not get failed update " + e.getMessage());
1361
+ call.reject("Could not get failed update", e);
1362
+ }
1363
+ }
1364
+
1239
1365
  public void checkForUpdateAfterDelay() {
1240
1366
  if (this.periodCheckDelay == 0 || !this._isAutoUpdateEnabled()) {
1241
1367
  return;
@@ -1480,10 +1606,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1480
1606
  try {
1481
1607
  if (jsRes.has("message")) {
1482
1608
  logger.info("API message: " + jsRes.get("message"));
1483
- if (jsRes.has("major") && jsRes.getBoolean("major") && jsRes.has("version")) {
1484
- final JSObject majorAvailable = new JSObject();
1485
- majorAvailable.put("version", jsRes.getString("version"));
1486
- CapacitorUpdaterPlugin.this.notifyListeners("majorAvailable", majorAvailable);
1609
+ if (jsRes.has("version") && (jsRes.has("breaking") || jsRes.has("major"))) {
1610
+ CapacitorUpdaterPlugin.this.notifyBreakingEvents(jsRes.getString("version"));
1487
1611
  }
1488
1612
  String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
1489
1613
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
@@ -1738,6 +1862,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1738
1862
  logger.info("Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
1739
1863
  final JSObject ret = new JSObject();
1740
1864
  ret.put("bundle", mapToJSObject(current.toJSONMap()));
1865
+ this.persistLastFailedBundle(current);
1741
1866
  this.notifyListeners("updateFailed", ret);
1742
1867
  this.implementation.sendStats("update_fail", current.getVersionName());
1743
1868
  this.implementation.setError(current);
@@ -35,6 +35,8 @@ import java.util.Objects;
35
35
  import java.util.Set;
36
36
  import java.util.concurrent.CompletableFuture;
37
37
  import java.util.concurrent.ConcurrentHashMap;
38
+ import java.util.concurrent.ExecutorService;
39
+ import java.util.concurrent.Executors;
38
40
  import java.util.concurrent.TimeUnit;
39
41
  import java.util.zip.ZipEntry;
40
42
  import java.util.zip.ZipInputStream;
@@ -80,6 +82,7 @@ public class CapgoUpdater {
80
82
  public int timeout = 20000;
81
83
 
82
84
  private final Map<String, CompletableFuture<BundleInfo>> downloadFutures = new ConcurrentHashMap<>();
85
+ private final ExecutorService io = Executors.newSingleThreadExecutor();
83
86
 
84
87
  public CapgoUpdater(Logger logger) {
85
88
  this.logger = logger;
@@ -245,60 +248,71 @@ public class CapgoUpdater {
245
248
  String checksum = outputData.getString(DownloadService.CHECKSUM);
246
249
  boolean isManifest = outputData.getBoolean(DownloadService.IS_MANIFEST, false);
247
250
 
248
- boolean success = finishDownload(id, dest, version, sessionKey, checksum, true, isManifest);
249
- BundleInfo resultBundle;
250
- if (!success) {
251
- logger.error("Finish download failed: " + version);
252
- resultBundle = new BundleInfo(id, version, BundleStatus.ERROR, new Date(System.currentTimeMillis()), "");
253
- saveBundleInfo(id, resultBundle);
254
- // Cleanup download tracking
255
- DownloadWorkerManager.cancelBundleDownload(activity, id, version);
256
- Map<String, Object> ret = new HashMap<>();
257
- ret.put("version", version);
258
- ret.put("error", "finish_download_fail");
259
- sendStats("finish_download_fail", version);
260
- notifyListeners("downloadFailed", ret);
261
- } else {
262
- // Successful download - cleanup tracking
263
- DownloadWorkerManager.cancelBundleDownload(activity, id, version);
264
- resultBundle = getBundleInfo(id);
265
- }
251
+ io.execute(() -> {
252
+ boolean success = finishDownload(id, dest, version, sessionKey, checksum, true, isManifest);
253
+ BundleInfo resultBundle;
254
+ if (!success) {
255
+ logger.error("Finish download failed: " + version);
256
+ resultBundle = new BundleInfo(
257
+ id,
258
+ version,
259
+ BundleStatus.ERROR,
260
+ new Date(System.currentTimeMillis()),
261
+ ""
262
+ );
263
+ saveBundleInfo(id, resultBundle);
264
+ // Cleanup download tracking
265
+ DownloadWorkerManager.cancelBundleDownload(activity, id, version);
266
+ Map<String, Object> ret = new HashMap<>();
267
+ ret.put("version", version);
268
+ ret.put("error", "finish_download_fail");
269
+ sendStats("finish_download_fail", version);
270
+ notifyListeners("downloadFailed", ret);
271
+ } else {
272
+ // Successful download - cleanup tracking
273
+ DownloadWorkerManager.cancelBundleDownload(activity, id, version);
274
+ resultBundle = getBundleInfo(id);
275
+ }
266
276
 
267
- // Complete the future if it exists
268
- CompletableFuture<BundleInfo> future = downloadFutures.remove(id);
269
- if (future != null) {
270
- future.complete(resultBundle);
271
- }
277
+ // Complete the future if it exists
278
+ CompletableFuture<BundleInfo> future = downloadFutures.remove(id);
279
+ if (future != null) {
280
+ future.complete(resultBundle);
281
+ }
282
+ });
272
283
  break;
273
284
  case FAILED:
274
285
  Data failedData = workInfo.getOutputData();
275
286
  String error = failedData.getString(DownloadService.ERROR);
276
287
  logger.error("Download failed: " + error + " " + workInfo.getState());
277
288
  String failedVersion = failedData.getString(DownloadService.VERSION);
278
- BundleInfo failedBundle = new BundleInfo(
279
- id,
280
- failedVersion,
281
- BundleStatus.ERROR,
282
- new Date(System.currentTimeMillis()),
283
- ""
284
- );
285
- saveBundleInfo(id, failedBundle);
286
- // Cleanup download tracking for failed downloads
287
- DownloadWorkerManager.cancelBundleDownload(activity, id, failedVersion);
288
- Map<String, Object> ret = new HashMap<>();
289
- ret.put("version", failedVersion);
290
- if ("low_mem_fail".equals(error)) {
291
- sendStats("low_mem_fail", failedVersion);
292
- }
293
- ret.put("error", error != null ? error : "download_fail");
294
- sendStats("download_fail", failedVersion);
295
- notifyListeners("downloadFailed", ret);
296
-
297
- // Complete the future with error status
298
- CompletableFuture<BundleInfo> failedFuture = downloadFutures.remove(id);
299
- if (failedFuture != null) {
300
- failedFuture.complete(failedBundle);
301
- }
289
+
290
+ io.execute(() -> {
291
+ BundleInfo failedBundle = new BundleInfo(
292
+ id,
293
+ failedVersion,
294
+ BundleStatus.ERROR,
295
+ new Date(System.currentTimeMillis()),
296
+ ""
297
+ );
298
+ saveBundleInfo(id, failedBundle);
299
+ // Cleanup download tracking for failed downloads
300
+ DownloadWorkerManager.cancelBundleDownload(activity, id, failedVersion);
301
+ Map<String, Object> ret = new HashMap<>();
302
+ ret.put("version", failedVersion);
303
+ if ("low_mem_fail".equals(error)) {
304
+ sendStats("low_mem_fail", failedVersion);
305
+ }
306
+ ret.put("error", error != null ? error : "download_fail");
307
+ sendStats("download_fail", failedVersion);
308
+ notifyListeners("downloadFailed", ret);
309
+
310
+ // Complete the future with error status
311
+ CompletableFuture<BundleInfo> failedFuture = downloadFutures.remove(id);
312
+ if (failedFuture != null) {
313
+ failedFuture.complete(failedBundle);
314
+ }
315
+ });
302
316
  break;
303
317
  }
304
318
  });