@capgo/capacitor-updater 7.20.0 → 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
@@ -63,12 +63,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
63
63
  private static final String updateUrlDefault = "https://plugin.capgo.app/updates";
64
64
  private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
65
65
  private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
66
+ private static final String KEEP_URL_FLAG_KEY = "__capgo_keep_url_path_after_reload";
66
67
  private static final String CUSTOM_ID_PREF_KEY = "CapacitorUpdater.customId";
67
68
  private static final String UPDATE_URL_PREF_KEY = "CapacitorUpdater.updateUrl";
68
69
  private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
69
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";
70
73
 
71
- private final String PLUGIN_VERSION = "7.20.0";
74
+ private final String PLUGIN_VERSION = "7.22.0";
72
75
  private static final String DELAY_CONDITION_PREFERENCES = "";
73
76
 
74
77
  private SharedPreferences.Editor editor;
@@ -96,6 +99,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
96
99
  private String directUpdateMode = "false";
97
100
  private Boolean wasRecentlyInstalledOrUpdated = false;
98
101
  Boolean shakeMenuEnabled = false;
102
+ private Boolean allowManualBundleError = false;
99
103
 
100
104
  private Boolean isPreviousMainActivity = true;
101
105
 
@@ -114,6 +118,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
114
118
  private FrameLayout splashscreenLoaderOverlay;
115
119
  private Runnable splashscreenTimeoutRunnable;
116
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
+
117
132
  private JSObject mapToJSObject(Map<String, Object> map) {
118
133
  JSObject jsObject = new JSObject();
119
134
  for (Map.Entry<String, Object> entry : map.entrySet()) {
@@ -122,6 +137,37 @@ public class CapacitorUpdaterPlugin extends Plugin {
122
137
  return jsObject;
123
138
  }
124
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
+
125
171
  public Thread startNewThread(final Runnable function, Number waitTime) {
126
172
  Thread bgTask = new Thread(() -> {
127
173
  try {
@@ -152,17 +198,23 @@ public class CapacitorUpdaterPlugin extends Plugin {
152
198
  this.implementation = new CapgoUpdater(logger) {
153
199
  @Override
154
200
  public void notifyDownload(final String id, final int percent) {
155
- CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
201
+ activity.runOnUiThread(() -> {
202
+ CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
203
+ });
156
204
  }
157
205
 
158
206
  @Override
159
207
  public void directUpdateFinish(final BundleInfo latest) {
160
- CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
208
+ activity.runOnUiThread(() -> {
209
+ CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
210
+ });
161
211
  }
162
212
 
163
213
  @Override
164
214
  public void notifyListeners(final String id, final Map<String, Object> res) {
165
- CapacitorUpdaterPlugin.this.notifyListeners(id, CapacitorUpdaterPlugin.this.mapToJSObject(res));
215
+ activity.runOnUiThread(() -> {
216
+ CapacitorUpdaterPlugin.this.notifyListeners(id, CapacitorUpdaterPlugin.this.mapToJSObject(res));
217
+ });
166
218
  }
167
219
  };
168
220
  final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
@@ -288,6 +340,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
288
340
  this.autoUpdate = this.getConfig().getBoolean("autoUpdate", true);
289
341
  this.appReadyTimeout = this.getConfig().getInt("appReadyTimeout", 10000);
290
342
  this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
343
+ this.syncKeepUrlPathFlag(this.keepUrlPathAfterReload);
344
+ this.allowManualBundleError = this.getConfig().getBoolean("allowManualBundleError", false);
291
345
  this.autoSplashscreen = this.getConfig().getBoolean("autoSplashscreen", false);
292
346
  this.autoSplashscreenLoader = this.getConfig().getBoolean("autoSplashscreenLoader", false);
293
347
  int splashscreenTimeoutValue = this.getConfig().getInt("autoSplashscreenTimeout", 10000);
@@ -584,30 +638,36 @@ public class CapacitorUpdaterPlugin extends Plugin {
584
638
  }
585
639
 
586
640
  private void cleanupObsoleteVersions() {
587
- final String previous = this.prefs.getString("LatestNativeBuildVersion", "");
588
- if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
589
- logger.info("New native build version detected: " + this.currentBuildVersion);
590
- this.implementation.reset(true);
591
- final List<BundleInfo> installed = this.implementation.list(false);
592
- for (final BundleInfo bundle : installed) {
593
- try {
594
- logger.info("Deleting obsolete bundle: " + bundle.getId());
595
- this.implementation.delete(bundle.getId());
596
- } catch (final Exception e) {
597
- logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
598
- }
599
- }
600
- final List<BundleInfo> storedBundles = this.implementation.list(true);
601
- final Set<String> allowedIds = new HashSet<>();
602
- for (final BundleInfo info : storedBundles) {
603
- if (info != null && info.getId() != null && !info.getId().isEmpty()) {
604
- 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);
605
664
  }
665
+ this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
666
+ this.editor.apply();
667
+ } catch (Exception e) {
668
+ logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
606
669
  }
607
- this.implementation.cleanupDownloadDirectories(allowedIds);
608
- }
609
- this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
610
- this.editor.apply();
670
+ });
611
671
  }
612
672
 
613
673
  public void notifyDownload(final String id, final int percent) {
@@ -943,8 +1003,25 @@ public class CapacitorUpdaterPlugin extends Plugin {
943
1003
  }
944
1004
  }
945
1005
 
1006
+ private void syncKeepUrlPathFlag(final boolean enabled) {
1007
+ if (this.bridge == null || this.bridge.getWebView() == null) {
1008
+ return;
1009
+ }
1010
+ final String script = enabled
1011
+ ? "(function(){try{localStorage.setItem('" +
1012
+ KEEP_URL_FLAG_KEY +
1013
+ "','1');}catch(e){}window.__capgoKeepUrlPathAfterReload=true;var evt;try{evt=new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',{detail:{enabled:true}});}catch(err){evt=document.createEvent('CustomEvent');evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',false,false,{enabled:true});}window.dispatchEvent(evt);})();"
1014
+ : "(function(){try{localStorage.removeItem('" +
1015
+ KEEP_URL_FLAG_KEY +
1016
+ "');}catch(e){}delete window.__capgoKeepUrlPathAfterReload;var evt;try{evt=new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',{detail:{enabled:false}});}catch(err){evt=document.createEvent('CustomEvent');evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',false,false,{enabled:false});}window.dispatchEvent(evt);})();";
1017
+ this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
1018
+ }
1019
+
946
1020
  protected boolean _reload() {
947
1021
  final String path = this.implementation.getCurrentBundlePath();
1022
+ if (this.keepUrlPathAfterReload) {
1023
+ this.syncKeepUrlPathFlag(true);
1024
+ }
948
1025
  this.semaphoreUp();
949
1026
  logger.info("Reloading: " + path);
950
1027
 
@@ -1003,7 +1080,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
1003
1080
  URL finalUrl1 = finalUrl;
1004
1081
  this.bridge.getWebView().post(() -> {
1005
1082
  this.bridge.getWebView().loadUrl(finalUrl1.toString());
1006
- this.bridge.getWebView().clearHistory();
1083
+ if (!this.keepUrlPathAfterReload) {
1084
+ this.bridge.getWebView().clearHistory();
1085
+ }
1007
1086
  });
1008
1087
  } catch (MalformedURLException e) {
1009
1088
  logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
@@ -1020,6 +1099,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
1020
1099
  } else {
1021
1100
  this.bridge.setServerBasePath(path);
1022
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
+ }
1023
1112
  }
1024
1113
 
1025
1114
  this.checkAppReady();
@@ -1119,6 +1208,44 @@ public class CapacitorUpdaterPlugin extends Plugin {
1119
1208
  }
1120
1209
  }
1121
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
+
1122
1249
  @PluginMethod
1123
1250
  public void list(final PluginCall call) {
1124
1251
  try {
@@ -1215,6 +1342,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
1215
1342
  }
1216
1343
  }
1217
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
+
1218
1365
  public void checkForUpdateAfterDelay() {
1219
1366
  if (this.periodCheckDelay == 0 || !this._isAutoUpdateEnabled()) {
1220
1367
  return;
@@ -1459,10 +1606,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1459
1606
  try {
1460
1607
  if (jsRes.has("message")) {
1461
1608
  logger.info("API message: " + jsRes.get("message"));
1462
- if (jsRes.has("major") && jsRes.getBoolean("major") && jsRes.has("version")) {
1463
- final JSObject majorAvailable = new JSObject();
1464
- majorAvailable.put("version", jsRes.getString("version"));
1465
- CapacitorUpdaterPlugin.this.notifyListeners("majorAvailable", majorAvailable);
1609
+ if (jsRes.has("version") && (jsRes.has("breaking") || jsRes.has("major"))) {
1610
+ CapacitorUpdaterPlugin.this.notifyBreakingEvents(jsRes.getString("version"));
1466
1611
  }
1467
1612
  String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
1468
1613
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
@@ -1717,6 +1862,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1717
1862
  logger.info("Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
1718
1863
  final JSObject ret = new JSObject();
1719
1864
  ret.put("bundle", mapToJSObject(current.toJSONMap()));
1865
+ this.persistLastFailedBundle(current);
1720
1866
  this.notifyListeners("updateFailed", ret);
1721
1867
  this.implementation.sendStats("update_fail", current.getVersionName());
1722
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
  });