@capgo/capacitor-updater 6.43.4 → 6.45.10
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 +5 -2
- package/README.md +151 -41
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +537 -172
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +173 -43
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +49 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +38 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +49 -9
- package/dist/docs.json +290 -10
- package/dist/esm/definitions.d.ts +134 -22
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -2
- package/dist/esm/web.js +0 -4
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +0 -4
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +0 -4
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +558 -140
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +213 -55
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +30 -36
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +37 -16
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +2 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +20 -3
- package/package.json +12 -9
|
@@ -81,9 +81,7 @@ public class CapgoUpdater {
|
|
|
81
81
|
public String channelUrl = "";
|
|
82
82
|
public String defaultChannel = "";
|
|
83
83
|
public String appId = "";
|
|
84
|
-
public String privateKey = "";
|
|
85
84
|
public String publicKey = "";
|
|
86
|
-
public boolean hasOldPrivateKeyPropertyInConfig = false;
|
|
87
85
|
public String deviceID = "";
|
|
88
86
|
public int timeout = 20000;
|
|
89
87
|
|
|
@@ -347,7 +345,7 @@ public class CapgoUpdater {
|
|
|
347
345
|
}
|
|
348
346
|
}
|
|
349
347
|
|
|
350
|
-
private void observeWorkProgress(Context context, String id) {
|
|
348
|
+
private void observeWorkProgress(Context context, String id, boolean setNext) {
|
|
351
349
|
if (!(context instanceof LifecycleOwner)) {
|
|
352
350
|
logger.error("Context is not a LifecycleOwner, cannot observe work progress");
|
|
353
351
|
return;
|
|
@@ -377,7 +375,7 @@ public class CapgoUpdater {
|
|
|
377
375
|
boolean isManifest = outputData.getBoolean(DownloadService.IS_MANIFEST, false);
|
|
378
376
|
|
|
379
377
|
io.execute(() -> {
|
|
380
|
-
boolean success = finishDownload(id, dest, version, sessionKey, checksum,
|
|
378
|
+
boolean success = finishDownload(id, dest, version, sessionKey, checksum, setNext, isManifest);
|
|
381
379
|
BundleInfo resultBundle;
|
|
382
380
|
if (!success) {
|
|
383
381
|
logger.error("Finish download failed");
|
|
@@ -456,13 +454,14 @@ public class CapgoUpdater {
|
|
|
456
454
|
final String version,
|
|
457
455
|
final String sessionKey,
|
|
458
456
|
final String checksum,
|
|
459
|
-
final JSONArray manifest
|
|
457
|
+
final JSONArray manifest,
|
|
458
|
+
final boolean setNext
|
|
460
459
|
) {
|
|
461
460
|
if (this.activity == null) {
|
|
462
461
|
logger.error("Activity is null, cannot observe work progress");
|
|
463
462
|
return;
|
|
464
463
|
}
|
|
465
|
-
observeWorkProgress(this.activity, id);
|
|
464
|
+
observeWorkProgress(this.activity, id, setNext);
|
|
466
465
|
|
|
467
466
|
DownloadWorkerManager.enqueueDownload(
|
|
468
467
|
this.activity,
|
|
@@ -520,15 +519,12 @@ public class CapgoUpdater {
|
|
|
520
519
|
throw new IOException("Checksum required when public key is present: " + id);
|
|
521
520
|
}
|
|
522
521
|
|
|
523
|
-
if (!
|
|
524
|
-
// V2 Encryption (publicKey)
|
|
522
|
+
if (!sessionKey.isEmpty()) {
|
|
525
523
|
CryptoCipher.decryptFile(downloaded, publicKey, sessionKey);
|
|
526
524
|
checksumDecrypted = CryptoCipher.decryptChecksum(checksumRes, publicKey);
|
|
527
525
|
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
528
|
-
} else
|
|
529
|
-
|
|
530
|
-
this.sendStats("checksum_fail");
|
|
531
|
-
throw new IOException("V1 decryption is no longer supported for security reasons.");
|
|
526
|
+
} else {
|
|
527
|
+
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
532
528
|
}
|
|
533
529
|
CryptoCipher.logChecksumInfo("Calculated checksum", checksum);
|
|
534
530
|
CryptoCipher.logChecksumInfo("Expected checksum", checksumDecrypted);
|
|
@@ -762,12 +758,37 @@ public class CapgoUpdater {
|
|
|
762
758
|
this.editor.commit();
|
|
763
759
|
}
|
|
764
760
|
|
|
761
|
+
static boolean shouldResetForForeignBundle(final String bundlePath, final boolean isBuiltin, final boolean hasStoredBundleInfo) {
|
|
762
|
+
return bundlePath != null && !bundlePath.trim().isEmpty() && !isBuiltin && !hasStoredBundleInfo;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private boolean hasStoredBundleInfo(final String id) {
|
|
766
|
+
return (
|
|
767
|
+
id != null &&
|
|
768
|
+
!id.isEmpty() &&
|
|
769
|
+
!BundleInfo.ID_BUILTIN.equals(id) &&
|
|
770
|
+
!BundleInfo.VERSION_UNKNOWN.equals(id) &&
|
|
771
|
+
this.prefs.contains(id + INFO_SUFFIX)
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
765
775
|
public void downloadBackground(
|
|
766
776
|
final String url,
|
|
767
777
|
final String version,
|
|
768
778
|
final String sessionKey,
|
|
769
779
|
final String checksum,
|
|
770
780
|
final JSONArray manifest
|
|
781
|
+
) {
|
|
782
|
+
downloadBackground(url, version, sessionKey, checksum, manifest, true);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
public void downloadBackground(
|
|
786
|
+
final String url,
|
|
787
|
+
final String version,
|
|
788
|
+
final String sessionKey,
|
|
789
|
+
final String checksum,
|
|
790
|
+
final JSONArray manifest,
|
|
791
|
+
final boolean setNext
|
|
771
792
|
) {
|
|
772
793
|
final String id = this.randomString();
|
|
773
794
|
|
|
@@ -789,7 +810,7 @@ public class CapgoUpdater {
|
|
|
789
810
|
this.notifyDownload(id, 0);
|
|
790
811
|
this.notifyDownload(id, 5);
|
|
791
812
|
|
|
792
|
-
this.download(id, url, this.randomString(), version, sessionKey, checksum, manifest);
|
|
813
|
+
this.download(id, url, this.randomString(), version, sessionKey, checksum, manifest, setNext);
|
|
793
814
|
}
|
|
794
815
|
|
|
795
816
|
public BundleInfo download(final String url, final String version, final String sessionKey, final String checksum) throws IOException {
|
|
@@ -811,7 +832,7 @@ public class CapgoUpdater {
|
|
|
811
832
|
downloadFutures.put(id, downloadFuture);
|
|
812
833
|
|
|
813
834
|
// Start the download
|
|
814
|
-
this.download(id, url, dest, version, sessionKey, checksum, null);
|
|
835
|
+
this.download(id, url, dest, version, sessionKey, checksum, null, false);
|
|
815
836
|
|
|
816
837
|
// Wait for completion without timeout
|
|
817
838
|
try {
|
|
@@ -863,7 +884,7 @@ public class CapgoUpdater {
|
|
|
863
884
|
downloadFutures.put(id, downloadFuture);
|
|
864
885
|
|
|
865
886
|
// Start the download
|
|
866
|
-
this.download(id, url, dest, version, sessionKey, checksum, manifest);
|
|
887
|
+
this.download(id, url, dest, version, sessionKey, checksum, manifest, false);
|
|
867
888
|
|
|
868
889
|
// Wait for completion without timeout
|
|
869
890
|
try {
|
|
@@ -970,6 +991,64 @@ public class CapgoUpdater {
|
|
|
970
991
|
return (bundle.isDirectory() && bundle.exists() && new File(bundle.getPath(), "/index.html").exists() && !bundleInfo.isDeleted());
|
|
971
992
|
}
|
|
972
993
|
|
|
994
|
+
static final class ResetState {
|
|
995
|
+
|
|
996
|
+
final String currentBundlePath;
|
|
997
|
+
final String fallbackBundleId;
|
|
998
|
+
final String nextBundleId;
|
|
999
|
+
|
|
1000
|
+
ResetState(final String currentBundlePath, final String fallbackBundleId, final String nextBundleId) {
|
|
1001
|
+
this.currentBundlePath = currentBundlePath;
|
|
1002
|
+
this.fallbackBundleId = fallbackBundleId;
|
|
1003
|
+
this.nextBundleId = nextBundleId;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
ResetState captureResetState() {
|
|
1008
|
+
return new ResetState(
|
|
1009
|
+
this.getCurrentBundlePath(),
|
|
1010
|
+
this.prefs.getString(FALLBACK_VERSION, BundleInfo.ID_BUILTIN),
|
|
1011
|
+
this.prefs.getString(NEXT_VERSION, null)
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
void restoreResetState(final ResetState state) {
|
|
1016
|
+
final String currentBundlePath = state.currentBundlePath == null || state.currentBundlePath.trim().isEmpty()
|
|
1017
|
+
? "public"
|
|
1018
|
+
: state.currentBundlePath;
|
|
1019
|
+
final String fallbackBundleId = state.fallbackBundleId == null || state.fallbackBundleId.isEmpty()
|
|
1020
|
+
? BundleInfo.ID_BUILTIN
|
|
1021
|
+
: state.fallbackBundleId;
|
|
1022
|
+
|
|
1023
|
+
this.editor.putString(this.CAP_SERVER_PATH, currentBundlePath);
|
|
1024
|
+
this.editor.putString(FALLBACK_VERSION, fallbackBundleId);
|
|
1025
|
+
if (state.nextBundleId == null || state.nextBundleId.isEmpty()) {
|
|
1026
|
+
this.editor.remove(NEXT_VERSION);
|
|
1027
|
+
} else {
|
|
1028
|
+
this.editor.putString(NEXT_VERSION, state.nextBundleId);
|
|
1029
|
+
}
|
|
1030
|
+
this.editor.commit();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
void prepareResetStateForTransition() {
|
|
1034
|
+
this.setCurrentBundle(new File("public"));
|
|
1035
|
+
this.setFallbackBundle(null);
|
|
1036
|
+
this.setNextBundle(null);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
void finalizeResetTransition(final String previousBundleName, final boolean internal) {
|
|
1040
|
+
if (this.activity != null) {
|
|
1041
|
+
DownloadWorkerManager.cancelAllDownloads(this.activity);
|
|
1042
|
+
}
|
|
1043
|
+
if (!internal) {
|
|
1044
|
+
this.sendStats("reset", this.getCurrentBundle().getVersionName(), previousBundleName);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
boolean canSet(final BundleInfo bundle) {
|
|
1049
|
+
return bundle != null && (bundle.isBuiltin() || this.bundleExists(bundle.getId()));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
973
1052
|
public Boolean set(final BundleInfo bundle) {
|
|
974
1053
|
return this.set(bundle.getId());
|
|
975
1054
|
}
|
|
@@ -994,11 +1073,32 @@ public class CapgoUpdater {
|
|
|
994
1073
|
return false;
|
|
995
1074
|
}
|
|
996
1075
|
|
|
1076
|
+
boolean stagePendingReload(final BundleInfo bundle) {
|
|
1077
|
+
if (bundle == null || bundle.isBuiltin() || !this.bundleExists(bundle.getId())) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
this.setCurrentBundle(this.getBundleDirectory(bundle.getId()));
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
void finalizePendingReload(final BundleInfo bundle, final String previousBundleName) {
|
|
1085
|
+
if (bundle == null || bundle.isBuiltin()) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
this.sendStats("set", bundle.getVersionName(), previousBundleName);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
997
1091
|
public void autoReset() {
|
|
998
1092
|
final BundleInfo currentBundle = this.getCurrentBundle();
|
|
999
1093
|
if (!currentBundle.isBuiltin() && !this.bundleExists(currentBundle.getId())) {
|
|
1000
1094
|
logger.info("Folder at bundle path does not exist. Triggering reset.");
|
|
1001
1095
|
this.reset();
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
String bundlePath = this.prefs.getString(this.CAP_SERVER_PATH, null);
|
|
1099
|
+
if (shouldResetForForeignBundle(bundlePath, currentBundle.isBuiltin(), this.hasStoredBundleInfo(currentBundle.getId()))) {
|
|
1100
|
+
logger.info("Current bundle id is not one of the bundle ids stored by this plugin. Triggering reset.");
|
|
1101
|
+
this.reset();
|
|
1002
1102
|
}
|
|
1003
1103
|
}
|
|
1004
1104
|
|
|
@@ -1031,17 +1131,9 @@ public class CapgoUpdater {
|
|
|
1031
1131
|
|
|
1032
1132
|
public void reset(final boolean internal) {
|
|
1033
1133
|
logger.debug("reset: " + internal);
|
|
1034
|
-
|
|
1035
|
-
this.
|
|
1036
|
-
this.
|
|
1037
|
-
this.setNextBundle(null);
|
|
1038
|
-
// Cancel any ongoing downloads
|
|
1039
|
-
if (this.activity != null) {
|
|
1040
|
-
DownloadWorkerManager.cancelAllDownloads(this.activity);
|
|
1041
|
-
}
|
|
1042
|
-
if (!internal) {
|
|
1043
|
-
this.sendStats("reset", this.getCurrentBundle().getVersionName(), currentBundleName);
|
|
1044
|
-
}
|
|
1134
|
+
final String currentBundleName = this.getCurrentBundle().getVersionName();
|
|
1135
|
+
this.prepareResetStateForTransition();
|
|
1136
|
+
this.finalizeResetTransition(currentBundleName, internal);
|
|
1045
1137
|
}
|
|
1046
1138
|
|
|
1047
1139
|
private JSONObject createInfoObject() throws JSONException {
|
|
@@ -1137,6 +1229,7 @@ public class CapgoUpdater {
|
|
|
1137
1229
|
Map<String, Object> retError = new HashMap<>();
|
|
1138
1230
|
retError.put("message", "Request failed: " + e.getMessage());
|
|
1139
1231
|
retError.put("error", "network_error");
|
|
1232
|
+
retError.put("kind", "failed");
|
|
1140
1233
|
callback.callback(retError);
|
|
1141
1234
|
}
|
|
1142
1235
|
|
|
@@ -1144,11 +1237,46 @@ public class CapgoUpdater {
|
|
|
1144
1237
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
1145
1238
|
try (ResponseBody responseBody = response.body()) {
|
|
1146
1239
|
final int statusCode = response.code();
|
|
1240
|
+
final String responseData = responseBody != null ? responseBody.string() : "";
|
|
1241
|
+
JSONObject jsonResponse = null;
|
|
1242
|
+
if (!responseData.isEmpty()) {
|
|
1243
|
+
try {
|
|
1244
|
+
jsonResponse = new JSONObject(responseData);
|
|
1245
|
+
} catch (JSONException ignored) {
|
|
1246
|
+
// Non-JSON responses are handled as response or parse errors below.
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (jsonResponse != null && (jsonResponse.has("error") || jsonResponse.has("kind"))) {
|
|
1251
|
+
if (statusCode == 429) {
|
|
1252
|
+
checkAndHandleRateLimitResponse(response);
|
|
1253
|
+
}
|
|
1254
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1255
|
+
if (jsonResponse.has("error") && !jsonResponse.isNull("error")) {
|
|
1256
|
+
retError.put("error", jsonResponse.getString("error"));
|
|
1257
|
+
}
|
|
1258
|
+
if (jsonResponse.has("kind") && !jsonResponse.isNull("kind")) {
|
|
1259
|
+
retError.put("kind", jsonResponse.getString("kind"));
|
|
1260
|
+
}
|
|
1261
|
+
if (jsonResponse.has("message") && !jsonResponse.isNull("message")) {
|
|
1262
|
+
retError.put("message", jsonResponse.getString("message"));
|
|
1263
|
+
} else {
|
|
1264
|
+
retError.put("message", "server did not provide a message");
|
|
1265
|
+
}
|
|
1266
|
+
if (jsonResponse.has("version") && !jsonResponse.isNull("version")) {
|
|
1267
|
+
retError.put("version", jsonResponse.getString("version"));
|
|
1268
|
+
}
|
|
1269
|
+
retError.put("statusCode", statusCode);
|
|
1270
|
+
callback.callback(retError);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1147
1274
|
// Check for 429 rate limit
|
|
1148
1275
|
if (checkAndHandleRateLimitResponse(response)) {
|
|
1149
1276
|
Map<String, Object> retError = new HashMap<>();
|
|
1150
1277
|
retError.put("message", "Rate limit exceeded");
|
|
1151
1278
|
retError.put("error", "rate_limit_exceeded");
|
|
1279
|
+
retError.put("kind", "failed");
|
|
1152
1280
|
retError.put("statusCode", statusCode);
|
|
1153
1281
|
callback.callback(retError);
|
|
1154
1282
|
return;
|
|
@@ -1158,27 +1286,14 @@ public class CapgoUpdater {
|
|
|
1158
1286
|
Map<String, Object> retError = new HashMap<>();
|
|
1159
1287
|
retError.put("message", "Server error: " + response.code());
|
|
1160
1288
|
retError.put("error", "response_error");
|
|
1289
|
+
retError.put("kind", "failed");
|
|
1161
1290
|
retError.put("statusCode", statusCode);
|
|
1162
1291
|
callback.callback(retError);
|
|
1163
1292
|
return;
|
|
1164
1293
|
}
|
|
1165
1294
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
JSONObject jsonResponse = new JSONObject(responseData);
|
|
1169
|
-
|
|
1170
|
-
// Check for server-side errors first
|
|
1171
|
-
if (jsonResponse.has("error")) {
|
|
1172
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1173
|
-
retError.put("error", jsonResponse.getString("error"));
|
|
1174
|
-
if (jsonResponse.has("message")) {
|
|
1175
|
-
retError.put("message", jsonResponse.getString("message"));
|
|
1176
|
-
} else {
|
|
1177
|
-
retError.put("message", "server did not provide a message");
|
|
1178
|
-
}
|
|
1179
|
-
retError.put("statusCode", statusCode);
|
|
1180
|
-
callback.callback(retError);
|
|
1181
|
-
return;
|
|
1295
|
+
if (jsonResponse == null) {
|
|
1296
|
+
throw new JSONException("Response is not a JSON object");
|
|
1182
1297
|
}
|
|
1183
1298
|
|
|
1184
1299
|
Map<String, Object> ret = new HashMap<>();
|
|
@@ -1200,6 +1315,7 @@ public class CapgoUpdater {
|
|
|
1200
1315
|
Map<String, Object> retError = new HashMap<>();
|
|
1201
1316
|
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
1202
1317
|
retError.put("error", "parse_error");
|
|
1318
|
+
retError.put("kind", "failed");
|
|
1203
1319
|
callback.callback(retError);
|
|
1204
1320
|
}
|
|
1205
1321
|
}
|
|
@@ -1700,7 +1816,13 @@ public class CapgoUpdater {
|
|
|
1700
1816
|
}
|
|
1701
1817
|
BundleInfo result;
|
|
1702
1818
|
if (BundleInfo.ID_BUILTIN.equals(trueId)) {
|
|
1703
|
-
result = new BundleInfo(
|
|
1819
|
+
result = new BundleInfo(
|
|
1820
|
+
trueId,
|
|
1821
|
+
this.versionBuild == null || this.versionBuild.isEmpty() ? null : this.versionBuild,
|
|
1822
|
+
BundleStatus.SUCCESS,
|
|
1823
|
+
"",
|
|
1824
|
+
""
|
|
1825
|
+
);
|
|
1704
1826
|
} else if (BundleInfo.VERSION_UNKNOWN.equals(trueId)) {
|
|
1705
1827
|
result = new BundleInfo(trueId, null, BundleStatus.ERROR, "", "");
|
|
1706
1828
|
} else {
|
|
@@ -1805,6 +1927,7 @@ public class CapgoUpdater {
|
|
|
1805
1927
|
}
|
|
1806
1928
|
|
|
1807
1929
|
public boolean setNextBundle(final String next) {
|
|
1930
|
+
BundleInfo bundleToNotify = null;
|
|
1808
1931
|
if (next == null) {
|
|
1809
1932
|
this.editor.remove(NEXT_VERSION);
|
|
1810
1933
|
} else {
|
|
@@ -1814,8 +1937,15 @@ public class CapgoUpdater {
|
|
|
1814
1937
|
}
|
|
1815
1938
|
this.editor.putString(NEXT_VERSION, next);
|
|
1816
1939
|
this.setBundleStatus(next, BundleStatus.PENDING);
|
|
1940
|
+
bundleToNotify = newBundle;
|
|
1817
1941
|
}
|
|
1818
1942
|
this.editor.commit();
|
|
1943
|
+
if (bundleToNotify != null) {
|
|
1944
|
+
this.sendStats("set_next", bundleToNotify.getVersionName(), this.getCurrentBundle().getVersionName());
|
|
1945
|
+
final Map<String, Object> payload = new HashMap<>();
|
|
1946
|
+
payload.put("bundle", bundleToNotify.toJSONMap());
|
|
1947
|
+
this.notifyListeners("setNext", payload);
|
|
1948
|
+
}
|
|
1819
1949
|
return true;
|
|
1820
1950
|
}
|
|
1821
1951
|
|
|
@@ -2,9 +2,12 @@ package ee.forgr.capacitor_updater;
|
|
|
2
2
|
|
|
3
3
|
import android.content.SharedPreferences;
|
|
4
4
|
import io.github.g00fy2.versioncompare.Version;
|
|
5
|
+
import java.text.ParsePosition;
|
|
5
6
|
import java.text.SimpleDateFormat;
|
|
6
7
|
import java.util.ArrayList;
|
|
7
8
|
import java.util.Date;
|
|
9
|
+
import java.util.Locale;
|
|
10
|
+
import java.util.TimeZone;
|
|
8
11
|
import org.json.JSONArray;
|
|
9
12
|
import org.json.JSONException;
|
|
10
13
|
import org.json.JSONObject;
|
|
@@ -98,25 +101,16 @@ public class DelayUpdateUtils {
|
|
|
98
101
|
break;
|
|
99
102
|
case date:
|
|
100
103
|
if (!"".equals(value)) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Date date = sdf.parse(value);
|
|
104
|
-
assert date != null;
|
|
104
|
+
Date date = parseDateCondition(value);
|
|
105
|
+
if (date != null) {
|
|
105
106
|
if (new Date().compareTo(date) > 0) {
|
|
106
107
|
logger.info("Date delay (value: " + value + ") condition removed due to expired date at index " + index);
|
|
107
108
|
} else {
|
|
108
109
|
delayConditionListToKeep.add(condition);
|
|
109
110
|
logger.info("Date delay (value: " + value + ") condition kept at index " + index);
|
|
110
111
|
}
|
|
111
|
-
}
|
|
112
|
-
logger.error(
|
|
113
|
-
"Date delay (value: " +
|
|
114
|
-
value +
|
|
115
|
-
") condition removed due to parsing issue at index " +
|
|
116
|
-
index +
|
|
117
|
-
" " +
|
|
118
|
-
e.getMessage()
|
|
119
|
-
);
|
|
112
|
+
} else {
|
|
113
|
+
logger.error("Date delay (value: " + value + ") condition removed due to parsing issue at index " + index);
|
|
120
114
|
}
|
|
121
115
|
} else {
|
|
122
116
|
logger.debug("Date delay (value: " + value + ") condition removed due to empty value at index " + index);
|
|
@@ -190,6 +184,48 @@ public class DelayUpdateUtils {
|
|
|
190
184
|
return conditions;
|
|
191
185
|
}
|
|
192
186
|
|
|
187
|
+
private Date parseDateCondition(String value) {
|
|
188
|
+
String[] patterns = {
|
|
189
|
+
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
|
190
|
+
"yyyy-MM-dd'T'HH:mm:ssXXX",
|
|
191
|
+
"yyyy-MM-dd'T'HH:mm:ss.SSSXX",
|
|
192
|
+
"yyyy-MM-dd'T'HH:mm:ssXX",
|
|
193
|
+
"yyyy-MM-dd'T'HH:mm:ss.SSSX",
|
|
194
|
+
"yyyy-MM-dd'T'HH:mm:ssX",
|
|
195
|
+
"yyyy-MM-dd'T'HH:mm:ss.SSS",
|
|
196
|
+
"yyyy-MM-dd'T'HH:mm:ss"
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
for (String pattern : patterns) {
|
|
200
|
+
Date parsed = parseDateWithPattern(value, pattern);
|
|
201
|
+
if (parsed != null) {
|
|
202
|
+
return parsed;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private Date parseDateWithPattern(String value, String pattern) {
|
|
210
|
+
try {
|
|
211
|
+
SimpleDateFormat sdf = new SimpleDateFormat(pattern, Locale.US);
|
|
212
|
+
sdf.setLenient(false);
|
|
213
|
+
|
|
214
|
+
// If no timezone is provided, keep historical behavior and interpret as local time.
|
|
215
|
+
if (!pattern.contains("X")) {
|
|
216
|
+
sdf.setTimeZone(TimeZone.getDefault());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
ParsePosition position = new ParsePosition(0);
|
|
220
|
+
Date parsed = sdf.parse(value, position);
|
|
221
|
+
if (parsed != null && position.getIndex() == value.length()) {
|
|
222
|
+
return parsed;
|
|
223
|
+
}
|
|
224
|
+
} catch (Exception ignored) {}
|
|
225
|
+
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
193
229
|
private String convertDelayConditionsToJson(ArrayList<DelayCondition> conditions) {
|
|
194
230
|
JSONArray array = new JSONArray();
|
|
195
231
|
for (DelayCondition condition : conditions) {
|
|
@@ -91,27 +91,52 @@ public class DownloadService extends Worker {
|
|
|
91
91
|
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
|
|
92
92
|
.addInterceptor((chain) -> {
|
|
93
93
|
Request originalRequest = chain.request();
|
|
94
|
-
String userAgent =
|
|
95
|
-
"CapacitorUpdater/" +
|
|
96
|
-
(currentPluginVersion != null ? currentPluginVersion : "unknown") +
|
|
97
|
-
" (" +
|
|
98
|
-
(currentAppId != null ? currentAppId : "unknown") +
|
|
99
|
-
") android/" +
|
|
100
|
-
(currentVersionOs != null ? currentVersionOs : "unknown");
|
|
94
|
+
String userAgent = buildUserAgent(currentAppId, currentPluginVersion, currentVersionOs);
|
|
101
95
|
Request requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build();
|
|
102
96
|
return chain.proceed(requestWithUserAgent);
|
|
103
97
|
})
|
|
104
98
|
.build();
|
|
105
99
|
}
|
|
106
100
|
|
|
101
|
+
static String buildUserAgent(String appId, String pluginVersion, String versionOs) {
|
|
102
|
+
return (
|
|
103
|
+
"CapacitorUpdater/" +
|
|
104
|
+
sanitizeUserAgentValue(pluginVersion) +
|
|
105
|
+
" (" +
|
|
106
|
+
sanitizeUserAgentValue(appId) +
|
|
107
|
+
") android/" +
|
|
108
|
+
sanitizeUserAgentValue(versionOs)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private static String sanitizeUserAgentValue(String value) {
|
|
113
|
+
if (value == null || value.isEmpty()) {
|
|
114
|
+
return "unknown";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
StringBuilder sanitized = new StringBuilder();
|
|
118
|
+
value
|
|
119
|
+
.codePoints()
|
|
120
|
+
.forEach((cp) -> {
|
|
121
|
+
boolean isVisibleAscii = cp >= 0x20 && cp <= 0x7E;
|
|
122
|
+
boolean isIso88591 = cp >= 0xA0 && cp <= 0xFF;
|
|
123
|
+
if (isVisibleAscii || isIso88591) {
|
|
124
|
+
sanitized.appendCodePoint(cp);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
String result = sanitized.toString().trim();
|
|
129
|
+
return result.isEmpty() ? "unknown" : result;
|
|
130
|
+
}
|
|
131
|
+
|
|
107
132
|
// Method to update User-Agent values
|
|
108
133
|
public static void updateUserAgent(String appId, String pluginVersion, String versionOs) {
|
|
109
|
-
currentAppId = appId
|
|
110
|
-
currentPluginVersion = pluginVersion
|
|
111
|
-
currentVersionOs = versionOs
|
|
112
|
-
logger
|
|
113
|
-
"Updated User-Agent:
|
|
114
|
-
|
|
134
|
+
currentAppId = sanitizeUserAgentValue(appId);
|
|
135
|
+
currentPluginVersion = sanitizeUserAgentValue(pluginVersion);
|
|
136
|
+
currentVersionOs = sanitizeUserAgentValue(versionOs);
|
|
137
|
+
if (logger != null) {
|
|
138
|
+
logger.debug("Updated User-Agent: " + buildUserAgent(currentAppId, currentPluginVersion, currentVersionOs));
|
|
139
|
+
}
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
public DownloadService(@NonNull Context context, @NonNull WorkerParameters params) {
|
|
@@ -249,12 +249,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
List<?> channelsRaw = (List<?>) channelsObj;
|
|
252
|
-
List<Map<String, Object>> channels =
|
|
253
|
-
for (Object item : channelsRaw) {
|
|
254
|
-
if (item instanceof Map) {
|
|
255
|
-
channels.add((Map<String, Object>) item);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
252
|
+
List<Map<String, Object>> channels = toChannelList(channelsRaw);
|
|
258
253
|
|
|
259
254
|
if (channels.isEmpty()) {
|
|
260
255
|
showError("No channels available for self-assignment");
|
|
@@ -273,6 +268,27 @@ public class ShakeMenu implements ShakeDetector.Listener {
|
|
|
273
268
|
});
|
|
274
269
|
}
|
|
275
270
|
|
|
271
|
+
private List<Map<String, Object>> toChannelList(List<?> channelsRaw) {
|
|
272
|
+
List<Map<String, Object>> channels = new ArrayList<>();
|
|
273
|
+
for (Object item : channelsRaw) {
|
|
274
|
+
if (!(item instanceof Map<?, ?> rawMap)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
Map<String, Object> channel = new java.util.HashMap<>();
|
|
279
|
+
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
|
|
280
|
+
if (entry.getKey() instanceof String key) {
|
|
281
|
+
channel.put(key, entry.getValue());
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!channel.isEmpty()) {
|
|
286
|
+
channels.add(channel);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return channels;
|
|
290
|
+
}
|
|
291
|
+
|
|
276
292
|
private void presentChannelPicker(List<Map<String, Object>> channels) {
|
|
277
293
|
try {
|
|
278
294
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
@@ -432,12 +448,36 @@ public class ShakeMenu implements ShakeDetector.Listener {
|
|
|
432
448
|
}
|
|
433
449
|
|
|
434
450
|
String latestError = getString(latestRes, "error");
|
|
451
|
+
String latestKind = getString(latestRes, "kind");
|
|
452
|
+
String latestMessage = getString(latestRes, "message");
|
|
453
|
+
|
|
454
|
+
String detail = latestMessage != null && !latestMessage.isEmpty()
|
|
455
|
+
? latestMessage
|
|
456
|
+
: latestError != null && !latestError.isEmpty()
|
|
457
|
+
? latestError
|
|
458
|
+
: latestKind != null && !latestKind.isEmpty()
|
|
459
|
+
? latestKind
|
|
460
|
+
: "server did not provide a message";
|
|
435
461
|
|
|
436
462
|
// Handle update errors first (before "no new version" check)
|
|
437
|
-
if (
|
|
463
|
+
if (
|
|
464
|
+
"failed".equals(latestKind) ||
|
|
465
|
+
(latestError != null &&
|
|
466
|
+
!latestError.isEmpty() &&
|
|
467
|
+
!"up_to_date".equals(latestKind) &&
|
|
468
|
+
!"blocked".equals(latestKind))
|
|
469
|
+
) {
|
|
470
|
+
activity.runOnUiThread(() -> {
|
|
471
|
+
progressDialog.dismiss();
|
|
472
|
+
showError("Channel set to " + channelName + ". Update check failed: " + detail);
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if ("blocked".equals(latestKind)) {
|
|
438
478
|
activity.runOnUiThread(() -> {
|
|
439
479
|
progressDialog.dismiss();
|
|
440
|
-
showError("Channel set to " + channelName + ". Update check
|
|
480
|
+
showError("Channel set to " + channelName + ". Update check blocked: " + detail);
|
|
441
481
|
});
|
|
442
482
|
return;
|
|
443
483
|
}
|
|
@@ -445,7 +485,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
|
|
|
445
485
|
String latestUrl = getString(latestRes, "url");
|
|
446
486
|
|
|
447
487
|
// Check if there's an actual update available
|
|
448
|
-
if ("
|
|
488
|
+
if ("up_to_date".equals(latestKind) || latestUrl == null || latestUrl.isEmpty()) {
|
|
449
489
|
activity.runOnUiThread(() -> {
|
|
450
490
|
progressDialog.dismiss();
|
|
451
491
|
showSuccess("Channel set to " + channelName + ". Already on latest version.");
|