@capgo/capacitor-updater 6.27.11 → 6.34.0

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
@@ -66,6 +66,14 @@ The most complete [documentation here](https://capgo.app/docs/).
66
66
  ## Community
67
67
  Join the [discord](https://discord.gg/VnYRvBfgA6) to get help.
68
68
 
69
+ ## Migration to v7.34
70
+
71
+ - **Channel storage change**: `setChannel()` now stores channel assignments locally on the device instead of in the cloud. This provides better offline support and reduces backend load.
72
+ - Channel assignments persist between app restarts
73
+ - Use `unsetChannel()` to clear the local assignment and revert to `defaultChannel`
74
+ - Old devices (< v7.34.0) will continue using cloud-based storage
75
+ - **New event**: Listen to the `channelPrivate` event to handle cases where a user tries to assign themselves to a private channel (one that doesn't allow self-assignment). See example in the `setChannel()` documentation above.
76
+
69
77
  ## Migration to v7
70
78
 
71
79
  - `privateKey` is not available anymore, it was used for the old encryption method. to migrate follow this guide : [https://capgo.app/docs/plugin/cloud-mode/getting-started/](https://capgo.app/docs/cli/migrations/encryption/)
@@ -272,6 +280,7 @@ CapacitorUpdater can be configured with these options:
272
280
  | **`allowManualBundleError`** | <code>boolean</code> | Allow marking bundles as errored from JavaScript while using manual update flows. When enabled, {@link CapacitorUpdaterPlugin.setBundleError} can change a bundle status to `error`. | <code>false</code> | 7.20.0 |
273
281
  | **`persistCustomId`** | <code>boolean</code> | Persist the customId set through {@link CapacitorUpdaterPlugin.setCustomId} across app restarts. Only available for Android and iOS. | <code>false (will be true by default in a future major release v8.x.x)</code> | 7.17.3 |
274
282
  | **`persistModifyUrl`** | <code>boolean</code> | Persist the updateUrl, statsUrl and channelUrl set through {@link CapacitorUpdaterPlugin.setUpdateUrl}, {@link CapacitorUpdaterPlugin.setStatsUrl} and {@link CapacitorUpdaterPlugin.setChannelUrl} across app restarts. Only available for Android and iOS. | <code>false</code> | 7.20.0 |
283
+ | **`allowSetDefaultChannel`** | <code>boolean</code> | Allow or disallow the {@link CapacitorUpdaterPlugin.setChannel} method to modify the defaultChannel. When set to `false`, calling `setChannel()` will return an error with code `disabled_by_config`. | <code>true</code> | 7.34.0 |
275
284
  | **`defaultChannel`** | <code>string</code> | Set the default channel for the app in the config. Case sensitive. This will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud. This requires the channel to allow devices to self dissociate/associate in the channel settings. https://capgo.app/docs/public-api/channels/#channel-configuration-options | <code>undefined</code> | 5.5.0 |
276
285
  | **`appId`** | <code>string</code> | Configure the app id for the app in the config. | <code>undefined</code> | 6.0.0 |
277
286
  | **`keepUrlPathAfterReload`** | <code>boolean</code> | Configure the plugin to keep the URL path after a reload. WARNING: When a reload is triggered, 'window.history' will be cleared. | <code>false</code> | 6.8.0 |
@@ -315,6 +324,7 @@ In `capacitor.config.json`:
315
324
  "allowManualBundleError": undefined,
316
325
  "persistCustomId": undefined,
317
326
  "persistModifyUrl": undefined,
327
+ "allowSetDefaultChannel": undefined,
318
328
  "defaultChannel": undefined,
319
329
  "appId": undefined,
320
330
  "keepUrlPathAfterReload": undefined,
@@ -364,6 +374,7 @@ const config: CapacitorConfig = {
364
374
  allowManualBundleError: undefined,
365
375
  persistCustomId: undefined,
366
376
  persistModifyUrl: undefined,
377
+ allowSetDefaultChannel: undefined,
367
378
  defaultChannel: undefined,
368
379
  appId: undefined,
369
380
  keepUrlPathAfterReload: undefined,
@@ -419,6 +430,7 @@ export default config;
419
430
  * [`addListener('downloadFailed', ...)`](#addlistenerdownloadfailed-)
420
431
  * [`addListener('appReloaded', ...)`](#addlistenerappreloaded-)
421
432
  * [`addListener('appReady', ...)`](#addlistenerappready-)
433
+ * [`addListener('channelPrivate', ...)`](#addlistenerchannelprivate-)
422
434
  * [`isAutoUpdateAvailable()`](#isautoupdateavailable)
423
435
  * [`getNextBundle()`](#getnextbundle)
424
436
  * [`getFailedUpdate()`](#getfailedupdate)
@@ -866,6 +878,34 @@ After receiving the latest version info, you can:
866
878
  2. Download it using {@link download}
867
879
  3. Apply it using {@link next} or {@link set}
868
880
 
881
+ **Important: Error handling for "no new version available"**
882
+
883
+ When the device's current version matches the latest version on the server (i.e., the device is already
884
+ up-to-date), the server returns a 200 response with `error: "no_new_version_available"` and
885
+ `message: "No new version available"`. **This causes `getLatest()` to throw an error**, even though
886
+ this is a normal, expected condition.
887
+
888
+ You should catch this specific error to handle it gracefully:
889
+
890
+ ```typescript
891
+ try {
892
+ const latest = await CapacitorUpdater.getLatest();
893
+ // New version is available, proceed with download
894
+ } catch (error) {
895
+ if (error.message === 'No new version available') {
896
+ // Device is already on the latest version - this is normal
897
+ console.log('Already up to date');
898
+ } else {
899
+ // Actual error occurred
900
+ console.error('Failed to check for updates:', error);
901
+ }
902
+ }
903
+ ```
904
+
905
+ In this scenario, the server:
906
+ - Logs the request with a "No new version available" message
907
+ - Sends a "noNew" stat action to track that the device checked for updates but was already current (done on the backend)
908
+
869
909
  | Param | Type | Description |
870
910
  | ------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
871
911
  | **`options`** | <code><a href="#getlatestoptions">GetLatestOptions</a></code> | Optional {@link <a href="#getlatestoptions">GetLatestOptions</a>} to specify which channel to check. |
@@ -901,6 +941,19 @@ Channels allow you to distribute different bundle versions to different groups o
901
941
  - At app boot/initialization - use {@link PluginsConfig.CapacitorUpdater.defaultChannel} config instead
902
942
  - Before user interaction
903
943
 
944
+ **Important: Listen for the `channelPrivate` event**
945
+
946
+ When a user attempts to set a channel that doesn't allow device self-assignment, the method will
947
+ throw an error AND fire a {@link addListener}('channelPrivate') event. You should listen to this event
948
+ to provide appropriate feedback to users:
949
+
950
+ ```typescript
951
+ CapacitorUpdater.addListener('channelPrivate', (data) =&gt; {
952
+ console.warn(`Cannot access channel "${data.channel}": ${data.message}`);
953
+ // Show user-friendly message
954
+ });
955
+ ```
956
+
904
957
  This sends a request to the Capgo backend linking your device ID to the specified channel.
905
958
 
906
959
  | Param | Type | Description |
@@ -1365,6 +1418,31 @@ Listen for app ready event in the App, let you know when app is ready to use, th
1365
1418
  --------------------
1366
1419
 
1367
1420
 
1421
+ #### addListener('channelPrivate', ...)
1422
+
1423
+ ```typescript
1424
+ addListener(eventName: 'channelPrivate', listenerFunc: (state: ChannelPrivateEvent) => void) => Promise<PluginListenerHandle>
1425
+ ```
1426
+
1427
+ Listen for channel private event, fired when attempting to set a channel that doesn't allow device self-assignment.
1428
+
1429
+ This event is useful for:
1430
+ - Informing users they don't have permission to switch to a specific channel
1431
+ - Implementing custom error handling for channel restrictions
1432
+ - Logging unauthorized channel access attempts
1433
+
1434
+ | Param | Type |
1435
+ | ------------------ | --------------------------------------------------------------------------------------- |
1436
+ | **`eventName`** | <code>'channelPrivate'</code> |
1437
+ | **`listenerFunc`** | <code>(state: <a href="#channelprivateevent">ChannelPrivateEvent</a>) =&gt; void</code> |
1438
+
1439
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
1440
+
1441
+ **Since:** 7.34.0
1442
+
1443
+ --------------------
1444
+
1445
+
1368
1446
  #### isAutoUpdateAvailable()
1369
1447
 
1370
1448
  ```typescript
@@ -1681,18 +1759,18 @@ If you don't use backend, you need to provide the URL and version of the bundle.
1681
1759
 
1682
1760
  ##### LatestVersion
1683
1761
 
1684
- | Prop | Type | Description | Since |
1685
- | ---------------- | ---------------------------- | -------------------------------------------------------------------- | ------ |
1686
- | **`version`** | <code>string</code> | Result of getLatest method | 4.0.0 |
1687
- | **`checksum`** | <code>string</code> | | 6 |
1688
- | **`breaking`** | <code>boolean</code> | Indicates whether the update was flagged as breaking by the backend. | 7.22.0 |
1689
- | **`major`** | <code>boolean</code> | | |
1690
- | **`message`** | <code>string</code> | | |
1691
- | **`sessionKey`** | <code>string</code> | | |
1692
- | **`error`** | <code>string</code> | | |
1693
- | **`old`** | <code>string</code> | | |
1694
- | **`url`** | <code>string</code> | | |
1695
- | **`manifest`** | <code>ManifestEntry[]</code> | | 6.1 |
1762
+ | Prop | Type | Description | Since |
1763
+ | ---------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
1764
+ | **`version`** | <code>string</code> | Result of getLatest method | 4.0.0 |
1765
+ | **`checksum`** | <code>string</code> | | 6 |
1766
+ | **`breaking`** | <code>boolean</code> | Indicates whether the update was flagged as breaking by the backend. | 7.22.0 |
1767
+ | **`major`** | <code>boolean</code> | | |
1768
+ | **`message`** | <code>string</code> | Optional message from the server. When no new version is available, this will be "No new version available". | |
1769
+ | **`sessionKey`** | <code>string</code> | | |
1770
+ | **`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 | |
1771
+ | **`old`** | <code>string</code> | The previous/current version name (provided for reference). | |
1772
+ | **`url`** | <code>string</code> | Download URL for the bundle (when a new version is available). | |
1773
+ | **`manifest`** | <code>ManifestEntry[]</code> | File list for partial updates (when using multi-file downloads). | 6.1 |
1696
1774
 
1697
1775
 
1698
1776
  ##### GetLatestOptions
@@ -1854,6 +1932,14 @@ If you don't use backend, you need to provide the URL and version of the bundle.
1854
1932
  | **`status`** | <code>string</code> | | |
1855
1933
 
1856
1934
 
1935
+ ##### ChannelPrivateEvent
1936
+
1937
+ | Prop | Type | Description | Since |
1938
+ | ------------- | ------------------- | ----------------------------------------------------------------------------------- | ------ |
1939
+ | **`channel`** | <code>string</code> | Emitted when attempting to set a channel that doesn't allow device self-assignment. | 7.34.0 |
1940
+ | **`message`** | <code>string</code> | | |
1941
+
1942
+
1857
1943
  ##### AutoUpdateAvailable
1858
1944
 
1859
1945
  | Prop | Type |
@@ -49,16 +49,16 @@ repositories {
49
49
 
50
50
 
51
51
  dependencies {
52
- def work_version = "2.11.0"
52
+ def work_version = "2.10.5"
53
53
  implementation "androidx.work:work-runtime:$work_version"
54
54
  implementation "com.google.android.gms:play-services-tasks:18.4.0"
55
- implementation "com.google.guava:guava:33.4.8-android"
55
+ implementation "com.google.guava:guava:33.5.0-android"
56
56
  implementation fileTree(dir: 'libs', include: ['*.jar'])
57
57
  implementation project(':capacitor-android')
58
58
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
59
59
  implementation 'io.github.g00fy2:versioncompare:1.5.0'
60
60
  testImplementation "junit:junit:$junitVersion"
61
- testImplementation 'org.mockito:mockito-core:5.14.2'
61
+ testImplementation 'org.mockito:mockito-core:5.20.0'
62
62
  testImplementation 'org.json:json:20250517'
63
63
  testImplementation 'org.robolectric:robolectric:4.13'
64
64
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
@@ -68,6 +68,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
68
68
  private static final String UPDATE_URL_PREF_KEY = "CapacitorUpdater.updateUrl";
69
69
  private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
70
70
  private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
71
+ private static final String DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.defaultChannel";
71
72
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
72
73
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
73
74
 
@@ -100,6 +101,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
100
101
  private Boolean onLaunchDirectUpdateUsed = false;
101
102
  Boolean shakeMenuEnabled = false;
102
103
  private Boolean allowManualBundleError = false;
104
+ private Boolean allowSetDefaultChannel = true;
103
105
 
104
106
  private Boolean isPreviousMainActivity = true;
105
107
 
@@ -283,8 +285,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
283
285
  }
284
286
 
285
287
  // Set logger for shared classes
286
- CryptoCipherV1.setLogger(logger);
287
- CryptoCipherV2.setLogger(logger);
288
+ CryptoCipher.setLogger(logger);
288
289
  DownloadService.setLogger(logger);
289
290
  DownloadWorkerManager.setLogger(logger);
290
291
 
@@ -302,6 +303,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
302
303
 
303
304
  this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
304
305
  this.persistModifyUrl = this.getConfig().getBoolean("persistModifyUrl", false);
306
+ this.allowSetDefaultChannel = this.getConfig().getBoolean("allowSetDefaultChannel", true);
305
307
  this.implementation.publicKey = this.getConfig().getString("publicKey", "");
306
308
  this.implementation.privateKey = this.getConfig().getString("privateKey", "");
307
309
  if (this.implementation.privateKey != null && !this.implementation.privateKey.isEmpty()) {
@@ -325,8 +327,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
325
327
  }
326
328
  }
327
329
  }
330
+
331
+ // Load defaultChannel: first try from persistent storage (set via setChannel), then fall back to config
332
+ if (this.prefs.contains(DEFAULT_CHANNEL_PREF_KEY)) {
333
+ final String storedDefaultChannel = this.prefs.getString(DEFAULT_CHANNEL_PREF_KEY, "");
334
+ if (storedDefaultChannel != null && !storedDefaultChannel.isEmpty()) {
335
+ this.implementation.defaultChannel = storedDefaultChannel;
336
+ logger.info("Loaded persisted defaultChannel from setChannel()");
337
+ } else {
338
+ this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
339
+ }
340
+ } else {
341
+ this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
342
+ }
343
+
328
344
  int userValue = this.getConfig().getInt("periodCheckDelay", 0);
329
- this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
330
345
 
331
346
  if (userValue >= 0 && userValue <= 600) {
332
347
  this.periodCheckDelay = 600 * 1000;
@@ -708,6 +723,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
708
723
  }
709
724
  }
710
725
  this.implementation.cleanupDownloadDirectories(allowedIds);
726
+ this.implementation.cleanupDeltaCache();
711
727
  }
712
728
  this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
713
729
  this.editor.apply();
@@ -867,27 +883,33 @@ public class CapacitorUpdaterPlugin extends Plugin {
867
883
 
868
884
  try {
869
885
  logger.info("unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
870
- startNewThread(() ->
871
- CapacitorUpdaterPlugin.this.implementation.unsetChannel((res) -> {
872
- JSObject jsRes = mapToJSObject(res);
873
- if (jsRes.has("error")) {
874
- String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
875
- String errorCode = jsRes.getString("error");
876
-
877
- JSObject errorObj = new JSObject();
878
- errorObj.put("message", errorMessage);
879
- errorObj.put("error", errorCode);
880
-
881
- call.reject(errorMessage, "UNSETCHANNEL_FAILED", null, errorObj);
882
- } else {
883
- if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
884
- logger.info("Calling autoupdater after channel change!");
885
- backgroundDownload();
886
+ startNewThread(() -> {
887
+ String configDefaultChannel = CapacitorUpdaterPlugin.this.getConfig().getString("defaultChannel", "");
888
+ CapacitorUpdaterPlugin.this.implementation.unsetChannel(
889
+ CapacitorUpdaterPlugin.this.editor,
890
+ DEFAULT_CHANNEL_PREF_KEY,
891
+ configDefaultChannel,
892
+ (res) -> {
893
+ JSObject jsRes = mapToJSObject(res);
894
+ if (jsRes.has("error")) {
895
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
896
+ String errorCode = jsRes.getString("error");
897
+
898
+ JSObject errorObj = new JSObject();
899
+ errorObj.put("message", errorMessage);
900
+ errorObj.put("error", errorCode);
901
+
902
+ call.reject(errorMessage, "UNSETCHANNEL_FAILED", null, errorObj);
903
+ } else {
904
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
905
+ logger.info("Calling autoupdater after channel change!");
906
+ backgroundDownload();
907
+ }
908
+ call.resolve(jsRes);
886
909
  }
887
- call.resolve(jsRes);
888
910
  }
889
- })
890
- );
911
+ );
912
+ });
891
913
  } catch (final Exception e) {
892
914
  logger.error("Failed to unsetChannel: " + e.getMessage());
893
915
  call.reject("Failed to unsetChannel: ", e);
@@ -910,25 +932,42 @@ public class CapacitorUpdaterPlugin extends Plugin {
910
932
  try {
911
933
  logger.info("setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
912
934
  startNewThread(() ->
913
- CapacitorUpdaterPlugin.this.implementation.setChannel(channel, (res) -> {
914
- JSObject jsRes = mapToJSObject(res);
915
- if (jsRes.has("error")) {
916
- String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
917
- String errorCode = jsRes.getString("error");
935
+ CapacitorUpdaterPlugin.this.implementation.setChannel(
936
+ channel,
937
+ CapacitorUpdaterPlugin.this.editor,
938
+ DEFAULT_CHANNEL_PREF_KEY,
939
+ CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
940
+ (res) -> {
941
+ JSObject jsRes = mapToJSObject(res);
942
+ if (jsRes.has("error")) {
943
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
944
+ String errorCode = jsRes.getString("error");
945
+
946
+ // Fire channelPrivate event if channel doesn't allow self-assignment
947
+ if (
948
+ errorCode.contains("cannot_update_via_private_channel") ||
949
+ errorCode.contains("channel_self_set_not_allowed")
950
+ ) {
951
+ JSObject eventData = new JSObject();
952
+ eventData.put("channel", channel);
953
+ eventData.put("message", errorMessage);
954
+ notifyListeners("channelPrivate", eventData);
955
+ }
918
956
 
919
- JSObject errorObj = new JSObject();
920
- errorObj.put("message", errorMessage);
921
- errorObj.put("error", errorCode);
957
+ JSObject errorObj = new JSObject();
958
+ errorObj.put("message", errorMessage);
959
+ errorObj.put("error", errorCode);
922
960
 
923
- call.reject(errorMessage, "SETCHANNEL_FAILED", null, errorObj);
924
- } else {
925
- if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
926
- logger.info("Calling autoupdater after channel change!");
927
- backgroundDownload();
961
+ call.reject(errorMessage, "SETCHANNEL_FAILED", null, errorObj);
962
+ } else {
963
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
964
+ logger.info("Calling autoupdater after channel change!");
965
+ backgroundDownload();
966
+ }
967
+ call.resolve(jsRes);
928
968
  }
929
- call.resolve(jsRes);
930
969
  }
931
- })
970
+ )
932
971
  );
933
972
  } catch (final Exception e) {
934
973
  logger.error("Failed to setChannel: " + channel + " " + e.getMessage());
@@ -400,16 +400,16 @@ public class CapgoUpdater {
400
400
 
401
401
  if (!this.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty()) {
402
402
  // V2 Encryption (publicKey)
403
- CryptoCipherV2.decryptFile(downloaded, publicKey, sessionKey);
404
- checksumDecrypted = CryptoCipherV2.decryptChecksum(checksumRes, publicKey);
405
- checksum = CryptoCipherV2.calcChecksum(downloaded);
403
+ CryptoCipher.decryptFile(downloaded, publicKey, sessionKey);
404
+ checksumDecrypted = CryptoCipher.decryptChecksum(checksumRes, publicKey);
405
+ checksum = CryptoCipher.calcChecksum(downloaded);
406
406
  } else if (this.hasOldPrivateKeyPropertyInConfig) {
407
- // V1 Encryption (privateKey) - deprecated but supported
408
- CryptoCipherV1.decryptFile(downloaded, privateKey, sessionKey, version);
409
- checksum = CryptoCipherV1.calcChecksum(downloaded);
410
- } else {
411
- checksum = CryptoCipherV2.calcChecksum(downloaded);
407
+ // V1 Encryption (privateKey) - deprecated not supported
408
+ this.sendStats("checksum_fail");
409
+ throw new IOException("V1 decryption is no longer supported for security reasons.");
412
410
  }
411
+ CryptoCipher.logChecksumInfo("Calculated checksum", checksum);
412
+ CryptoCipher.logChecksumInfo("Expected checksum", checksumDecrypted);
413
413
  if ((!checksumDecrypted.isEmpty() || !this.publicKey.isEmpty()) && !checksumDecrypted.equals(checksum)) {
414
414
  logger.error("Error checksum '" + checksumDecrypted + "' '" + checksum + "' '");
415
415
  this.sendStats("checksum_fail");
@@ -498,6 +498,23 @@ public class CapgoUpdater {
498
498
  }
499
499
  }
500
500
 
501
+ public void cleanupDeltaCache() {
502
+ if (this.activity == null) {
503
+ logger.warn("Activity is null, skipping delta cache cleanup");
504
+ return;
505
+ }
506
+ final File cacheFolder = new File(this.activity.getCacheDir(), "capgo_downloads");
507
+ if (!cacheFolder.exists()) {
508
+ return;
509
+ }
510
+ try {
511
+ this.deleteDirectory(cacheFolder);
512
+ logger.info("Cleaned up delta cache folder");
513
+ } catch (IOException e) {
514
+ logger.error("Failed to cleanup delta cache: " + e.getMessage());
515
+ }
516
+ }
517
+
501
518
  public void cleanupDownloadDirectories(final Set<String> allowedIds) {
502
519
  if (this.documentsDir == null) {
503
520
  logger.warn("Documents directory is null, skipping download cleanup");
@@ -956,115 +973,41 @@ public class CapgoUpdater {
956
973
  makeJsonRequest(updateUrl, json, callback);
957
974
  }
958
975
 
959
- public void unsetChannel(final Callback callback) {
960
- // Check if rate limit was exceeded
961
- if (rateLimitExceeded) {
962
- logger.debug("Skipping unsetChannel due to rate limit (429). Requests will resume after app restart.");
963
- final Map<String, Object> retError = new HashMap<>();
964
- retError.put("message", "Rate limit exceeded");
965
- retError.put("error", "rate_limit_exceeded");
966
- callback.callback(retError);
967
- return;
968
- }
976
+ public void unsetChannel(
977
+ final SharedPreferences.Editor editor,
978
+ final String defaultChannelKey,
979
+ final String configDefaultChannel,
980
+ final Callback callback
981
+ ) {
982
+ // Clear persisted defaultChannel and revert to config value
983
+ editor.remove(defaultChannelKey);
984
+ editor.apply();
985
+ this.defaultChannel = configDefaultChannel;
986
+ logger.info("Persisted defaultChannel cleared, reverted to config value: " + configDefaultChannel);
987
+
988
+ Map<String, Object> ret = new HashMap<>();
989
+ ret.put("status", "ok");
990
+ ret.put("message", "Channel override removed");
991
+ callback.callback(ret);
992
+ }
969
993
 
970
- String channelUrl = this.channelUrl;
971
- if (channelUrl == null || channelUrl.isEmpty()) {
972
- logger.error("Channel URL is not set");
973
- final Map<String, Object> retError = new HashMap<>();
974
- retError.put("message", "channelUrl missing");
975
- retError.put("error", "missing_config");
976
- callback.callback(retError);
977
- return;
978
- }
979
- JSONObject json;
980
- try {
981
- json = this.createInfoObject();
982
- } catch (JSONException e) {
983
- logger.error("Error unsetChannel JSONException " + e.getMessage());
994
+ public void setChannel(
995
+ final String channel,
996
+ final SharedPreferences.Editor editor,
997
+ final String defaultChannelKey,
998
+ final boolean allowSetDefaultChannel,
999
+ final Callback callback
1000
+ ) {
1001
+ // Check if setting defaultChannel is allowed
1002
+ if (!allowSetDefaultChannel) {
1003
+ logger.error("setChannel is disabled by allowSetDefaultChannel config");
984
1004
  final Map<String, Object> retError = new HashMap<>();
985
- retError.put("message", "Cannot get info: " + e);
986
- retError.put("error", "json_error");
1005
+ retError.put("message", "setChannel is disabled by configuration");
1006
+ retError.put("error", "disabled_by_config");
987
1007
  callback.callback(retError);
988
1008
  return;
989
1009
  }
990
1010
 
991
- Request request = new Request.Builder()
992
- .url(channelUrl)
993
- .delete(RequestBody.create(json.toString(), MediaType.get("application/json")))
994
- .build();
995
-
996
- DownloadService.sharedClient
997
- .newCall(request)
998
- .enqueue(
999
- new okhttp3.Callback() {
1000
- @Override
1001
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
1002
- Map<String, Object> retError = new HashMap<>();
1003
- retError.put("message", "Request failed: " + e.getMessage());
1004
- retError.put("error", "network_error");
1005
- callback.callback(retError);
1006
- }
1007
-
1008
- @Override
1009
- public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
1010
- try (ResponseBody responseBody = response.body()) {
1011
- // Check for 429 rate limit
1012
- if (checkAndHandleRateLimitResponse(response)) {
1013
- Map<String, Object> retError = new HashMap<>();
1014
- retError.put("message", "Rate limit exceeded");
1015
- retError.put("error", "rate_limit_exceeded");
1016
- callback.callback(retError);
1017
- return;
1018
- }
1019
-
1020
- if (!response.isSuccessful()) {
1021
- Map<String, Object> retError = new HashMap<>();
1022
- retError.put("message", "Server error: " + response.code());
1023
- retError.put("error", "response_error");
1024
- callback.callback(retError);
1025
- return;
1026
- }
1027
-
1028
- assert responseBody != null;
1029
- String responseData = responseBody.string();
1030
- JSONObject jsonResponse = new JSONObject(responseData);
1031
-
1032
- // Check for server-side errors first
1033
- if (jsonResponse.has("error")) {
1034
- Map<String, Object> retError = new HashMap<>();
1035
- retError.put("error", jsonResponse.getString("error"));
1036
- if (jsonResponse.has("message")) {
1037
- retError.put("message", jsonResponse.getString("message"));
1038
- } else {
1039
- retError.put("message", "server did not provide a message");
1040
- }
1041
- callback.callback(retError);
1042
- return;
1043
- }
1044
-
1045
- Map<String, Object> ret = new HashMap<>();
1046
-
1047
- Iterator<String> keys = jsonResponse.keys();
1048
- while (keys.hasNext()) {
1049
- String key = keys.next();
1050
- if (jsonResponse.has(key)) {
1051
- ret.put(key, jsonResponse.get(key));
1052
- }
1053
- }
1054
- logger.info("Channel unset");
1055
- callback.callback(ret);
1056
- } catch (JSONException e) {
1057
- Map<String, Object> retError = new HashMap<>();
1058
- retError.put("message", "JSON parse error: " + e.getMessage());
1059
- retError.put("error", "parse_error");
1060
- callback.callback(retError);
1061
- }
1062
- }
1063
- }
1064
- );
1065
- }
1066
-
1067
- public void setChannel(final String channel, final Callback callback) {
1068
1011
  // Check if rate limit was exceeded
1069
1012
  if (rateLimitExceeded) {
1070
1013
  logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.");
@@ -1097,7 +1040,18 @@ public class CapgoUpdater {
1097
1040
  return;
1098
1041
  }
1099
1042
 
1100
- makeJsonRequest(channelUrl, json, callback);
1043
+ makeJsonRequest(channelUrl, json, (res) -> {
1044
+ if (res.containsKey("error")) {
1045
+ callback.callback(res);
1046
+ } else {
1047
+ // Success - persist defaultChannel
1048
+ this.defaultChannel = channel;
1049
+ editor.putString(defaultChannelKey, channel);
1050
+ editor.apply();
1051
+ logger.info("defaultChannel persisted locally: " + channel);
1052
+ callback.callback(res);
1053
+ }
1054
+ });
1101
1055
  }
1102
1056
 
1103
1057
  public void getChannel(final Callback callback) {