@capgo/capacitor-updater 8.45.10 → 8.45.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -462,6 +462,7 @@ export default config;
462
462
  * [`removeAllListeners()`](#removealllisteners)
463
463
  * [`addListener('download', ...)`](#addlistenerdownload-)
464
464
  * [`addListener('noNeedUpdate', ...)`](#addlistenernoneedupdate-)
465
+ * [`addListener('updateCheckResult', ...)`](#addlistenerupdatecheckresult-)
465
466
  * [`addListener('updateAvailable', ...)`](#addlistenerupdateavailable-)
466
467
  * [`addListener('downloadComplete', ...)`](#addlistenerdownloadcomplete-)
467
468
  * [`addListener('breakingAvailable', ...)`](#addlistenerbreakingavailable-)
@@ -929,27 +930,23 @@ After receiving the latest version info, you can:
929
930
  2. Download it using {@link download}
930
931
  3. Apply it using {@link next} or {@link set}
931
932
 
932
- **Important: Error handling for "no new version available"**
933
+ **Important: Handling "no new version available"**
933
934
 
934
935
  When the device's current version matches the latest version on the server (i.e., the device is already
935
936
  up-to-date), the server returns a 200 response with `error: "no_new_version_available"` and
936
- `message: "No new version available"`. **This causes `getLatest()` to throw an error**, even though
937
- this is a normal, expected condition.
937
+ `message: "No new version available"`. This is a normal, expected condition and resolves with
938
+ `kind: "up_to_date"` when the backend provides that classification.
938
939
 
939
- You should catch this specific error to handle it gracefully:
940
+ You should check `kind` and `error` before attempting to download:
940
941
 
941
942
  ```typescript
942
- try {
943
- const latest = await CapacitorUpdater.getLatest();
943
+ const latest = await CapacitorUpdater.getLatest();
944
+ if (latest.kind === 'up_to_date') {
945
+ console.log('Already up to date');
946
+ } else if (latest.kind === 'blocked') {
947
+ console.log('Update is blocked:', latest.error);
948
+ } else if (latest.url) {
944
949
  // New version is available, proceed with download
945
- } catch (error) {
946
- if (error.message === 'No new version available') {
947
- // Device is already on the latest version - this is normal
948
- console.log('Already up to date');
949
- } else {
950
- // Actual error occurred
951
- console.error('Failed to check for updates:', error);
952
- }
953
950
  }
954
951
  ```
955
952
 
@@ -1251,6 +1248,7 @@ Remove all event listeners registered for this plugin.
1251
1248
  This unregisters all listeners added via {@link addListener} for all event types:
1252
1249
  - `download`
1253
1250
  - `noNeedUpdate`
1251
+ - `updateCheckResult`
1254
1252
  - `updateAvailable`
1255
1253
  - `downloadComplete`
1256
1254
  - `downloadFailed`
@@ -1308,6 +1306,31 @@ Listen for no need to update event, useful when you want force check every time
1308
1306
  --------------------
1309
1307
 
1310
1308
 
1309
+ #### addListener('updateCheckResult', ...)
1310
+
1311
+ ```typescript
1312
+ addListener(eventName: 'updateCheckResult', listenerFunc: (state: UpdateCheckResultEvent) => void) => Promise<PluginListenerHandle>
1313
+ ```
1314
+
1315
+ Listen for update check results before the updater decides whether to download.
1316
+ The backend can classify the <a href="#updatecheckresultevent">UpdateCheckResultEvent</a> payload as `up_to_date`, `blocked`, or `failed`.
1317
+
1318
+ This event is emitted alongside legacy events. For `up_to_date` and `blocked`, it is emitted before
1319
+ `noNeedUpdate` and does not emit `downloadFailed`. For `failed`, it is emitted before the legacy
1320
+ `downloadFailed` event and keeps the existing failure stats behavior.
1321
+
1322
+ | Param | Type |
1323
+ | ------------------ | --------------------------------------------------------------------------------------------- |
1324
+ | **`eventName`** | <code>'updateCheckResult'</code> |
1325
+ | **`listenerFunc`** | <code>(state: <a href="#updatecheckresultevent">UpdateCheckResultEvent</a>) =&gt; void</code> |
1326
+
1327
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
1328
+
1329
+ **Since:** 8.45.11
1330
+
1331
+ --------------------
1332
+
1333
+
1311
1334
  #### addListener('updateAvailable', ...)
1312
1335
 
1313
1336
  ```typescript
@@ -2094,20 +2117,22 @@ If you don't use backend, you need to provide the URL and version of the bundle.
2094
2117
 
2095
2118
  ##### LatestVersion
2096
2119
 
2097
- | Prop | Type | Description | Since |
2098
- | ---------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
2099
- | **`version`** | <code>string</code> | Result of getLatest method | 4.0.0 |
2100
- | **`checksum`** | <code>string</code> | | 6 |
2101
- | **`breaking`** | <code>boolean</code> | Indicates whether the update was flagged as breaking by the backend. | 7.22.0 |
2102
- | **`major`** | <code>boolean</code> | | |
2103
- | **`message`** | <code>string</code> | Optional message from the server. When no new version is available, this will be "No new version available". | |
2104
- | **`sessionKey`** | <code>string</code> | | |
2105
- | **`error`** | <code>string</code> | Error code from the server, if any. Common values: - `"no_new_version_available"`: Device is already on the latest version (not a failure) - Other error codes indicate actual failures in the update process | |
2106
- | **`old`** | <code>string</code> | The previous/current version name (provided for reference). | |
2107
- | **`url`** | <code>string</code> | Download URL for the bundle (when a new version is available). | |
2108
- | **`manifest`** | <code>ManifestEntry[]</code> | File list for delta updates (when using multi-file downloads). | 6.1 |
2109
- | **`link`** | <code>string</code> | Optional link associated with this bundle version (e.g., release notes URL, changelog, GitHub release). | 7.35.0 |
2110
- | **`comment`** | <code>string</code> | Optional comment or description for this bundle version. | 7.35.0 |
2120
+ | Prop | Type | Description | Since |
2121
+ | ---------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------- |
2122
+ | **`version`** | <code>string</code> | Result of getLatest method | 4.0.0 |
2123
+ | **`checksum`** | <code>string</code> | | 6 |
2124
+ | **`breaking`** | <code>boolean</code> | Indicates whether the update was flagged as breaking by the backend. | 7.22.0 |
2125
+ | **`major`** | <code>boolean</code> | | |
2126
+ | **`message`** | <code>string</code> | Optional message from the server. When no new version is available, this will be "No new version available". | |
2127
+ | **`sessionKey`** | <code>string</code> | | |
2128
+ | **`error`** | <code>string</code> | Error code from the server, if any. Use `kind` for classification instead of parsing this value. | |
2129
+ | **`kind`** | <code><a href="#updateresponsekind">UpdateResponseKind</a></code> | Classification for this response, provided by the backend. | 8.45.11 |
2130
+ | **`statusCode`** | <code>number</code> | HTTP status code returned by the update server for classified update-check responses. | 8.45.11 |
2131
+ | **`old`** | <code>string</code> | The previous/current version name (provided for reference). | |
2132
+ | **`url`** | <code>string</code> | Download URL for the bundle (when a new version is available). | |
2133
+ | **`manifest`** | <code>ManifestEntry[]</code> | File list for delta updates (when using multi-file downloads). | 6.1 |
2134
+ | **`link`** | <code>string</code> | Optional link associated with this bundle version (e.g., release notes URL, changelog, GitHub release). | 7.35.0 |
2135
+ | **`comment`** | <code>string</code> | Optional comment or description for this bundle version. | 7.35.0 |
2111
2136
 
2112
2137
 
2113
2138
  ##### GetLatestOptions
@@ -2226,6 +2251,18 @@ If you don't use backend, you need to provide the URL and version of the bundle.
2226
2251
  | **`bundle`** | <code><a href="#bundleinfo">BundleInfo</a></code> | Current status of download, between 0 and 100. | 4.0.0 |
2227
2252
 
2228
2253
 
2254
+ ##### UpdateCheckResultEvent
2255
+
2256
+ | Prop | Type | Description | Since |
2257
+ | ---------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- | ------- |
2258
+ | **`kind`** | <code><a href="#updateresponsekind">UpdateResponseKind</a></code> | Classification for the update check result, provided by the backend. | 8.45.11 |
2259
+ | **`error`** | <code>string</code> | Backend error code, when provided. | 8.45.11 |
2260
+ | **`message`** | <code>string</code> | Backend message, when provided. | 8.45.11 |
2261
+ | **`statusCode`** | <code>number</code> | HTTP status code returned by the update endpoint. | 8.45.11 |
2262
+ | **`version`** | <code>string</code> | Version referenced by the update check result. | 8.45.11 |
2263
+ | **`bundle`** | <code><a href="#bundleinfo">BundleInfo</a></code> | Current bundle on the device. | 8.45.11 |
2264
+
2265
+
2229
2266
  ##### UpdateAvailableEvent
2230
2267
 
2231
2268
  | Prop | Type | Description | Since |
@@ -2417,6 +2454,15 @@ error: The bundle has failed to download.
2417
2454
  <code>'background' | 'kill' | 'nativeVersion' | 'date'</code>
2418
2455
 
2419
2456
 
2457
+ ##### UpdateResponseKind
2458
+
2459
+ Classification for update-check responses that do not provide a downloadable bundle.
2460
+ The update backend provides this field directly. Missing or unknown values are treated as
2461
+ failed by native clients.
2462
+
2463
+ <code>'up_to_date' | 'blocked' | 'failed'</code>
2464
+
2465
+
2420
2466
  ##### BreakingAvailableEvent
2421
2467
 
2422
2468
  Payload emitted by {@link CapacitorUpdaterPlugin.addListener} with `breakingAvailable`.
@@ -50,7 +50,6 @@ import java.io.IOException;
50
50
  import java.net.MalformedURLException;
51
51
  import java.net.URL;
52
52
  import java.util.ArrayList;
53
- import java.util.Arrays;
54
53
  import java.util.Date;
55
54
  import java.util.HashSet;
56
55
  import java.util.List;
@@ -90,7 +89,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
90
89
  private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
91
90
  private static final long PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS = 30000L;
92
91
 
93
- private final String pluginVersion = "8.45.10";
92
+ private final String pluginVersion = "8.45.11";
94
93
  private static final String DELAY_CONDITION_PREFERENCES = "";
95
94
 
96
95
  private SharedPreferences.Editor editor;
@@ -1293,6 +1292,23 @@ public class CapacitorUpdaterPlugin extends Plugin {
1293
1292
  startNewThread(() ->
1294
1293
  CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
1295
1294
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1295
+ Object channels = res.get("channels");
1296
+ if (channels instanceof List<?> channelsList) {
1297
+ JSArray channelsArray = new JSArray();
1298
+ for (Object channel : channelsList) {
1299
+ if (channel instanceof Map<?, ?> channelMap) {
1300
+ JSObject channelObject = new JSObject();
1301
+ for (Map.Entry<?, ?> entry : channelMap.entrySet()) {
1302
+ Object key = entry.getKey();
1303
+ if (key != null) {
1304
+ channelObject.put(key.toString(), entry.getValue());
1305
+ }
1306
+ }
1307
+ channelsArray.put(channelObject);
1308
+ }
1309
+ }
1310
+ jsRes.put("channels", channelsArray);
1311
+ }
1296
1312
  if (jsRes.has("error")) {
1297
1313
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1298
1314
  String errorCode = jsRes.getString("error");
@@ -1582,23 +1598,25 @@ public class CapacitorUpdaterPlugin extends Plugin {
1582
1598
  call.reject("Set called without id");
1583
1599
  return;
1584
1600
  }
1585
- try {
1586
- logger.info("Setting active bundle " + id);
1587
- if (!this.implementation.set(id)) {
1588
- logger.info("No such bundle " + id);
1589
- call.reject("Update failed, id " + id + " does not exist.");
1590
- } else if (!this._reload()) {
1591
- logger.error("Reload failed after setting bundle " + id);
1592
- call.reject("Reload failed after setting bundle " + id);
1593
- } else {
1594
- logger.info("Bundle successfully set to " + id);
1595
- this.notifyBundleSet(this.implementation.getBundleInfo(id));
1596
- call.resolve();
1601
+ startNewThread(() -> {
1602
+ try {
1603
+ logger.info("Setting active bundle " + id);
1604
+ if (!this.implementation.set(id)) {
1605
+ logger.info("No such bundle " + id);
1606
+ call.reject("Update failed, id " + id + " does not exist.");
1607
+ } else if (!this._reload()) {
1608
+ logger.error("Reload failed after setting bundle " + id);
1609
+ call.reject("Reload failed after setting bundle " + id);
1610
+ } else {
1611
+ logger.info("Bundle successfully set to " + id);
1612
+ this.notifyBundleSet(this.implementation.getBundleInfo(id));
1613
+ call.resolve();
1614
+ }
1615
+ } catch (final Exception e) {
1616
+ logger.error("Could not set id " + id + " " + e.getMessage());
1617
+ call.reject("Could not set id " + id, e);
1597
1618
  }
1598
- } catch (final Exception e) {
1599
- logger.error("Could not set id " + id + " " + e.getMessage());
1600
- call.reject("Could not set id " + id, e);
1601
- }
1619
+ });
1602
1620
  }
1603
1621
 
1604
1622
  @PluginMethod
@@ -1685,11 +1703,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
1685
1703
  startNewThread(() ->
1686
1704
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
1687
1705
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1688
- if (jsRes.has("error")) {
1689
- String error = jsRes.getString("error");
1706
+ if (jsRes.has("error") || jsRes.has("kind")) {
1707
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1690
1708
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1691
- logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1692
- call.reject(jsRes.getString("error"));
1709
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
1710
+ jsRes.put("kind", kind);
1711
+ if ("failed".equals(kind)) {
1712
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1713
+ call.reject(error.isEmpty() ? errorMessage : error);
1714
+ } else {
1715
+ if (!jsRes.has("version") || jsRes.getString("version").isEmpty()) {
1716
+ jsRes.put("version", CapacitorUpdaterPlugin.this.implementation.getCurrentBundle().getVersionName());
1717
+ }
1718
+ logger.info("getLatest returned " + kind + ": " + errorMessage);
1719
+ call.resolve(jsRes);
1720
+ }
1693
1721
  return;
1694
1722
  } else if (jsRes.has("message")) {
1695
1723
  call.reject(jsRes.getString("message"));
@@ -1774,19 +1802,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
1774
1802
 
1775
1803
  @PluginMethod
1776
1804
  public void reset(final PluginCall call) {
1777
- try {
1778
- final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
1779
- final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
1780
- if (this._reset(toLastSuccessful, usePendingBundle)) {
1781
- call.resolve();
1782
- return;
1805
+ startNewThread(() -> {
1806
+ try {
1807
+ final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
1808
+ final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
1809
+ if (this._reset(toLastSuccessful, usePendingBundle)) {
1810
+ call.resolve();
1811
+ return;
1812
+ }
1813
+ logger.error("Reset failed");
1814
+ call.reject("Reset failed");
1815
+ } catch (final Exception e) {
1816
+ logger.error("Reset failed " + e.getMessage());
1817
+ call.reject("Reset failed", e);
1783
1818
  }
1784
- logger.error("Reset failed");
1785
- call.reject("Reset failed");
1786
- } catch (final Exception e) {
1787
- logger.error("Reset failed " + e.getMessage());
1788
- call.reject("Reset failed", e);
1789
- }
1819
+ });
1790
1820
  }
1791
1821
 
1792
1822
  @PluginMethod
@@ -1852,12 +1882,33 @@ public class CapacitorUpdaterPlugin extends Plugin {
1852
1882
  try {
1853
1883
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1854
1884
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1855
- if (jsRes.has("error")) {
1856
- String error = jsRes.getString("error");
1885
+ if (jsRes.has("error") || jsRes.has("kind")) {
1886
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1887
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1857
1888
  String errorMessage = jsRes.has("message")
1858
1889
  ? jsRes.getString("message")
1859
1890
  : "server did not provide a message";
1860
- logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1891
+ int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
1892
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(
1893
+ jsRes.has("kind") ? jsRes.getString("kind") : null
1894
+ );
1895
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
1896
+ CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
1897
+ kind,
1898
+ error,
1899
+ errorMessage,
1900
+ statusCode,
1901
+ latestVersion,
1902
+ current
1903
+ );
1904
+
1905
+ if ("failed".equals(kind)) {
1906
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1907
+ } else if ("blocked".equals(kind)) {
1908
+ logger.info("Update check blocked with error: " + error);
1909
+ } else {
1910
+ logger.info("No new version available");
1911
+ }
1861
1912
  } else if (jsRes.has("version")) {
1862
1913
  String newVersion = jsRes.getString("version");
1863
1914
  String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
@@ -1982,10 +2033,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
1982
2033
  this.checkAppReady(this.resolveAppReadyCheckTimeoutMs());
1983
2034
  }
1984
2035
 
2036
+ synchronized boolean shouldInterruptAppReadyCheck(final Thread existingCheck, final Thread currentThread) {
2037
+ return existingCheck != null && existingCheck != currentThread;
2038
+ }
2039
+
2040
+ synchronized void clearAppReadyCheckIfCurrent(final Thread expectedThread) {
2041
+ if (this.appReadyCheck == expectedThread) {
2042
+ this.appReadyCheck = null;
2043
+ }
2044
+ }
2045
+
1985
2046
  private void checkAppReady(final long waitTimeMs) {
1986
2047
  try {
1987
- if (this.appReadyCheck != null) {
1988
- this.appReadyCheck.interrupt();
2048
+ final Thread currentThread = Thread.currentThread();
2049
+ final Thread existingCheck = this.appReadyCheck;
2050
+ if (this.shouldInterruptAppReadyCheck(existingCheck, currentThread)) {
2051
+ existingCheck.interrupt();
1989
2052
  }
1990
2053
  this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck(waitTimeMs));
1991
2054
  } catch (final Exception e) {
@@ -2002,6 +2065,31 @@ public class CapacitorUpdaterPlugin extends Plugin {
2002
2065
  }
2003
2066
  }
2004
2067
 
2068
+ private String getUpdateResponseKind(final String kind) {
2069
+ if ("up_to_date".equals(kind) || "blocked".equals(kind) || "failed".equals(kind)) {
2070
+ return kind;
2071
+ }
2072
+ return "failed";
2073
+ }
2074
+
2075
+ private void notifyUpdateCheckResult(
2076
+ final String kind,
2077
+ final String error,
2078
+ final String message,
2079
+ final int statusCode,
2080
+ final String version,
2081
+ final BundleInfo current
2082
+ ) {
2083
+ JSObject ret = new JSObject();
2084
+ ret.put("kind", kind);
2085
+ ret.put("error", error);
2086
+ ret.put("message", message);
2087
+ ret.put("statusCode", statusCode);
2088
+ ret.put("version", version);
2089
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
2090
+ this.notifyListeners("updateCheckResult", ret);
2091
+ }
2092
+
2005
2093
  private void ensureBridgeSet() {
2006
2094
  if (this.bridge != null && this.bridge.getWebView() != null) {
2007
2095
  logger.setBridge(this.bridge);
@@ -2111,30 +2199,37 @@ public class CapacitorUpdaterPlugin extends Plugin {
2111
2199
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2112
2200
 
2113
2201
  // Handle network errors and other failures first
2114
- if (jsRes.has("error")) {
2115
- String error = jsRes.getString("error");
2202
+ if (jsRes.has("error") || jsRes.has("kind")) {
2203
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
2116
2204
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
2117
2205
  int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
2118
- boolean responseIsOk = statusCode >= 200 && statusCode < 300;
2119
-
2120
- logger.error(
2121
- "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
2122
- );
2206
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
2123
2207
  String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
2208
+ CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(kind, error, errorMessage, statusCode, latestVersion, current);
2124
2209
 
2210
+ if ("up_to_date".equals(kind)) {
2211
+ logger.info("No new version available");
2212
+ } else if ("blocked".equals(kind)) {
2213
+ logger.info("Update check blocked with error: " + error);
2214
+ } else {
2215
+ logger.error(
2216
+ "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
2217
+ );
2218
+ }
2219
+
2220
+ boolean isFailure = "failed".equals(kind);
2125
2221
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2126
2222
  errorMessage,
2127
2223
  latestVersion,
2128
2224
  current,
2129
- true,
2225
+ isFailure,
2130
2226
  plannedDirectUpdate,
2131
2227
  "download_fail",
2132
2228
  "downloadFailed",
2133
- !responseIsOk
2229
+ isFailure
2134
2230
  );
2135
2231
  return;
2136
2232
  }
2137
-
2138
2233
  try {
2139
2234
  final String latestVersionName = jsRes.getString("version");
2140
2235
 
@@ -2440,12 +2535,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
2440
2535
 
2441
2536
  @Override
2442
2537
  public void run() {
2538
+ final Thread currentThread = Thread.currentThread();
2443
2539
  try {
2444
2540
  logger.info("Wait for " + this.waitTimeMs + "ms, then check for notifyAppReady");
2445
2541
  Thread.sleep(this.waitTimeMs);
2446
2542
  CapacitorUpdaterPlugin.this.checkRevert();
2447
- CapacitorUpdaterPlugin.this.appReadyCheck = null;
2543
+ CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
2448
2544
  } catch (final InterruptedException e) {
2545
+ CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
2449
2546
  logger.info(DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
2450
2547
  }
2451
2548
  }
@@ -1229,6 +1229,7 @@ public class CapgoUpdater {
1229
1229
  Map<String, Object> retError = new HashMap<>();
1230
1230
  retError.put("message", "Request failed: " + e.getMessage());
1231
1231
  retError.put("error", "network_error");
1232
+ retError.put("kind", "failed");
1232
1233
  callback.callback(retError);
1233
1234
  }
1234
1235
 
@@ -1236,11 +1237,46 @@ public class CapgoUpdater {
1236
1237
  public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
1237
1238
  try (ResponseBody responseBody = response.body()) {
1238
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
+
1239
1274
  // Check for 429 rate limit
1240
1275
  if (checkAndHandleRateLimitResponse(response)) {
1241
1276
  Map<String, Object> retError = new HashMap<>();
1242
1277
  retError.put("message", "Rate limit exceeded");
1243
1278
  retError.put("error", "rate_limit_exceeded");
1279
+ retError.put("kind", "failed");
1244
1280
  retError.put("statusCode", statusCode);
1245
1281
  callback.callback(retError);
1246
1282
  return;
@@ -1250,27 +1286,14 @@ public class CapgoUpdater {
1250
1286
  Map<String, Object> retError = new HashMap<>();
1251
1287
  retError.put("message", "Server error: " + response.code());
1252
1288
  retError.put("error", "response_error");
1289
+ retError.put("kind", "failed");
1253
1290
  retError.put("statusCode", statusCode);
1254
1291
  callback.callback(retError);
1255
1292
  return;
1256
1293
  }
1257
1294
 
1258
- assert responseBody != null;
1259
- String responseData = responseBody.string();
1260
- JSONObject jsonResponse = new JSONObject(responseData);
1261
-
1262
- // Check for server-side errors first
1263
- if (jsonResponse.has("error")) {
1264
- Map<String, Object> retError = new HashMap<>();
1265
- retError.put("error", jsonResponse.getString("error"));
1266
- if (jsonResponse.has("message")) {
1267
- retError.put("message", jsonResponse.getString("message"));
1268
- } else {
1269
- retError.put("message", "server did not provide a message");
1270
- }
1271
- retError.put("statusCode", statusCode);
1272
- callback.callback(retError);
1273
- return;
1295
+ if (jsonResponse == null) {
1296
+ throw new JSONException("Response is not a JSON object");
1274
1297
  }
1275
1298
 
1276
1299
  Map<String, Object> ret = new HashMap<>();
@@ -1292,6 +1315,7 @@ public class CapgoUpdater {
1292
1315
  Map<String, Object> retError = new HashMap<>();
1293
1316
  retError.put("message", "JSON parse error: " + e.getMessage());
1294
1317
  retError.put("error", "parse_error");
1318
+ retError.put("kind", "failed");
1295
1319
  callback.callback(retError);
1296
1320
  }
1297
1321
  }
@@ -448,12 +448,36 @@ public class ShakeMenu implements ShakeDetector.Listener {
448
448
  }
449
449
 
450
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";
451
461
 
452
462
  // Handle update errors first (before "no new version" check)
453
- 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)) {
454
478
  activity.runOnUiThread(() -> {
455
479
  progressDialog.dismiss();
456
- showError("Channel set to " + channelName + ". Update check failed: " + latestError);
480
+ showError("Channel set to " + channelName + ". Update check blocked: " + detail);
457
481
  });
458
482
  return;
459
483
  }
@@ -461,7 +485,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
461
485
  String latestUrl = getString(latestRes, "url");
462
486
 
463
487
  // Check if there's an actual update available
464
- if ("no_new_version_available".equals(latestError) || latestUrl == null || latestUrl.isEmpty()) {
488
+ if ("up_to_date".equals(latestKind) || latestUrl == null || latestUrl.isEmpty()) {
465
489
  activity.runOnUiThread(() -> {
466
490
  progressDialog.dismiss();
467
491
  showSuccess("Channel set to " + channelName + ". Already on latest version.");