@capgo/capacitor-updater 6.34.0 → 6.37.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 +11 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +60 -8
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +82 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +48 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +19 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +111 -79
- package/dist/docs.json +26 -2
- package/dist/esm/definitions.d.ts +12 -2
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +37 -10
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +79 -22
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +100 -31
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +19 -0
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +5 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ 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 v8
|
|
70
|
+
|
|
69
71
|
## Migration to v7.34
|
|
70
72
|
|
|
71
73
|
- **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.
|
|
@@ -76,13 +78,15 @@ Join the [discord](https://discord.gg/VnYRvBfgA6) to get help.
|
|
|
76
78
|
|
|
77
79
|
## Migration to v7
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
The min version of IOS is now 15.5 instead of 15 as Capacitor 8 requirement.
|
|
82
|
+
This is due to bump of ZipArchive to latest, a key dependency of this project is the zlib library. zlib before version 1.2.12 allows memory corruption when deflating (i.e., when compressing) if the input has many distant matches according to [CVE-2018-25032](https://nvd.nist.gov/vuln/detail/cve-2018-25032).
|
|
83
|
+
zlib is a native library so we need to bump the minimum iOS version to 15.5 as ZipArchive did the same in their latest versions.
|
|
81
84
|
|
|
82
85
|
## Compatibility
|
|
83
86
|
|
|
84
87
|
| Plugin version | Capacitor compatibility | Maintained |
|
|
85
88
|
| -------------- | ----------------------- | ----------------- |
|
|
89
|
+
| v8.\*.\* | v8.\*.\* | Beta |
|
|
86
90
|
| v7.\*.\* | v7.\*.\* | ✅ |
|
|
87
91
|
| v6.\*.\* | v6.\*.\* | ✅ |
|
|
88
92
|
| v5.\*.\* | v5.\*.\* | ⚠️ Deprecated |
|
|
@@ -256,7 +260,7 @@ CapacitorUpdater can be configured with these options:
|
|
|
256
260
|
| **`autoDeleteFailed`** | <code>boolean</code> | Configure whether the plugin should use automatically delete failed bundles. Only available for Android and iOS. | <code>true</code> | |
|
|
257
261
|
| **`autoDeletePrevious`** | <code>boolean</code> | Configure whether the plugin should use automatically delete previous bundles after a successful update. Only available for Android and iOS. | <code>true</code> | |
|
|
258
262
|
| **`autoUpdate`** | <code>boolean</code> | Configure whether the plugin should use Auto Update via an update server. Only available for Android and iOS. | <code>true</code> | |
|
|
259
|
-
| **`resetWhenUpdate`** | <code>boolean</code> | Automatically delete previous downloaded bundles when a newer native app bundle is installed to the device. Only available for Android and iOS.
|
|
263
|
+
| **`resetWhenUpdate`** | <code>boolean</code> | Automatically delete previous downloaded bundles when a newer native app bundle is installed to the device. Setting this to false can broke the auto update flow if the user download from the store a native app bundle that is older than the current downloaded bundle. Upload will be prevented by channel setting downgrade_under_native. Only available for Android and iOS. | <code>true</code> | |
|
|
260
264
|
| **`updateUrl`** | <code>string</code> | Configure the URL / endpoint to which update checks are sent. Only available for Android and iOS. | <code>https://plugin.capgo.app/updates</code> | |
|
|
261
265
|
| **`channelUrl`** | <code>string</code> | Configure the URL / endpoint for channel operations. Only available for Android and iOS. | <code>https://plugin.capgo.app/channel_self</code> | |
|
|
262
266
|
| **`statsUrl`** | <code>string</code> | Configure the URL / endpoint to which update statistics are sent. Only available for Android and iOS. Set to "" to disable stats reporting. | <code>https://plugin.capgo.app/stats</code> | |
|
|
@@ -295,7 +299,7 @@ In `capacitor.config.json`:
|
|
|
295
299
|
{
|
|
296
300
|
"plugins": {
|
|
297
301
|
"CapacitorUpdater": {
|
|
298
|
-
"appReadyTimeout": 1000 // (1 second),
|
|
302
|
+
"appReadyTimeout": 1000 // (1 second, minimum 1000),
|
|
299
303
|
"responseTimeout": 10 // (10 second),
|
|
300
304
|
"autoDeleteFailed": false,
|
|
301
305
|
"autoDeletePrevious": false,
|
|
@@ -345,7 +349,7 @@ import { CapacitorConfig } from '@capacitor/cli';
|
|
|
345
349
|
const config: CapacitorConfig = {
|
|
346
350
|
plugins: {
|
|
347
351
|
CapacitorUpdater: {
|
|
348
|
-
appReadyTimeout: 1000 // (1 second),
|
|
352
|
+
appReadyTimeout: 1000 // (1 second, minimum 1000),
|
|
349
353
|
responseTimeout: 10 // (10 second),
|
|
350
354
|
autoDeleteFailed: false,
|
|
351
355
|
autoDeletePrevious: false,
|
|
@@ -1771,6 +1775,8 @@ If you don't use backend, you need to provide the URL and version of the bundle.
|
|
|
1771
1775
|
| **`old`** | <code>string</code> | The previous/current version name (provided for reference). | |
|
|
1772
1776
|
| **`url`** | <code>string</code> | Download URL for the bundle (when a new version is available). | |
|
|
1773
1777
|
| **`manifest`** | <code>ManifestEntry[]</code> | File list for partial updates (when using multi-file downloads). | 6.1 |
|
|
1778
|
+
| **`link`** | <code>string</code> | Optional link associated with this bundle version (e.g., release notes URL, changelog, GitHub release). | 7.35.0 |
|
|
1779
|
+
| **`comment`** | <code>string</code> | Optional comment or description for this bundle version. | 7.35.0 |
|
|
1774
1780
|
|
|
1775
1781
|
|
|
1776
1782
|
##### GetLatestOptions
|
|
@@ -29,25 +29,53 @@ public class BundleInfo {
|
|
|
29
29
|
private final String version;
|
|
30
30
|
private final String checksum;
|
|
31
31
|
private final BundleStatus status;
|
|
32
|
+
private final String link;
|
|
33
|
+
private final String comment;
|
|
32
34
|
|
|
33
35
|
static {
|
|
34
36
|
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
public BundleInfo(final BundleInfo source) {
|
|
38
|
-
this(source.id, source.version, source.status, source.downloaded, source.checksum);
|
|
40
|
+
this(source.id, source.version, source.status, source.downloaded, source.checksum, source.link, source.comment);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
public BundleInfo(final String id, final String version, final BundleStatus status, final Date downloaded, final String checksum) {
|
|
42
|
-
this(id, version, status, sdf.format(downloaded), checksum);
|
|
44
|
+
this(id, version, status, sdf.format(downloaded), checksum, null, null);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public BundleInfo(
|
|
48
|
+
final String id,
|
|
49
|
+
final String version,
|
|
50
|
+
final BundleStatus status,
|
|
51
|
+
final Date downloaded,
|
|
52
|
+
final String checksum,
|
|
53
|
+
final String link,
|
|
54
|
+
final String comment
|
|
55
|
+
) {
|
|
56
|
+
this(id, version, status, sdf.format(downloaded), checksum, link, comment);
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
public BundleInfo(final String id, final String version, final BundleStatus status, final String downloaded, final String checksum) {
|
|
60
|
+
this(id, version, status, downloaded, checksum, null, null);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public BundleInfo(
|
|
64
|
+
final String id,
|
|
65
|
+
final String version,
|
|
66
|
+
final BundleStatus status,
|
|
67
|
+
final String downloaded,
|
|
68
|
+
final String checksum,
|
|
69
|
+
final String link,
|
|
70
|
+
final String comment
|
|
71
|
+
) {
|
|
46
72
|
this.downloaded = downloaded != null ? downloaded.trim() : "";
|
|
47
73
|
this.id = id != null ? id : "";
|
|
48
74
|
this.version = version;
|
|
49
75
|
this.checksum = checksum != null ? checksum : "";
|
|
50
76
|
this.status = status != null ? status : BundleStatus.ERROR;
|
|
77
|
+
this.link = link;
|
|
78
|
+
this.comment = comment;
|
|
51
79
|
}
|
|
52
80
|
|
|
53
81
|
public Boolean isBuiltin() {
|
|
@@ -75,7 +103,7 @@ public class BundleInfo {
|
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
public BundleInfo setDownloaded(Date downloaded) {
|
|
78
|
-
return new BundleInfo(this.id, this.version, this.status, downloaded, this.checksum);
|
|
106
|
+
return new BundleInfo(this.id, this.version, this.status, downloaded, this.checksum, this.link, this.comment);
|
|
79
107
|
}
|
|
80
108
|
|
|
81
109
|
public String getChecksum() {
|
|
@@ -83,7 +111,7 @@ public class BundleInfo {
|
|
|
83
111
|
}
|
|
84
112
|
|
|
85
113
|
public BundleInfo setChecksum(String checksum) {
|
|
86
|
-
return new BundleInfo(this.id, this.version, this.status, this.downloaded, checksum);
|
|
114
|
+
return new BundleInfo(this.id, this.version, this.status, this.downloaded, checksum, this.link, this.comment);
|
|
87
115
|
}
|
|
88
116
|
|
|
89
117
|
public String getId() {
|
|
@@ -91,7 +119,7 @@ public class BundleInfo {
|
|
|
91
119
|
}
|
|
92
120
|
|
|
93
121
|
public BundleInfo setId(String id) {
|
|
94
|
-
return new BundleInfo(id, this.version, this.status, this.downloaded, this.checksum);
|
|
122
|
+
return new BundleInfo(id, this.version, this.status, this.downloaded, this.checksum, this.link, this.comment);
|
|
95
123
|
}
|
|
96
124
|
|
|
97
125
|
public String getVersionName() {
|
|
@@ -99,7 +127,7 @@ public class BundleInfo {
|
|
|
99
127
|
}
|
|
100
128
|
|
|
101
129
|
public BundleInfo setVersionName(String version) {
|
|
102
|
-
return new BundleInfo(this.id, version, this.status, this.downloaded, this.checksum);
|
|
130
|
+
return new BundleInfo(this.id, version, this.status, this.downloaded, this.checksum, this.link, this.comment);
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
public BundleStatus getStatus() {
|
|
@@ -110,7 +138,23 @@ public class BundleInfo {
|
|
|
110
138
|
}
|
|
111
139
|
|
|
112
140
|
public BundleInfo setStatus(BundleStatus status) {
|
|
113
|
-
return new BundleInfo(this.id, this.version, status, this.downloaded, this.checksum);
|
|
141
|
+
return new BundleInfo(this.id, this.version, status, this.downloaded, this.checksum, this.link, this.comment);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public String getLink() {
|
|
145
|
+
return this.link;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public BundleInfo setLink(String link) {
|
|
149
|
+
return new BundleInfo(this.id, this.version, this.status, this.downloaded, this.checksum, link, this.comment);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public String getComment() {
|
|
153
|
+
return this.comment;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public BundleInfo setComment(String comment) {
|
|
157
|
+
return new BundleInfo(this.id, this.version, this.status, this.downloaded, this.checksum, this.link, comment);
|
|
114
158
|
}
|
|
115
159
|
|
|
116
160
|
public static BundleInfo fromJSON(final String jsonString) throws JSONException {
|
|
@@ -120,7 +164,9 @@ public class BundleInfo {
|
|
|
120
164
|
json.has("version") ? json.getString("version") : BundleInfo.VERSION_UNKNOWN,
|
|
121
165
|
json.has("status") ? BundleStatus.fromString(json.getString("status")) : BundleStatus.PENDING,
|
|
122
166
|
json.has("downloaded") ? json.getString("downloaded") : "",
|
|
123
|
-
json.has("checksum") ? json.getString("checksum") : ""
|
|
167
|
+
json.has("checksum") ? json.getString("checksum") : "",
|
|
168
|
+
json.has("link") ? json.getString("link") : null,
|
|
169
|
+
json.has("comment") ? json.getString("comment") : null
|
|
124
170
|
);
|
|
125
171
|
}
|
|
126
172
|
|
|
@@ -131,6 +177,12 @@ public class BundleInfo {
|
|
|
131
177
|
result.put("downloaded", this.getDownloaded());
|
|
132
178
|
result.put("checksum", this.getChecksum());
|
|
133
179
|
result.put("status", this.getStatus().toString());
|
|
180
|
+
if (this.link != null && !this.link.isEmpty()) {
|
|
181
|
+
result.put("link", this.link);
|
|
182
|
+
}
|
|
183
|
+
if (this.comment != null && !this.comment.isEmpty()) {
|
|
184
|
+
result.put("comment", this.comment);
|
|
185
|
+
}
|
|
134
186
|
return result;
|
|
135
187
|
}
|
|
136
188
|
|
|
@@ -72,7 +72,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
72
72
|
private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
|
|
73
73
|
private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
|
|
74
74
|
|
|
75
|
-
private final String pluginVersion = "6.
|
|
75
|
+
private final String pluginVersion = "6.37.0";
|
|
76
76
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
77
77
|
|
|
78
78
|
private SharedPreferences.Editor editor;
|
|
@@ -111,6 +111,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
111
111
|
// private static final CountDownLatch semaphoreReady = new CountDownLatch(1);
|
|
112
112
|
private static final Phaser semaphoreReady = new Phaser(1);
|
|
113
113
|
|
|
114
|
+
// Lock to ensure cleanup completes before downloads start
|
|
115
|
+
private final Object cleanupLock = new Object();
|
|
116
|
+
private volatile boolean cleanupComplete = false;
|
|
117
|
+
private volatile Thread cleanupThread = null;
|
|
118
|
+
|
|
114
119
|
private int lastNotifiedStatPercent = 0;
|
|
115
120
|
|
|
116
121
|
private DelayUpdateUtils delayUpdateUtils;
|
|
@@ -304,10 +309,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
304
309
|
this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
|
|
305
310
|
this.persistModifyUrl = this.getConfig().getBoolean("persistModifyUrl", false);
|
|
306
311
|
this.allowSetDefaultChannel = this.getConfig().getBoolean("allowSetDefaultChannel", true);
|
|
307
|
-
this.implementation.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
312
|
+
this.implementation.setPublicKey(this.getConfig().getString("publicKey", ""));
|
|
313
|
+
// Log public key prefix if encryption is enabled
|
|
314
|
+
String keyId = this.implementation.getKeyId();
|
|
315
|
+
if (keyId != null && !keyId.isEmpty()) {
|
|
316
|
+
logger.info("Public key prefix: " + keyId);
|
|
311
317
|
}
|
|
312
318
|
this.implementation.statsUrl = this.getConfig().getString("statsUrl", statsUrlDefault);
|
|
313
319
|
this.implementation.channelUrl = this.getConfig().getString("channelUrl", channelUrlDefault);
|
|
@@ -381,7 +387,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
381
387
|
}
|
|
382
388
|
}
|
|
383
389
|
this.autoUpdate = this.getConfig().getBoolean("autoUpdate", true);
|
|
384
|
-
this.appReadyTimeout = this.getConfig().getInt("appReadyTimeout", 10000);
|
|
390
|
+
this.appReadyTimeout = Math.max(1000, this.getConfig().getInt("appReadyTimeout", 10000)); // Minimum 1 second
|
|
385
391
|
this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
|
|
386
392
|
this.syncKeepUrlPathFlag(this.keepUrlPathAfterReload);
|
|
387
393
|
this.allowManualBundleError = this.getConfig().getBoolean("allowManualBundleError", false);
|
|
@@ -700,37 +706,80 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
700
706
|
}
|
|
701
707
|
|
|
702
708
|
private void cleanupObsoleteVersions() {
|
|
703
|
-
startNewThread(() -> {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
709
|
+
cleanupThread = startNewThread(() -> {
|
|
710
|
+
synchronized (cleanupLock) {
|
|
711
|
+
try {
|
|
712
|
+
final String previous = this.prefs.getString("LatestNativeBuildVersion", "");
|
|
713
|
+
if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
|
|
714
|
+
logger.info("New native build version detected: " + this.currentBuildVersion);
|
|
715
|
+
this.implementation.reset(true);
|
|
716
|
+
final List<BundleInfo> installed = this.implementation.list(false);
|
|
717
|
+
for (final BundleInfo bundle : installed) {
|
|
718
|
+
// Check if thread was interrupted (cancelled)
|
|
719
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
720
|
+
logger.warn("Cleanup was cancelled, stopping");
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
logger.info("Deleting obsolete bundle: " + bundle.getId());
|
|
725
|
+
this.implementation.delete(bundle.getId());
|
|
726
|
+
} catch (final Exception e) {
|
|
727
|
+
logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
|
|
728
|
+
}
|
|
716
729
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
730
|
+
final List<BundleInfo> storedBundles = this.implementation.list(true);
|
|
731
|
+
final Set<String> allowedIds = new HashSet<>();
|
|
732
|
+
for (final BundleInfo info : storedBundles) {
|
|
733
|
+
if (info != null && info.getId() != null && !info.getId().isEmpty()) {
|
|
734
|
+
allowedIds.add(info.getId());
|
|
735
|
+
}
|
|
723
736
|
}
|
|
737
|
+
this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
|
|
738
|
+
|
|
739
|
+
// Check again before the expensive delta cache cleanup
|
|
740
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
741
|
+
logger.warn("Cleanup was cancelled before delta cache cleanup");
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
this.implementation.cleanupDeltaCache(Thread.currentThread());
|
|
724
745
|
}
|
|
725
|
-
this.
|
|
726
|
-
this.
|
|
746
|
+
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
747
|
+
this.editor.apply();
|
|
748
|
+
} catch (Exception e) {
|
|
749
|
+
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
750
|
+
} finally {
|
|
751
|
+
cleanupComplete = true;
|
|
752
|
+
logger.info("Cleanup complete");
|
|
727
753
|
}
|
|
728
|
-
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
729
|
-
this.editor.apply();
|
|
730
|
-
} catch (Exception e) {
|
|
731
|
-
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
732
754
|
}
|
|
733
755
|
});
|
|
756
|
+
|
|
757
|
+
// Start a timeout watchdog thread to cancel cleanup if it takes too long
|
|
758
|
+
final long timeout = this.appReadyTimeout / 2;
|
|
759
|
+
startNewThread(() -> {
|
|
760
|
+
try {
|
|
761
|
+
Thread.sleep(timeout);
|
|
762
|
+
if (cleanupThread != null && cleanupThread.isAlive() && !cleanupComplete) {
|
|
763
|
+
logger.warn("Cleanup timeout exceeded (" + timeout + "ms), interrupting cleanup thread");
|
|
764
|
+
cleanupThread.interrupt();
|
|
765
|
+
}
|
|
766
|
+
} catch (InterruptedException e) {
|
|
767
|
+
// Watchdog thread was interrupted, that's fine
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private void waitForCleanupIfNeeded() {
|
|
773
|
+
if (cleanupComplete) {
|
|
774
|
+
return; // Already done, no need to wait
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
logger.info("Waiting for cleanup to complete before starting download...");
|
|
778
|
+
|
|
779
|
+
// Wait for cleanup to complete - blocks until lock is released
|
|
780
|
+
synchronized (cleanupLock) {
|
|
781
|
+
logger.info("Cleanup finished, proceeding with download");
|
|
782
|
+
}
|
|
734
783
|
}
|
|
735
784
|
|
|
736
785
|
public void notifyDownload(final String id, final int percent) {
|
|
@@ -1678,6 +1727,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1678
1727
|
? "Update will occur now."
|
|
1679
1728
|
: "Update will occur next time app moves to background.";
|
|
1680
1729
|
return startNewThread(() -> {
|
|
1730
|
+
// Wait for cleanup to complete before starting download
|
|
1731
|
+
waitForCleanupIfNeeded();
|
|
1681
1732
|
logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
|
|
1682
1733
|
try {
|
|
1683
1734
|
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
@@ -83,6 +83,9 @@ public class CapgoUpdater {
|
|
|
83
83
|
public String deviceID = "";
|
|
84
84
|
public int timeout = 20000;
|
|
85
85
|
|
|
86
|
+
// Cached key ID calculated once from publicKey
|
|
87
|
+
private String cachedKeyId = "";
|
|
88
|
+
|
|
86
89
|
// Flag to track if we received a 429 response - stops requests until app restart
|
|
87
90
|
private static volatile boolean rateLimitExceeded = false;
|
|
88
91
|
|
|
@@ -147,6 +150,19 @@ public class CapgoUpdater {
|
|
|
147
150
|
return sb.toString();
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
public void setPublicKey(String publicKey) {
|
|
154
|
+
this.publicKey = publicKey;
|
|
155
|
+
if (!publicKey.isEmpty()) {
|
|
156
|
+
this.cachedKeyId = CryptoCipher.calcKeyId(publicKey);
|
|
157
|
+
} else {
|
|
158
|
+
this.cachedKeyId = "";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public String getKeyId() {
|
|
163
|
+
return this.cachedKeyId;
|
|
164
|
+
}
|
|
165
|
+
|
|
150
166
|
private File unzip(final String id, final File zipFile, final String dest) throws IOException {
|
|
151
167
|
final File targetDirectory = new File(this.documentsDir, dest);
|
|
152
168
|
try (
|
|
@@ -485,11 +501,20 @@ public class CapgoUpdater {
|
|
|
485
501
|
}
|
|
486
502
|
|
|
487
503
|
private void deleteDirectory(final File file) throws IOException {
|
|
504
|
+
deleteDirectory(file, null);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private void deleteDirectory(final File file, final Thread threadToCheck) throws IOException {
|
|
508
|
+
// Check if thread was interrupted (cancelled)
|
|
509
|
+
if (threadToCheck != null && threadToCheck.isInterrupted()) {
|
|
510
|
+
throw new IOException("Operation cancelled");
|
|
511
|
+
}
|
|
512
|
+
|
|
488
513
|
if (file.isDirectory()) {
|
|
489
514
|
final File[] entries = file.listFiles();
|
|
490
515
|
if (entries != null) {
|
|
491
516
|
for (final File entry : entries) {
|
|
492
|
-
this.deleteDirectory(entry);
|
|
517
|
+
this.deleteDirectory(entry, threadToCheck);
|
|
493
518
|
}
|
|
494
519
|
}
|
|
495
520
|
}
|
|
@@ -499,6 +524,10 @@ public class CapgoUpdater {
|
|
|
499
524
|
}
|
|
500
525
|
|
|
501
526
|
public void cleanupDeltaCache() {
|
|
527
|
+
cleanupDeltaCache(null);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
public void cleanupDeltaCache(final Thread threadToCheck) {
|
|
502
531
|
if (this.activity == null) {
|
|
503
532
|
logger.warn("Activity is null, skipping delta cache cleanup");
|
|
504
533
|
return;
|
|
@@ -508,7 +537,7 @@ public class CapgoUpdater {
|
|
|
508
537
|
return;
|
|
509
538
|
}
|
|
510
539
|
try {
|
|
511
|
-
this.deleteDirectory(cacheFolder);
|
|
540
|
+
this.deleteDirectory(cacheFolder, threadToCheck);
|
|
512
541
|
logger.info("Cleaned up delta cache folder");
|
|
513
542
|
} catch (IOException e) {
|
|
514
543
|
logger.error("Failed to cleanup delta cache: " + e.getMessage());
|
|
@@ -516,6 +545,10 @@ public class CapgoUpdater {
|
|
|
516
545
|
}
|
|
517
546
|
|
|
518
547
|
public void cleanupDownloadDirectories(final Set<String> allowedIds) {
|
|
548
|
+
cleanupDownloadDirectories(allowedIds, null);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
public void cleanupDownloadDirectories(final Set<String> allowedIds, final Thread threadToCheck) {
|
|
519
552
|
if (this.documentsDir == null) {
|
|
520
553
|
logger.warn("Documents directory is null, skipping download cleanup");
|
|
521
554
|
return;
|
|
@@ -529,6 +562,12 @@ public class CapgoUpdater {
|
|
|
529
562
|
final File[] entries = bundleRoot.listFiles();
|
|
530
563
|
if (entries != null) {
|
|
531
564
|
for (final File entry : entries) {
|
|
565
|
+
// Check if thread was interrupted (cancelled)
|
|
566
|
+
if (threadToCheck != null && threadToCheck.isInterrupted()) {
|
|
567
|
+
logger.warn("cleanupDownloadDirectories was cancelled");
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
532
571
|
if (!entry.isDirectory()) {
|
|
533
572
|
continue;
|
|
534
573
|
}
|
|
@@ -540,7 +579,7 @@ public class CapgoUpdater {
|
|
|
540
579
|
}
|
|
541
580
|
|
|
542
581
|
try {
|
|
543
|
-
this.deleteDirectory(entry);
|
|
582
|
+
this.deleteDirectory(entry, threadToCheck);
|
|
544
583
|
this.removeBundleInfo(id);
|
|
545
584
|
logger.info("Deleted orphan bundle directory: " + id);
|
|
546
585
|
} catch (IOException e) {
|
|
@@ -811,6 +850,12 @@ public class CapgoUpdater {
|
|
|
811
850
|
json.put("is_emulator", this.isEmulator());
|
|
812
851
|
json.put("is_prod", this.isProd());
|
|
813
852
|
json.put("defaultChannel", this.defaultChannel);
|
|
853
|
+
|
|
854
|
+
// Add encryption key ID if encryption is enabled (use cached value)
|
|
855
|
+
if (!this.cachedKeyId.isEmpty()) {
|
|
856
|
+
json.put("key_id", this.cachedKeyId);
|
|
857
|
+
}
|
|
858
|
+
|
|
814
859
|
return json;
|
|
815
860
|
}
|
|
816
861
|
|
|
@@ -359,4 +359,23 @@ public class CryptoCipher {
|
|
|
359
359
|
|
|
360
360
|
throw new IllegalArgumentException("size too large, only up to 64KiB length encoding supported: " + size);
|
|
361
361
|
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get first 4 characters of the public key for identification.
|
|
365
|
+
* Returns 4-character string or empty string if key is invalid/empty.
|
|
366
|
+
*/
|
|
367
|
+
public static String calcKeyId(String publicKey) {
|
|
368
|
+
if (publicKey == null || publicKey.isEmpty()) {
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Remove PEM headers and whitespace to get the raw key data
|
|
373
|
+
String cleanedKey = publicKey
|
|
374
|
+
.replaceAll("\\s+", "")
|
|
375
|
+
.replace("-----BEGINRSAPUBLICKEY-----", "")
|
|
376
|
+
.replace("-----ENDRSAPUBLICKEY-----", "");
|
|
377
|
+
|
|
378
|
+
// Return first 4 characters of the base64-encoded key
|
|
379
|
+
return cleanedKey.length() >= 4 ? cleanedKey.substring(0, 4) : cleanedKey;
|
|
380
|
+
}
|
|
362
381
|
}
|