@capgo/capacitor-updater 6.43.5 → 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.
@@ -758,6 +758,20 @@ public class CapgoUpdater {
758
758
  this.editor.commit();
759
759
  }
760
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
+
761
775
  public void downloadBackground(
762
776
  final String url,
763
777
  final String version,
@@ -977,6 +991,64 @@ public class CapgoUpdater {
977
991
  return (bundle.isDirectory() && bundle.exists() && new File(bundle.getPath(), "/index.html").exists() && !bundleInfo.isDeleted());
978
992
  }
979
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
+
980
1052
  public Boolean set(final BundleInfo bundle) {
981
1053
  return this.set(bundle.getId());
982
1054
  }
@@ -1001,11 +1073,32 @@ public class CapgoUpdater {
1001
1073
  return false;
1002
1074
  }
1003
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
+
1004
1091
  public void autoReset() {
1005
1092
  final BundleInfo currentBundle = this.getCurrentBundle();
1006
1093
  if (!currentBundle.isBuiltin() && !this.bundleExists(currentBundle.getId())) {
1007
1094
  logger.info("Folder at bundle path does not exist. Triggering reset.");
1008
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();
1009
1102
  }
1010
1103
  }
1011
1104
 
@@ -1038,17 +1131,9 @@ public class CapgoUpdater {
1038
1131
 
1039
1132
  public void reset(final boolean internal) {
1040
1133
  logger.debug("reset: " + internal);
1041
- var currentBundleName = this.getCurrentBundle().getVersionName();
1042
- this.setCurrentBundle(new File("public"));
1043
- this.setFallbackBundle(null);
1044
- this.setNextBundle(null);
1045
- // Cancel any ongoing downloads
1046
- if (this.activity != null) {
1047
- DownloadWorkerManager.cancelAllDownloads(this.activity);
1048
- }
1049
- if (!internal) {
1050
- this.sendStats("reset", this.getCurrentBundle().getVersionName(), currentBundleName);
1051
- }
1134
+ final String currentBundleName = this.getCurrentBundle().getVersionName();
1135
+ this.prepareResetStateForTransition();
1136
+ this.finalizeResetTransition(currentBundleName, internal);
1052
1137
  }
1053
1138
 
1054
1139
  private JSONObject createInfoObject() throws JSONException {
@@ -1144,6 +1229,7 @@ public class CapgoUpdater {
1144
1229
  Map<String, Object> retError = new HashMap<>();
1145
1230
  retError.put("message", "Request failed: " + e.getMessage());
1146
1231
  retError.put("error", "network_error");
1232
+ retError.put("kind", "failed");
1147
1233
  callback.callback(retError);
1148
1234
  }
1149
1235
 
@@ -1151,11 +1237,46 @@ public class CapgoUpdater {
1151
1237
  public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
1152
1238
  try (ResponseBody responseBody = response.body()) {
1153
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
+
1154
1274
  // Check for 429 rate limit
1155
1275
  if (checkAndHandleRateLimitResponse(response)) {
1156
1276
  Map<String, Object> retError = new HashMap<>();
1157
1277
  retError.put("message", "Rate limit exceeded");
1158
1278
  retError.put("error", "rate_limit_exceeded");
1279
+ retError.put("kind", "failed");
1159
1280
  retError.put("statusCode", statusCode);
1160
1281
  callback.callback(retError);
1161
1282
  return;
@@ -1165,27 +1286,14 @@ public class CapgoUpdater {
1165
1286
  Map<String, Object> retError = new HashMap<>();
1166
1287
  retError.put("message", "Server error: " + response.code());
1167
1288
  retError.put("error", "response_error");
1289
+ retError.put("kind", "failed");
1168
1290
  retError.put("statusCode", statusCode);
1169
1291
  callback.callback(retError);
1170
1292
  return;
1171
1293
  }
1172
1294
 
1173
- assert responseBody != null;
1174
- String responseData = responseBody.string();
1175
- JSONObject jsonResponse = new JSONObject(responseData);
1176
-
1177
- // Check for server-side errors first
1178
- if (jsonResponse.has("error")) {
1179
- Map<String, Object> retError = new HashMap<>();
1180
- retError.put("error", jsonResponse.getString("error"));
1181
- if (jsonResponse.has("message")) {
1182
- retError.put("message", jsonResponse.getString("message"));
1183
- } else {
1184
- retError.put("message", "server did not provide a message");
1185
- }
1186
- retError.put("statusCode", statusCode);
1187
- callback.callback(retError);
1188
- return;
1295
+ if (jsonResponse == null) {
1296
+ throw new JSONException("Response is not a JSON object");
1189
1297
  }
1190
1298
 
1191
1299
  Map<String, Object> ret = new HashMap<>();
@@ -1207,6 +1315,7 @@ public class CapgoUpdater {
1207
1315
  Map<String, Object> retError = new HashMap<>();
1208
1316
  retError.put("message", "JSON parse error: " + e.getMessage());
1209
1317
  retError.put("error", "parse_error");
1318
+ retError.put("kind", "failed");
1210
1319
  callback.callback(retError);
1211
1320
  }
1212
1321
  }
@@ -1707,7 +1816,13 @@ public class CapgoUpdater {
1707
1816
  }
1708
1817
  BundleInfo result;
1709
1818
  if (BundleInfo.ID_BUILTIN.equals(trueId)) {
1710
- result = new BundleInfo(trueId, null, BundleStatus.SUCCESS, "", "");
1819
+ result = new BundleInfo(
1820
+ trueId,
1821
+ this.versionBuild == null || this.versionBuild.isEmpty() ? null : this.versionBuild,
1822
+ BundleStatus.SUCCESS,
1823
+ "",
1824
+ ""
1825
+ );
1711
1826
  } else if (BundleInfo.VERSION_UNKNOWN.equals(trueId)) {
1712
1827
  result = new BundleInfo(trueId, null, BundleStatus.ERROR, "", "");
1713
1828
  } else {
@@ -1812,6 +1927,7 @@ public class CapgoUpdater {
1812
1927
  }
1813
1928
 
1814
1929
  public boolean setNextBundle(final String next) {
1930
+ BundleInfo bundleToNotify = null;
1815
1931
  if (next == null) {
1816
1932
  this.editor.remove(NEXT_VERSION);
1817
1933
  } else {
@@ -1821,8 +1937,15 @@ public class CapgoUpdater {
1821
1937
  }
1822
1938
  this.editor.putString(NEXT_VERSION, next);
1823
1939
  this.setBundleStatus(next, BundleStatus.PENDING);
1940
+ bundleToNotify = newBundle;
1824
1941
  }
1825
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
+ }
1826
1949
  return true;
1827
1950
  }
1828
1951
 
@@ -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 != null ? appId : "unknown";
110
- currentPluginVersion = pluginVersion != null ? pluginVersion : "unknown";
111
- currentVersionOs = versionOs != null ? versionOs : "unknown";
112
- logger.debug(
113
- "Updated User-Agent: CapacitorUpdater/" + currentPluginVersion + " (" + currentAppId + ") android/" + currentVersionOs
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 = new ArrayList<>();
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 (latestError != null && !latestError.isEmpty() && !"no_new_version_available".equals(latestError)) {
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 failed: " + latestError);
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 ("no_new_version_available".equals(latestError) || latestUrl == null || latestUrl.isEmpty()) {
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.");