@capgo/capacitor-updater 7.42.9 → 7.43.3

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.
@@ -84,7 +84,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
84
84
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
85
85
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
86
86
 
87
- private final String pluginVersion = "7.42.9";
87
+ private final String pluginVersion = "7.43.3";
88
88
  private static final String DELAY_CONDITION_PREFERENCES = "";
89
89
 
90
90
  private SharedPreferences.Editor editor;
@@ -112,14 +112,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
112
112
  private Boolean wasRecentlyInstalledOrUpdated = false;
113
113
  private Boolean onLaunchDirectUpdateUsed = false;
114
114
  Boolean shakeMenuEnabled = false;
115
+ Boolean shakeChannelSelectorEnabled = false;
115
116
  private Boolean allowManualBundleError = false;
116
- private Boolean allowSetDefaultChannel = true;
117
+ Boolean allowSetDefaultChannel = true;
118
+
119
+ String getUpdateUrl() {
120
+ return this.updateUrl;
121
+ }
117
122
 
118
123
  // Used for activity-based foreground/background detection on Android < 14
119
124
  private Boolean isPreviousMainActivity = true;
120
125
 
121
126
  private volatile Thread backgroundDownloadTask;
122
127
  private volatile Thread appReadyCheck;
128
+ private volatile long downloadStartTimeMs = 0;
129
+ private static final long DOWNLOAD_TIMEOUT_MS = 3600000; // 1 hour timeout
123
130
 
124
131
  // private static final CountDownLatch semaphoreReady = new CountDownLatch(1);
125
132
  private static final Phaser semaphoreReady = new Phaser(1);
@@ -158,14 +165,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
158
165
  }
159
166
  }
160
167
 
161
- private JSObject mapToJSObject(Map<String, Object> map) {
162
- JSObject jsObject = new JSObject();
163
- for (Map.Entry<String, Object> entry : map.entrySet()) {
164
- jsObject.put(entry.getKey(), entry.getValue());
165
- }
166
- return jsObject;
167
- }
168
-
169
168
  private void persistLastFailedBundle(BundleInfo bundle) {
170
169
  if (this.prefs == null) {
171
170
  return;
@@ -233,23 +232,35 @@ public class CapacitorUpdaterPlugin extends Plugin {
233
232
  this.implementation = new CapgoUpdater(logger) {
234
233
  @Override
235
234
  public void notifyDownload(final String id, final int percent) {
236
- activity.runOnUiThread(() -> {
237
- CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
238
- });
235
+ if (activity != null) {
236
+ activity.runOnUiThread(() -> {
237
+ CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
238
+ });
239
+ } else {
240
+ logger.warn("notifyDownload: Activity is null, skipping notification");
241
+ }
239
242
  }
240
243
 
241
244
  @Override
242
245
  public void directUpdateFinish(final BundleInfo latest) {
243
- activity.runOnUiThread(() -> {
244
- CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
245
- });
246
+ if (activity != null) {
247
+ activity.runOnUiThread(() -> {
248
+ CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
249
+ });
250
+ } else {
251
+ logger.warn("directUpdateFinish: Activity is null, skipping notification");
252
+ }
246
253
  }
247
254
 
248
255
  @Override
249
256
  public void notifyListeners(final String id, final Map<String, Object> res) {
250
- activity.runOnUiThread(() -> {
251
- CapacitorUpdaterPlugin.this.notifyListeners(id, CapacitorUpdaterPlugin.this.mapToJSObject(res));
252
- });
257
+ if (activity != null) {
258
+ activity.runOnUiThread(() -> {
259
+ CapacitorUpdaterPlugin.this.notifyListeners(id, InternalUtils.mapToJSObject(res));
260
+ });
261
+ } else {
262
+ logger.warn("notifyListeners: Activity is null, skipping notification for event: " + id);
263
+ }
253
264
  }
254
265
  };
255
266
  final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
@@ -426,6 +437,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
426
437
  this.autoSplashscreenTimeout = Math.max(0, splashscreenTimeoutValue);
427
438
  this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
428
439
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
440
+ this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
429
441
  boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
430
442
 
431
443
  // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
@@ -499,7 +511,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
499
511
  private void sendReadyToJs(final BundleInfo current, final String msg, final boolean isDirectUpdate) {
500
512
  logger.info("sendReadyToJs: " + msg);
501
513
  final JSObject ret = new JSObject();
502
- ret.put("bundle", mapToJSObject(current.toJSONMap()));
514
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
503
515
  ret.put("status", msg);
504
516
 
505
517
  // No need to wait for semaphore anymore since _reload() has already waited
@@ -842,7 +854,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
842
854
  final JSObject ret = new JSObject();
843
855
  ret.put("percent", percent);
844
856
  final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
845
- ret.put("bundle", mapToJSObject(bundleInfo.toJSONMap()));
857
+ ret.put("bundle", InternalUtils.mapToJSObject(bundleInfo.toJSONMap()));
846
858
  this.notifyListeners("download", ret);
847
859
 
848
860
  if (percent == 100) {
@@ -994,7 +1006,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
994
1006
  DEFAULT_CHANNEL_PREF_KEY,
995
1007
  configDefaultChannel,
996
1008
  (res) -> {
997
- JSObject jsRes = mapToJSObject(res);
1009
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
998
1010
  if (jsRes.has("error")) {
999
1011
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1000
1012
  String errorCode = jsRes.getString("error");
@@ -1007,7 +1019,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1007
1019
  } else {
1008
1020
  if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
1009
1021
  logger.info("Calling autoupdater after channel change!");
1010
- backgroundDownload();
1022
+ // Check if download is already in progress (with timeout protection)
1023
+ if (!this.isDownloadStuckOrTimedOut()) {
1024
+ backgroundDownload();
1025
+ } else {
1026
+ logger.info("Download already in progress, skipping duplicate download request");
1027
+ }
1011
1028
  }
1012
1029
  call.resolve(jsRes);
1013
1030
  }
@@ -1042,7 +1059,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1042
1059
  DEFAULT_CHANNEL_PREF_KEY,
1043
1060
  CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
1044
1061
  (res) -> {
1045
- JSObject jsRes = mapToJSObject(res);
1062
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1046
1063
  if (jsRes.has("error")) {
1047
1064
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1048
1065
  String errorCode = jsRes.getString("error");
@@ -1066,7 +1083,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1066
1083
  } else {
1067
1084
  if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
1068
1085
  logger.info("Calling autoupdater after channel change!");
1069
- backgroundDownload();
1086
+ // Check if download is already in progress (with timeout protection)
1087
+ if (!this.isDownloadStuckOrTimedOut()) {
1088
+ backgroundDownload();
1089
+ } else {
1090
+ logger.info("Download already in progress, skipping duplicate download request");
1091
+ }
1070
1092
  }
1071
1093
  call.resolve(jsRes);
1072
1094
  }
@@ -1085,7 +1107,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1085
1107
  logger.info("getChannel");
1086
1108
  startNewThread(() ->
1087
1109
  CapacitorUpdaterPlugin.this.implementation.getChannel((res) -> {
1088
- JSObject jsRes = mapToJSObject(res);
1110
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1089
1111
  if (jsRes.has("error")) {
1090
1112
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1091
1113
  String errorCode = jsRes.getString("error");
@@ -1112,7 +1134,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1112
1134
  logger.info("listChannels");
1113
1135
  startNewThread(() ->
1114
1136
  CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
1115
- JSObject jsRes = mapToJSObject(res);
1137
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1116
1138
  if (jsRes.has("error")) {
1117
1139
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1118
1140
  String errorCode = jsRes.getString("error");
@@ -1162,7 +1184,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1162
1184
  // Return immediately with a pending status - the actual result will come via listeners
1163
1185
  final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
1164
1186
  downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
1165
- call.resolve(mapToJSObject(downloaded.toJSONMap()));
1187
+ call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
1166
1188
  return;
1167
1189
  } else {
1168
1190
  downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
@@ -1170,7 +1192,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1170
1192
  if (downloaded.isErrorStatus()) {
1171
1193
  throw new RuntimeException("Download failed: " + downloaded.getStatus());
1172
1194
  } else {
1173
- call.resolve(mapToJSObject(downloaded.toJSONMap()));
1195
+ call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
1174
1196
  }
1175
1197
  } catch (final Exception e) {
1176
1198
  logger.error("Failed to download from: " + url + " " + e.getMessage());
@@ -1344,7 +1366,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1344
1366
  logger.error("Set next id failed. Bundle " + id + " does not exist.");
1345
1367
  call.reject("Set next id failed. Bundle " + id + " does not exist.");
1346
1368
  } else {
1347
- call.resolve(mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1369
+ call.resolve(InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1348
1370
  }
1349
1371
  } catch (final Exception e) {
1350
1372
  logger.error("Could not set next id " + id + " " + e.getMessage());
@@ -1428,7 +1450,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1428
1450
  }
1429
1451
  this.implementation.setError(bundle);
1430
1452
  final JSObject ret = new JSObject();
1431
- ret.put("bundle", mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1453
+ ret.put("bundle", InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1432
1454
  call.resolve(ret);
1433
1455
  } catch (final Exception e) {
1434
1456
  logger.error("Could not set bundle error for id " + id + " " + e.getMessage());
@@ -1443,7 +1465,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1443
1465
  final JSObject ret = new JSObject();
1444
1466
  final JSArray values = new JSArray();
1445
1467
  for (final BundleInfo bundle : res) {
1446
- values.put(mapToJSObject(bundle.toJSONMap()));
1468
+ values.put(InternalUtils.mapToJSObject(bundle.toJSONMap()));
1447
1469
  }
1448
1470
  ret.put("bundles", values);
1449
1471
  call.resolve(ret);
@@ -1458,7 +1480,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1458
1480
  final String channel = call.getString("channel");
1459
1481
  startNewThread(() ->
1460
1482
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
1461
- JSObject jsRes = mapToJSObject(res);
1483
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1462
1484
  if (jsRes.has("error")) {
1463
1485
  String error = jsRes.getString("error");
1464
1486
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
@@ -1510,7 +1532,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1510
1532
  try {
1511
1533
  final JSObject ret = new JSObject();
1512
1534
  final BundleInfo bundle = this.implementation.getCurrentBundle();
1513
- ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
1535
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
1514
1536
  ret.put("native", this.currentVersionNative);
1515
1537
  call.resolve(ret);
1516
1538
  } catch (final Exception e) {
@@ -1528,7 +1550,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1528
1550
  return;
1529
1551
  }
1530
1552
 
1531
- call.resolve(mapToJSObject(bundle.toJSONMap()));
1553
+ call.resolve(InternalUtils.mapToJSObject(bundle.toJSONMap()));
1532
1554
  } catch (final Exception e) {
1533
1555
  logger.error("Could not get next bundle " + e.getMessage());
1534
1556
  call.reject("Could not get next bundle", e);
@@ -1547,7 +1569,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1547
1569
  this.persistLastFailedBundle(null);
1548
1570
 
1549
1571
  final JSObject ret = new JSObject();
1550
- ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
1572
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
1551
1573
  call.resolve(ret);
1552
1574
  } catch (final Exception e) {
1553
1575
  logger.error("Could not get failed update " + e.getMessage());
@@ -1566,7 +1588,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1566
1588
  public void run() {
1567
1589
  try {
1568
1590
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1569
- JSObject jsRes = mapToJSObject(res);
1591
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1570
1592
  if (jsRes.has("error")) {
1571
1593
  String error = jsRes.getString("error");
1572
1594
  String errorMessage = jsRes.has("message")
@@ -1578,7 +1600,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1578
1600
  String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
1579
1601
  if (!Objects.equals(newVersion, currentVersion)) {
1580
1602
  logger.info("New version found: " + newVersion);
1581
- CapacitorUpdaterPlugin.this.backgroundDownload();
1603
+ // Check if download is already in progress (with timeout protection)
1604
+ if (!CapacitorUpdaterPlugin.this.isDownloadStuckOrTimedOut()) {
1605
+ CapacitorUpdaterPlugin.this.backgroundDownload();
1606
+ } else {
1607
+ logger.info("Download already in progress, skipping duplicate download request");
1608
+ }
1582
1609
  }
1583
1610
  }
1584
1611
  });
@@ -1603,7 +1630,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1603
1630
  this.semaphoreDown();
1604
1631
  logger.info("semaphoreReady countDown done");
1605
1632
  final JSObject ret = new JSObject();
1606
- ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
1633
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
1607
1634
  call.resolve(ret);
1608
1635
  } catch (final Exception e) {
1609
1636
  logger.error("Failed to notify app ready state. [Error calling 'notifyAppReady()'] " + e.getMessage());
@@ -1767,13 +1794,39 @@ public class CapacitorUpdaterPlugin extends Plugin {
1767
1794
  this.notifyListeners(failureEvent, ret);
1768
1795
  }
1769
1796
  final JSObject ret = new JSObject();
1770
- ret.put("bundle", mapToJSObject(current.toJSONMap()));
1797
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
1771
1798
  this.notifyListeners("noNeedUpdate", ret);
1772
1799
  this.sendReadyToJs(current, msg, isDirectUpdate);
1773
1800
  this.backgroundDownloadTask = null;
1801
+ this.downloadStartTimeMs = 0;
1774
1802
  logger.info("endBackGroundTaskWithNotif " + msg);
1775
1803
  }
1776
1804
 
1805
+ private boolean isDownloadStuckOrTimedOut() {
1806
+ if (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive()) {
1807
+ return false;
1808
+ }
1809
+
1810
+ // Check if download has timed out
1811
+ if (this.downloadStartTimeMs > 0) {
1812
+ long elapsed = System.currentTimeMillis() - this.downloadStartTimeMs;
1813
+ if (elapsed > DOWNLOAD_TIMEOUT_MS) {
1814
+ logger.warn(
1815
+ "Download has been in progress for " +
1816
+ elapsed +
1817
+ " ms, exceeding timeout of " +
1818
+ DOWNLOAD_TIMEOUT_MS +
1819
+ " ms. Clearing stuck state."
1820
+ );
1821
+ this.backgroundDownloadTask = null;
1822
+ this.downloadStartTimeMs = 0;
1823
+ return false; // Now it's not stuck anymore, caller can proceed
1824
+ }
1825
+ }
1826
+
1827
+ return true;
1828
+ }
1829
+
1777
1830
  private Thread backgroundDownload() {
1778
1831
  final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
1779
1832
  final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
@@ -1781,13 +1834,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
1781
1834
  final String messageUpdate = initialDirectUpdateAllowed
1782
1835
  ? "Update will occur now."
1783
1836
  : "Update will occur next time app moves to background.";
1784
- return startNewThread(() -> {
1837
+ Thread newTask = startNewThread(() -> {
1785
1838
  // Wait for cleanup to complete before starting download
1786
1839
  waitForCleanupIfNeeded();
1787
1840
  logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
1788
1841
  try {
1789
1842
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1790
- JSObject jsRes = mapToJSObject(res);
1843
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1791
1844
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1792
1845
 
1793
1846
  // Handle network errors and other failures first
@@ -1869,7 +1922,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1869
1922
  final BundleInfo latest = CapacitorUpdaterPlugin.this.implementation.getBundleInfoByName(latestVersionName);
1870
1923
  if (latest != null) {
1871
1924
  final JSObject ret = new JSObject();
1872
- ret.put("bundle", mapToJSObject(latest.toJSONMap()));
1925
+ ret.put("bundle", InternalUtils.mapToJSObject(latest.toJSONMap()));
1873
1926
  if (latest.isErrorStatus()) {
1874
1927
  logger.error("Latest bundle already exists, and is in error state. Aborting update.");
1875
1928
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
@@ -2013,6 +2066,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
2013
2066
  );
2014
2067
  }
2015
2068
  });
2069
+ this.backgroundDownloadTask = newTask;
2070
+ this.downloadStartTimeMs = System.currentTimeMillis();
2071
+ return newTask;
2016
2072
  }
2017
2073
 
2018
2074
  private void installNext() {
@@ -2055,7 +2111,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2055
2111
  logger.error("notifyAppReady was not called, roll back current bundle: " + current.getId());
2056
2112
  logger.info("Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
2057
2113
  final JSObject ret = new JSObject();
2058
- ret.put("bundle", mapToJSObject(current.toJSONMap()));
2114
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
2059
2115
  this.persistLastFailedBundle(current);
2060
2116
  this.notifyListeners("updateFailed", ret);
2061
2117
  this.implementation.sendStats("update_fail", current.getVersionName());
@@ -2093,16 +2149,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
2093
2149
  }
2094
2150
 
2095
2151
  public void appMovedToForeground() {
2152
+ // Ensure activity reference is up-to-date before proceeding
2153
+ // This is critical for callbacks that may be invoked during background operations
2154
+ try {
2155
+ Activity currentActivity = this.getActivity();
2156
+ if (currentActivity != null) {
2157
+ CapacitorUpdaterPlugin.this.implementation.activity = currentActivity;
2158
+ } else {
2159
+ logger.warn("appMovedToForeground: Activity is null, operations may be limited");
2160
+ }
2161
+ } catch (Exception e) {
2162
+ logger.error("appMovedToForeground: Failed to update activity reference: " + e.getMessage());
2163
+ }
2164
+
2096
2165
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2097
2166
  CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_foreground", current.getVersionName());
2098
2167
  this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
2099
2168
  this.delayUpdateUtils.unsetBackgroundTimestamp();
2100
2169
 
2101
- if (
2102
- CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() &&
2103
- (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive())
2104
- ) {
2105
- this.backgroundDownloadTask = this.backgroundDownload();
2170
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && !this.isDownloadStuckOrTimedOut()) {
2171
+ this.backgroundDownload();
2106
2172
  } else {
2107
2173
  final CapConfig config = CapConfig.loadDefault(this.getActivity());
2108
2174
  String serverUrl = config.getServerUrl();
@@ -2119,6 +2185,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
2119
2185
  // Reset timeout flag at start of each background cycle
2120
2186
  this.autoSplashscreenTimedOut = false;
2121
2187
 
2188
+ // Ensure activity reference is up-to-date before proceeding
2189
+ try {
2190
+ Activity currentActivity = this.getActivity();
2191
+ if (currentActivity != null) {
2192
+ CapacitorUpdaterPlugin.this.implementation.activity = currentActivity;
2193
+ } else {
2194
+ logger.warn("appMovedToBackground: Activity is null, operations may be limited");
2195
+ }
2196
+ } catch (Exception e) {
2197
+ logger.error("appMovedToBackground: Failed to update activity reference: " + e.getMessage());
2198
+ }
2199
+
2122
2200
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2123
2201
 
2124
2202
  // Show splashscreen FIRST, before any other background work to ensure launcher shows it
@@ -2307,6 +2385,32 @@ public class CapacitorUpdaterPlugin extends Plugin {
2307
2385
  }
2308
2386
  }
2309
2387
 
2388
+ @PluginMethod
2389
+ public void setShakeChannelSelector(final PluginCall call) {
2390
+ final Boolean enabled = call.getBoolean("enabled");
2391
+ if (enabled == null) {
2392
+ logger.error("setShakeChannelSelector called without enabled parameter");
2393
+ call.reject("setShakeChannelSelector called without enabled parameter");
2394
+ return;
2395
+ }
2396
+
2397
+ this.shakeChannelSelectorEnabled = enabled;
2398
+ logger.info("Shake channel selector " + (enabled ? "enabled" : "disabled"));
2399
+ call.resolve();
2400
+ }
2401
+
2402
+ @PluginMethod
2403
+ public void isShakeChannelSelectorEnabled(final PluginCall call) {
2404
+ try {
2405
+ final JSObject ret = new JSObject();
2406
+ ret.put("enabled", this.shakeChannelSelectorEnabled);
2407
+ call.resolve(ret);
2408
+ } catch (final Exception e) {
2409
+ logger.error("Could not get shake channel selector status " + e.getMessage());
2410
+ call.reject("Could not get shake channel selector status", e);
2411
+ }
2412
+ }
2413
+
2310
2414
  @PluginMethod
2311
2415
  public void getAppId(final PluginCall call) {
2312
2416
  try {
@@ -573,7 +573,7 @@ public class CapgoUpdater {
573
573
  this.notifyDownload(id, 100);
574
574
 
575
575
  final Map<String, Object> ret = new HashMap<>();
576
- ret.put("bundle", next.toJSONMap());
576
+ ret.put("bundle", InternalUtils.mapToJSObject(next.toJSONMap()));
577
577
  logger.info("updateAvailable: " + ret);
578
578
  CapgoUpdater.this.notifyListeners("updateAvailable", ret);
579
579
  logger.info("setNext: " + setNext);
@@ -829,6 +829,58 @@ public class CapgoUpdater {
829
829
  }
830
830
  }
831
831
 
832
+ public BundleInfo downloadManifest(
833
+ final String url,
834
+ final String version,
835
+ final String sessionKey,
836
+ final String checksum,
837
+ final JSONArray manifest
838
+ ) throws IOException {
839
+ if (manifest == null) {
840
+ return download(url, version, sessionKey, checksum);
841
+ }
842
+
843
+ // Check for existing bundle with same version and clean up if in error state
844
+ BundleInfo existingBundle = this.getBundleInfoByName(version);
845
+ if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted())) {
846
+ logger.info("Found existing failed bundle for version " + version + ", deleting before retry");
847
+ this.delete(existingBundle.getId(), true);
848
+ }
849
+
850
+ final String id = this.randomString();
851
+ saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), ""));
852
+ this.notifyDownload(id, 0);
853
+ this.notifyDownload(id, 5);
854
+ final String dest = this.randomString();
855
+
856
+ // Create a CompletableFuture to track download completion
857
+ CompletableFuture<BundleInfo> downloadFuture = new CompletableFuture<>();
858
+ downloadFutures.put(id, downloadFuture);
859
+
860
+ // Start the download
861
+ this.download(id, url, dest, version, sessionKey, checksum, manifest);
862
+
863
+ // Wait for completion without timeout
864
+ try {
865
+ BundleInfo result = downloadFuture.get();
866
+ if (result.isErrorStatus()) {
867
+ throw new IOException("Download failed with status: " + result.getStatus());
868
+ }
869
+ return result;
870
+ } catch (Exception e) {
871
+ // Clean up on failure
872
+ downloadFutures.remove(id);
873
+ logger.error("Error waiting for download");
874
+ logger.debug("Error: " + e.getMessage());
875
+ BundleInfo errorBundle = new BundleInfo(id, version, BundleStatus.ERROR, new Date(System.currentTimeMillis()), "");
876
+ saveBundleInfo(id, errorBundle);
877
+ if (e instanceof IOException) {
878
+ throw (IOException) e;
879
+ }
880
+ throw new IOException("Error waiting for download: " + e.getMessage(), e);
881
+ }
882
+ }
883
+
832
884
  public List<BundleInfo> list(boolean rawList) {
833
885
  if (!rawList) {
834
886
  final List<BundleInfo> res = new ArrayList<>();
@@ -3,9 +3,22 @@ package ee.forgr.capacitor_updater;
3
3
  import android.content.pm.PackageInfo;
4
4
  import android.content.pm.PackageManager;
5
5
  import android.os.Build;
6
+ import com.getcapacitor.JSObject;
7
+ import java.util.Map;
6
8
 
7
9
  public class InternalUtils {
8
10
 
11
+ /**
12
+ * Converts a Map to JSObject for proper bridge serialization.
13
+ */
14
+ public static JSObject mapToJSObject(Map<String, Object> map) {
15
+ JSObject jsObject = new JSObject();
16
+ for (Map.Entry<String, Object> entry : map.entrySet()) {
17
+ jsObject.put(entry.getKey(), entry.getValue());
18
+ }
19
+ return jsObject;
20
+ }
21
+
9
22
  public static String getPackageName(PackageManager pm, String packageName) {
10
23
  try {
11
24
  PackageInfo pInfo = getPackageInfoInternal(pm, packageName);