@capgo/capacitor-updater 7.42.3 → 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.
- package/Package.swift +1 -1
- package/README.md +104 -41
- package/android/build.gradle +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +153 -49
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +60 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +23 -6
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +13 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +436 -2
- package/dist/docs.json +116 -4
- package/dist/esm/definitions.d.ts +54 -4
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +3 -1
- package/dist/esm/web.js +6 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +6 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +6 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +97 -6
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +23 -11
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +1 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +330 -2
- package/package.json +3 -2
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
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
|
|
244
|
-
|
|
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
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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<>();
|
|
@@ -1243,6 +1295,13 @@ public class CapgoUpdater {
|
|
|
1243
1295
|
makeJsonRequest(channelUrl, json, (res) -> {
|
|
1244
1296
|
if (res.containsKey("error")) {
|
|
1245
1297
|
callback.callback(res);
|
|
1298
|
+
} else if (Boolean.TRUE.equals(res.get("unset"))) {
|
|
1299
|
+
// Server requested to unset channel (public channel was requested)
|
|
1300
|
+
// Clear persisted defaultChannel and revert to config value
|
|
1301
|
+
editor.remove(defaultChannelKey);
|
|
1302
|
+
editor.apply();
|
|
1303
|
+
logger.info("Public channel requested, channel override removed");
|
|
1304
|
+
callback.callback(res);
|
|
1246
1305
|
} else {
|
|
1247
1306
|
// Success - persist defaultChannel
|
|
1248
1307
|
this.defaultChannel = channel;
|
|
@@ -288,9 +288,15 @@ public class DownloadService extends Worker {
|
|
|
288
288
|
for (int i = 0; i < totalFiles; i++) {
|
|
289
289
|
JSONObject entry = manifest.getJSONObject(i);
|
|
290
290
|
String fileName = entry.getString("file_name");
|
|
291
|
-
String fileHash = entry.
|
|
291
|
+
String fileHash = entry.optString("file_hash", "");
|
|
292
292
|
String downloadUrl = entry.getString("download_url");
|
|
293
293
|
|
|
294
|
+
if (fileHash.isEmpty()) {
|
|
295
|
+
logger.error("Missing file_hash for manifest entry: " + fileName);
|
|
296
|
+
hasError.set(true);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
294
300
|
if (publicKey != null && !publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
295
301
|
try {
|
|
296
302
|
fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
|
|
@@ -619,8 +625,12 @@ public class DownloadService extends Worker {
|
|
|
619
625
|
// targetFile is already the final destination without .br extension
|
|
620
626
|
File finalTargetFile = targetFile;
|
|
621
627
|
|
|
622
|
-
// Create a temporary file for the compressed data
|
|
623
|
-
|
|
628
|
+
// Create a temporary file for the compressed data with a unique name to avoid race conditions
|
|
629
|
+
// between threads processing files with the same basename in different directories
|
|
630
|
+
File compressedFile = new File(
|
|
631
|
+
getApplicationContext().getCacheDir(),
|
|
632
|
+
"temp_" + java.util.UUID.randomUUID().toString() + "_" + targetFile.getName() + ".tmp"
|
|
633
|
+
);
|
|
624
634
|
|
|
625
635
|
try {
|
|
626
636
|
try (Response response = sharedClient.newCall(request).execute()) {
|
|
@@ -648,7 +658,14 @@ public class DownloadService extends Worker {
|
|
|
648
658
|
// Use new decompression method with atomic write
|
|
649
659
|
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
650
660
|
byte[] compressedData = new byte[(int) compressedFile.length()];
|
|
651
|
-
|
|
661
|
+
int offset = 0;
|
|
662
|
+
int bytesRead;
|
|
663
|
+
while (
|
|
664
|
+
offset < compressedData.length &&
|
|
665
|
+
(bytesRead = fis.read(compressedData, offset, compressedData.length - offset)) != -1
|
|
666
|
+
) {
|
|
667
|
+
offset += bytesRead;
|
|
668
|
+
}
|
|
652
669
|
byte[] decompressedData;
|
|
653
670
|
try {
|
|
654
671
|
decompressedData = decompressBrotli(compressedData, targetFile.getName());
|
|
@@ -679,7 +696,7 @@ public class DownloadService extends Worker {
|
|
|
679
696
|
CryptoCipher.logChecksumInfo("Expected checksum", expectedHash);
|
|
680
697
|
|
|
681
698
|
// Verify checksum
|
|
682
|
-
if (calculatedHash.
|
|
699
|
+
if (calculatedHash.equalsIgnoreCase(expectedHash)) {
|
|
683
700
|
// Only cache if checksum is correct - use atomic copy
|
|
684
701
|
try (FileInputStream fis = new FileInputStream(finalTargetFile)) {
|
|
685
702
|
writeFileAtomic(cacheFile, fis, expectedHash);
|
|
@@ -712,7 +729,7 @@ public class DownloadService extends Worker {
|
|
|
712
729
|
private boolean verifyChecksum(File file, String expectedHash) {
|
|
713
730
|
try {
|
|
714
731
|
String actualHash = calculateFileHash(file);
|
|
715
|
-
return actualHash.
|
|
732
|
+
return actualHash.equalsIgnoreCase(expectedHash);
|
|
716
733
|
} catch (Exception e) {
|
|
717
734
|
e.printStackTrace();
|
|
718
735
|
return false;
|
|
@@ -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);
|