@capgo/capacitor-updater 5.34.0 → 5.38.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/CapgoCapacitorUpdater.podspec +1 -1
- package/Package.swift +1 -1
- 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 +84 -28
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +88 -4
- 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 +81 -23
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +137 -25
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +19 -0
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +5 -0
- package/package.json +2 -1
|
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.author = package['author']
|
|
12
12
|
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
13
|
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
-
s.ios.deployment_target = '
|
|
14
|
+
s.ios.deployment_target = '13.0'
|
|
15
15
|
s.dependency 'Capacitor'
|
|
16
16
|
s.dependency 'SSZipArchive', '2.4.3'
|
|
17
17
|
s.dependency 'Alamofire', '5.10.2'
|
package/Package.swift
CHANGED
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> | |
|
|
@@ -294,7 +298,7 @@ In `capacitor.config.json`:
|
|
|
294
298
|
{
|
|
295
299
|
"plugins": {
|
|
296
300
|
"CapacitorUpdater": {
|
|
297
|
-
"appReadyTimeout": 1000 // (1 second),
|
|
301
|
+
"appReadyTimeout": 1000 // (1 second, minimum 1000),
|
|
298
302
|
"responseTimeout": 10 // (10 second),
|
|
299
303
|
"autoDeleteFailed": false,
|
|
300
304
|
"autoDeletePrevious": false,
|
|
@@ -343,7 +347,7 @@ import { CapacitorConfig } from '@capacitor/cli';
|
|
|
343
347
|
const config: CapacitorConfig = {
|
|
344
348
|
plugins: {
|
|
345
349
|
CapacitorUpdater: {
|
|
346
|
-
appReadyTimeout: 1000 // (1 second),
|
|
350
|
+
appReadyTimeout: 1000 // (1 second, minimum 1000),
|
|
347
351
|
responseTimeout: 10 // (10 second),
|
|
348
352
|
autoDeleteFailed: false,
|
|
349
353
|
autoDeletePrevious: false,
|
|
@@ -1768,6 +1772,8 @@ If you don't use backend, you need to provide the URL and version of the bundle.
|
|
|
1768
1772
|
| **`old`** | <code>string</code> | The previous/current version name (provided for reference). | |
|
|
1769
1773
|
| **`url`** | <code>string</code> | Download URL for the bundle (when a new version is available). | |
|
|
1770
1774
|
| **`manifest`** | <code>ManifestEntry[]</code> | File list for partial updates (when using multi-file downloads). | 6.1 |
|
|
1775
|
+
| **`link`** | <code>string</code> | Optional link associated with this bundle version (e.g., release notes URL, changelog, GitHub release). | 7.35.0 |
|
|
1776
|
+
| **`comment`** | <code>string</code> | Optional comment or description for this bundle version. | 7.35.0 |
|
|
1771
1777
|
|
|
1772
1778
|
|
|
1773
1779
|
##### 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 = "5.
|
|
75
|
+
private final String pluginVersion = "5.38.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,7 +309,12 @@ 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.
|
|
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);
|
|
317
|
+
}
|
|
308
318
|
this.implementation.statsUrl = this.getConfig().getString("statsUrl", statsUrlDefault);
|
|
309
319
|
this.implementation.channelUrl = this.getConfig().getString("channelUrl", channelUrlDefault);
|
|
310
320
|
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
@@ -377,7 +387,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
377
387
|
}
|
|
378
388
|
}
|
|
379
389
|
this.autoUpdate = this.getConfig().getBoolean("autoUpdate", true);
|
|
380
|
-
this.appReadyTimeout = this.getConfig().getInt("appReadyTimeout", 10000);
|
|
390
|
+
this.appReadyTimeout = Math.max(1000, this.getConfig().getInt("appReadyTimeout", 10000)); // Minimum 1 second
|
|
381
391
|
this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
|
|
382
392
|
this.syncKeepUrlPathFlag(this.keepUrlPathAfterReload);
|
|
383
393
|
this.allowManualBundleError = this.getConfig().getBoolean("allowManualBundleError", false);
|
|
@@ -696,37 +706,81 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
696
706
|
}
|
|
697
707
|
|
|
698
708
|
private void cleanupObsoleteVersions() {
|
|
699
|
-
startNewThread(() -> {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
+
}
|
|
712
729
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
+
}
|
|
719
736
|
}
|
|
737
|
+
this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
|
|
738
|
+
this.implementation.cleanupOrphanedTempFolders(Thread.currentThread());
|
|
739
|
+
|
|
740
|
+
// Check again before the expensive delta cache cleanup
|
|
741
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
742
|
+
logger.warn("Cleanup was cancelled before delta cache cleanup");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
this.implementation.cleanupDeltaCache(Thread.currentThread());
|
|
720
746
|
}
|
|
721
|
-
this.
|
|
722
|
-
this.
|
|
747
|
+
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
748
|
+
this.editor.apply();
|
|
749
|
+
} catch (Exception e) {
|
|
750
|
+
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
751
|
+
} finally {
|
|
752
|
+
cleanupComplete = true;
|
|
753
|
+
logger.info("Cleanup complete");
|
|
723
754
|
}
|
|
724
|
-
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
725
|
-
this.editor.apply();
|
|
726
|
-
} catch (Exception e) {
|
|
727
|
-
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
728
755
|
}
|
|
729
756
|
});
|
|
757
|
+
|
|
758
|
+
// Start a timeout watchdog thread to cancel cleanup if it takes too long
|
|
759
|
+
final long timeout = this.appReadyTimeout / 2;
|
|
760
|
+
startNewThread(() -> {
|
|
761
|
+
try {
|
|
762
|
+
Thread.sleep(timeout);
|
|
763
|
+
if (cleanupThread != null && cleanupThread.isAlive() && !cleanupComplete) {
|
|
764
|
+
logger.warn("Cleanup timeout exceeded (" + timeout + "ms), interrupting cleanup thread");
|
|
765
|
+
cleanupThread.interrupt();
|
|
766
|
+
}
|
|
767
|
+
} catch (InterruptedException e) {
|
|
768
|
+
// Watchdog thread was interrupted, that's fine
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private void waitForCleanupIfNeeded() {
|
|
774
|
+
if (cleanupComplete) {
|
|
775
|
+
return; // Already done, no need to wait
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
logger.info("Waiting for cleanup to complete before starting download...");
|
|
779
|
+
|
|
780
|
+
// Wait for cleanup to complete - blocks until lock is released
|
|
781
|
+
synchronized (cleanupLock) {
|
|
782
|
+
logger.info("Cleanup finished, proceeding with download");
|
|
783
|
+
}
|
|
730
784
|
}
|
|
731
785
|
|
|
732
786
|
public void notifyDownload(final String id, final int percent) {
|
|
@@ -1674,6 +1728,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1674
1728
|
? "Update will occur now."
|
|
1675
1729
|
: "Update will occur next time app moves to background.";
|
|
1676
1730
|
return startNewThread(() -> {
|
|
1731
|
+
// Wait for cleanup to complete before starting download
|
|
1732
|
+
waitForCleanupIfNeeded();
|
|
1677
1733
|
logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
|
|
1678
1734
|
try {
|
|
1679
1735
|
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
@@ -58,6 +58,7 @@ public class CapgoUpdater {
|
|
|
58
58
|
private static final String FALLBACK_VERSION = "pastVersion";
|
|
59
59
|
private static final String NEXT_VERSION = "nextVersion";
|
|
60
60
|
private static final String bundleDirectory = "versions";
|
|
61
|
+
private static final String TEMP_UNZIP_PREFIX = "capgo_unzip_";
|
|
61
62
|
|
|
62
63
|
public static final String TAG = "Capacitor-updater";
|
|
63
64
|
public SharedPreferences.Editor editor;
|
|
@@ -81,6 +82,9 @@ public class CapgoUpdater {
|
|
|
81
82
|
public String deviceID = "";
|
|
82
83
|
public int timeout = 20000;
|
|
83
84
|
|
|
85
|
+
// Cached key ID calculated once from publicKey
|
|
86
|
+
private String cachedKeyId = "";
|
|
87
|
+
|
|
84
88
|
// Flag to track if we received a 429 response - stops requests until app restart
|
|
85
89
|
private static volatile boolean rateLimitExceeded = false;
|
|
86
90
|
|
|
@@ -145,6 +149,19 @@ public class CapgoUpdater {
|
|
|
145
149
|
return sb.toString();
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
public void setPublicKey(String publicKey) {
|
|
153
|
+
this.publicKey = publicKey;
|
|
154
|
+
if (!publicKey.isEmpty()) {
|
|
155
|
+
this.cachedKeyId = CryptoCipher.calcKeyId(publicKey);
|
|
156
|
+
} else {
|
|
157
|
+
this.cachedKeyId = "";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public String getKeyId() {
|
|
162
|
+
return this.cachedKeyId;
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
private File unzip(final String id, final File zipFile, final String dest) throws IOException {
|
|
149
166
|
final File targetDirectory = new File(this.documentsDir, dest);
|
|
150
167
|
try (
|
|
@@ -431,7 +448,7 @@ public class CapgoUpdater {
|
|
|
431
448
|
|
|
432
449
|
try {
|
|
433
450
|
if (!isManifest) {
|
|
434
|
-
extractedDir = this.unzip(id, downloaded, this.randomString());
|
|
451
|
+
extractedDir = this.unzip(id, downloaded, TEMP_UNZIP_PREFIX + this.randomString());
|
|
435
452
|
this.notifyDownload(id, 91);
|
|
436
453
|
final String idName = bundleDirectory + "/" + id;
|
|
437
454
|
this.flattenAssets(extractedDir, idName);
|
|
@@ -480,11 +497,20 @@ public class CapgoUpdater {
|
|
|
480
497
|
}
|
|
481
498
|
|
|
482
499
|
private void deleteDirectory(final File file) throws IOException {
|
|
500
|
+
deleteDirectory(file, null);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private void deleteDirectory(final File file, final Thread threadToCheck) throws IOException {
|
|
504
|
+
// Check if thread was interrupted (cancelled)
|
|
505
|
+
if (threadToCheck != null && threadToCheck.isInterrupted()) {
|
|
506
|
+
throw new IOException("Operation cancelled");
|
|
507
|
+
}
|
|
508
|
+
|
|
483
509
|
if (file.isDirectory()) {
|
|
484
510
|
final File[] entries = file.listFiles();
|
|
485
511
|
if (entries != null) {
|
|
486
512
|
for (final File entry : entries) {
|
|
487
|
-
this.deleteDirectory(entry);
|
|
513
|
+
this.deleteDirectory(entry, threadToCheck);
|
|
488
514
|
}
|
|
489
515
|
}
|
|
490
516
|
}
|
|
@@ -494,6 +520,10 @@ public class CapgoUpdater {
|
|
|
494
520
|
}
|
|
495
521
|
|
|
496
522
|
public void cleanupDeltaCache() {
|
|
523
|
+
cleanupDeltaCache(null);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
public void cleanupDeltaCache(final Thread threadToCheck) {
|
|
497
527
|
if (this.activity == null) {
|
|
498
528
|
logger.warn("Activity is null, skipping delta cache cleanup");
|
|
499
529
|
return;
|
|
@@ -503,7 +533,7 @@ public class CapgoUpdater {
|
|
|
503
533
|
return;
|
|
504
534
|
}
|
|
505
535
|
try {
|
|
506
|
-
this.deleteDirectory(cacheFolder);
|
|
536
|
+
this.deleteDirectory(cacheFolder, threadToCheck);
|
|
507
537
|
logger.info("Cleaned up delta cache folder");
|
|
508
538
|
} catch (IOException e) {
|
|
509
539
|
logger.error("Failed to cleanup delta cache: " + e.getMessage());
|
|
@@ -511,6 +541,10 @@ public class CapgoUpdater {
|
|
|
511
541
|
}
|
|
512
542
|
|
|
513
543
|
public void cleanupDownloadDirectories(final Set<String> allowedIds) {
|
|
544
|
+
cleanupDownloadDirectories(allowedIds, null);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
public void cleanupDownloadDirectories(final Set<String> allowedIds, final Thread threadToCheck) {
|
|
514
548
|
if (this.documentsDir == null) {
|
|
515
549
|
logger.warn("Documents directory is null, skipping download cleanup");
|
|
516
550
|
return;
|
|
@@ -524,6 +558,12 @@ public class CapgoUpdater {
|
|
|
524
558
|
final File[] entries = bundleRoot.listFiles();
|
|
525
559
|
if (entries != null) {
|
|
526
560
|
for (final File entry : entries) {
|
|
561
|
+
// Check if thread was interrupted (cancelled)
|
|
562
|
+
if (threadToCheck != null && threadToCheck.isInterrupted()) {
|
|
563
|
+
logger.warn("cleanupDownloadDirectories was cancelled");
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
527
567
|
if (!entry.isDirectory()) {
|
|
528
568
|
continue;
|
|
529
569
|
}
|
|
@@ -535,7 +575,7 @@ public class CapgoUpdater {
|
|
|
535
575
|
}
|
|
536
576
|
|
|
537
577
|
try {
|
|
538
|
-
this.deleteDirectory(entry);
|
|
578
|
+
this.deleteDirectory(entry, threadToCheck);
|
|
539
579
|
this.removeBundleInfo(id);
|
|
540
580
|
logger.info("Deleted orphan bundle directory: " + id);
|
|
541
581
|
} catch (IOException e) {
|
|
@@ -545,6 +585,44 @@ public class CapgoUpdater {
|
|
|
545
585
|
}
|
|
546
586
|
}
|
|
547
587
|
|
|
588
|
+
public void cleanupOrphanedTempFolders(final Thread threadToCheck) {
|
|
589
|
+
if (this.documentsDir == null) {
|
|
590
|
+
logger.warn("Documents directory is null, skipping temp folder cleanup");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
final File[] entries = this.documentsDir.listFiles();
|
|
595
|
+
if (entries == null) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
for (final File entry : entries) {
|
|
600
|
+
// Check if thread was interrupted (cancelled)
|
|
601
|
+
if (threadToCheck != null && threadToCheck.isInterrupted()) {
|
|
602
|
+
logger.warn("cleanupOrphanedTempFolders was cancelled");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!entry.isDirectory()) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
final String folderName = entry.getName();
|
|
611
|
+
|
|
612
|
+
// Only delete folders with the temp unzip prefix
|
|
613
|
+
if (!folderName.startsWith(TEMP_UNZIP_PREFIX)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
this.deleteDirectory(entry, threadToCheck);
|
|
619
|
+
logger.info("Deleted orphaned temp unzip folder: " + folderName);
|
|
620
|
+
} catch (IOException e) {
|
|
621
|
+
logger.error("Failed to delete orphaned temp folder: " + folderName + " " + e.getMessage());
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
548
626
|
private void safeDelete(final File target) {
|
|
549
627
|
if (target == null || !target.exists()) {
|
|
550
628
|
return;
|
|
@@ -806,6 +884,12 @@ public class CapgoUpdater {
|
|
|
806
884
|
json.put("is_emulator", this.isEmulator());
|
|
807
885
|
json.put("is_prod", this.isProd());
|
|
808
886
|
json.put("defaultChannel", this.defaultChannel);
|
|
887
|
+
|
|
888
|
+
// Add encryption key ID if encryption is enabled (use cached value)
|
|
889
|
+
if (!this.cachedKeyId.isEmpty()) {
|
|
890
|
+
json.put("key_id", this.cachedKeyId);
|
|
891
|
+
}
|
|
892
|
+
|
|
809
893
|
return json;
|
|
810
894
|
}
|
|
811
895
|
|
|
@@ -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
|
}
|