@capgo/capacitor-updater 8.51.14 → 8.51.16
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 +0 -2
- package/Package.swift +4 -5
- package/README.md +1 -1
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +55 -22
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +59 -29
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +11 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +57 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- package/dist/docs.json +1 -1
- package/dist/esm/definitions.d.ts +4 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +4 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +114 -30
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoRawRsa.swift +36 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +56 -20
- package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +73 -251
- package/package.json +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -39
|
@@ -83,7 +83,11 @@ public class DownloadService extends Worker {
|
|
|
83
83
|
private static final String UPDATE_FILE = "update.dat";
|
|
84
84
|
|
|
85
85
|
// Shared OkHttpClient to prevent resource leaks
|
|
86
|
-
protected static OkHttpClient sharedClient;
|
|
86
|
+
protected static volatile OkHttpClient sharedClient;
|
|
87
|
+
private static final Object HTTP_CLIENT_LOCK = new Object();
|
|
88
|
+
// Match CapgoUpdater.timeout / responseTimeout default (20s). OkHttp's 10s
|
|
89
|
+
// defaults were unused by the plugin config and aborted slow manifest GETs.
|
|
90
|
+
private static volatile int httpTimeoutMs = 20_000;
|
|
87
91
|
private static String currentAppId = "unknown";
|
|
88
92
|
private static String currentPluginVersion = "unknown";
|
|
89
93
|
private static String currentVersionOs = "unknown";
|
|
@@ -96,6 +100,9 @@ public class DownloadService extends Worker {
|
|
|
96
100
|
sharedClient = new OkHttpClient.Builder()
|
|
97
101
|
.dispatcher(dispatcher)
|
|
98
102
|
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
|
|
103
|
+
.connectTimeout(httpTimeoutMs, TimeUnit.MILLISECONDS)
|
|
104
|
+
.readTimeout(httpTimeoutMs, TimeUnit.MILLISECONDS)
|
|
105
|
+
.writeTimeout(httpTimeoutMs, TimeUnit.MILLISECONDS)
|
|
99
106
|
.addInterceptor((chain) -> {
|
|
100
107
|
Request originalRequest = chain.request();
|
|
101
108
|
String userAgent = buildUserAgent(currentAppId, currentPluginVersion, currentVersionOs);
|
|
@@ -105,6 +112,32 @@ public class DownloadService extends Worker {
|
|
|
105
112
|
.build();
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
static int httpTimeoutMs() {
|
|
116
|
+
return httpTimeoutMs;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static void applyHttpTimeouts(int timeoutMs) {
|
|
120
|
+
// OkHttp treats 0 as infinite; keep plugin responseTimeout floor (20s default).
|
|
121
|
+
int ms = timeoutMs > 0 ? timeoutMs : 20_000;
|
|
122
|
+
synchronized (HTTP_CLIENT_LOCK) {
|
|
123
|
+
if (
|
|
124
|
+
sharedClient.connectTimeoutMillis() == ms &&
|
|
125
|
+
sharedClient.readTimeoutMillis() == ms &&
|
|
126
|
+
sharedClient.writeTimeoutMillis() == ms
|
|
127
|
+
) {
|
|
128
|
+
httpTimeoutMs = ms;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
sharedClient = sharedClient
|
|
132
|
+
.newBuilder()
|
|
133
|
+
.connectTimeout(ms, TimeUnit.MILLISECONDS)
|
|
134
|
+
.readTimeout(ms, TimeUnit.MILLISECONDS)
|
|
135
|
+
.writeTimeout(ms, TimeUnit.MILLISECONDS)
|
|
136
|
+
.build();
|
|
137
|
+
httpTimeoutMs = ms;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
108
141
|
static int manifestMaxConcurrentFiles() {
|
|
109
142
|
return manifestMaxConcurrentFiles(Runtime.getRuntime().availableProcessors());
|
|
110
143
|
}
|
|
@@ -342,10 +375,12 @@ public class DownloadService extends Worker {
|
|
|
342
375
|
logger.debug("doWork isManifest: " + isManifest);
|
|
343
376
|
|
|
344
377
|
if (isManifest) {
|
|
345
|
-
JSONArray manifest = DataManager.getInstance().getAndClearManifest();
|
|
378
|
+
JSONArray manifest = DataManager.getInstance().getAndClearManifest(id);
|
|
346
379
|
if (manifest != null) {
|
|
347
380
|
handleManifestDownload(id, documentsDir, dest, version, sessionKey, publicKey, manifest);
|
|
348
381
|
return createSuccessResult(dest, version, sessionKey, checksum, true);
|
|
382
|
+
} else if (isStopped()) {
|
|
383
|
+
return createFailureResult("download_cancelled");
|
|
349
384
|
} else {
|
|
350
385
|
logger.error("Manifest is null");
|
|
351
386
|
return createFailureResult("Manifest is null");
|
|
@@ -526,8 +561,7 @@ public class DownloadService extends Worker {
|
|
|
526
561
|
try {
|
|
527
562
|
if (tryCopyBuiltinAsset(assets, fileName, targetFile, finalFileHash)) {
|
|
528
563
|
logger.debug("using builtin asset " + fileName);
|
|
529
|
-
} else if (
|
|
530
|
-
copyFile(builtinFile, targetFile);
|
|
564
|
+
} else if (tryCopyBuiltinFile(builtinFile, targetFile, finalFileHash)) {
|
|
531
565
|
logger.debug("using builtin file " + fileName);
|
|
532
566
|
} else if (
|
|
533
567
|
tryCopyFromCache(cacheFile, targetFile, finalFileHash) ||
|
|
@@ -626,9 +660,11 @@ public class DownloadService extends Worker {
|
|
|
626
660
|
URL u = new URL(url);
|
|
627
661
|
httpConn = (HttpURLConnection) u.openConnection();
|
|
628
662
|
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
663
|
+
// Zip can stall longer than a JSON API call; keep a floor so
|
|
664
|
+
// responseTimeout cannot shrink large-bundle downloads.
|
|
665
|
+
int zipTimeoutMs = Math.max(httpTimeoutMs, 60_000);
|
|
666
|
+
httpConn.setConnectTimeout(zipTimeoutMs);
|
|
667
|
+
httpConn.setReadTimeout(zipTimeoutMs);
|
|
632
668
|
|
|
633
669
|
// Reading progress file (if exist)
|
|
634
670
|
long downloadedBytes = 0;
|
|
@@ -678,7 +714,7 @@ public class DownloadService extends Worker {
|
|
|
678
714
|
writer = null;
|
|
679
715
|
}
|
|
680
716
|
|
|
681
|
-
byte[] buffer = new byte[
|
|
717
|
+
byte[] buffer = new byte[CryptoCipher.ioBufferBytes()];
|
|
682
718
|
int lastNotifiedPercent = 0;
|
|
683
719
|
int bytesRead;
|
|
684
720
|
|
|
@@ -801,6 +837,10 @@ public class DownloadService extends Worker {
|
|
|
801
837
|
}
|
|
802
838
|
|
|
803
839
|
private void copyFile(File source, File dest) throws IOException {
|
|
840
|
+
copyFileChannel(source, dest);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
static void copyFileChannel(final File source, final File dest) throws IOException {
|
|
804
844
|
final File parent = dest.getParentFile();
|
|
805
845
|
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
|
806
846
|
throw new IOException("Failed to create parent directory: " + parent.getAbsolutePath());
|
|
@@ -971,33 +1011,19 @@ public class DownloadService extends Worker {
|
|
|
971
1011
|
}
|
|
972
1012
|
}
|
|
973
1013
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
String actualHash = calculateFileHash(file);
|
|
977
|
-
return actualHash.equalsIgnoreCase(expectedHash);
|
|
978
|
-
} catch (Exception e) {
|
|
979
|
-
e.printStackTrace();
|
|
1014
|
+
static boolean tryCopyBuiltinFile(final File builtinFile, final File dest, final String expectedHash) {
|
|
1015
|
+
if (builtinFile == null || dest == null || expectedHash == null || expectedHash.isEmpty() || !builtinFile.isFile()) {
|
|
980
1016
|
return false;
|
|
981
1017
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
986
|
-
byte[] byteArray = new byte[1024];
|
|
987
|
-
int bytesCount = 0;
|
|
988
|
-
|
|
989
|
-
try (FileInputStream fis = new FileInputStream(file)) {
|
|
990
|
-
while ((bytesCount = fis.read(byteArray)) != -1) {
|
|
991
|
-
digest.update(byteArray, 0, bytesCount);
|
|
1018
|
+
try {
|
|
1019
|
+
if (!expectedHash.equalsIgnoreCase(CryptoCipher.calcChecksum(builtinFile))) {
|
|
1020
|
+
return false;
|
|
992
1021
|
}
|
|
1022
|
+
copyFileChannel(builtinFile, dest);
|
|
1023
|
+
return true;
|
|
1024
|
+
} catch (IOException e) {
|
|
1025
|
+
return false;
|
|
993
1026
|
}
|
|
994
|
-
|
|
995
|
-
byte[] bytes = digest.digest();
|
|
996
|
-
StringBuilder sb = new StringBuilder();
|
|
997
|
-
for (byte aByte : bytes) {
|
|
998
|
-
sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
|
|
999
|
-
}
|
|
1000
|
-
return sb.toString();
|
|
1001
1027
|
}
|
|
1002
1028
|
|
|
1003
1029
|
static void decompressBrotli(File input, File output, String fileName) throws IOException {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package ee.forgr.capacitor_updater;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
+
import android.os.Build;
|
|
4
5
|
import androidx.work.BackoffPolicy;
|
|
5
6
|
import androidx.work.Configuration;
|
|
6
7
|
import androidx.work.Constraints;
|
|
@@ -8,9 +9,18 @@ import androidx.work.Data;
|
|
|
8
9
|
import androidx.work.ExistingWorkPolicy;
|
|
9
10
|
import androidx.work.NetworkType;
|
|
10
11
|
import androidx.work.OneTimeWorkRequest;
|
|
12
|
+
import androidx.work.OutOfQuotaPolicy;
|
|
13
|
+
import androidx.work.WorkInfo;
|
|
11
14
|
import androidx.work.WorkManager;
|
|
12
15
|
import androidx.work.WorkRequest;
|
|
16
|
+
import java.util.HashSet;
|
|
17
|
+
import java.util.List;
|
|
18
|
+
import java.util.Set;
|
|
19
|
+
import java.util.concurrent.ExecutorService;
|
|
20
|
+
import java.util.concurrent.Executors;
|
|
21
|
+
import java.util.concurrent.Future;
|
|
13
22
|
import java.util.concurrent.TimeUnit;
|
|
23
|
+
import java.util.concurrent.TimeoutException;
|
|
14
24
|
|
|
15
25
|
public class DownloadWorkerManager {
|
|
16
26
|
|
|
@@ -21,6 +31,7 @@ public class DownloadWorkerManager {
|
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
private static volatile boolean isInitialized = false;
|
|
34
|
+
private static final ExecutorService cancelExecutor = Executors.newSingleThreadExecutor();
|
|
24
35
|
|
|
25
36
|
private static synchronized void initializeIfNeeded(Context context) {
|
|
26
37
|
if (!isInitialized) {
|
|
@@ -120,6 +131,11 @@ public class DownloadWorkerManager {
|
|
|
120
131
|
.addTag(id)
|
|
121
132
|
.addTag(version)
|
|
122
133
|
.addTag("capacitor_updater_download");
|
|
134
|
+
// Android 12+ expedited jobs skip the WorkManager delay without a
|
|
135
|
+
// foreground service. Older APIs require getForegroundInfo().
|
|
136
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
137
|
+
workRequestBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST);
|
|
138
|
+
}
|
|
123
139
|
|
|
124
140
|
// More aggressive retry policy for emulators
|
|
125
141
|
if (isEmulator) {
|
|
@@ -142,17 +158,101 @@ public class DownloadWorkerManager {
|
|
|
142
158
|
|
|
143
159
|
public static void cancelVersionDownload(Context context, String version) {
|
|
144
160
|
initializeIfNeeded(context.getApplicationContext());
|
|
145
|
-
|
|
161
|
+
cancelExecutor.execute(() -> cancelVersionDownloadInternal(context, version, false));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public static boolean cancelVersionDownloadAndAwait(Context context, String version) {
|
|
165
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
166
|
+
Future<?> future = cancelExecutor.submit(() -> cancelVersionDownloadInternal(context, version, true));
|
|
167
|
+
try {
|
|
168
|
+
future.get(10, TimeUnit.SECONDS);
|
|
169
|
+
return true;
|
|
170
|
+
} catch (TimeoutException e) {
|
|
171
|
+
future.cancel(true);
|
|
172
|
+
logger.error("Timed out awaiting version download cancel");
|
|
173
|
+
return false;
|
|
174
|
+
} catch (Exception e) {
|
|
175
|
+
logger.error("Error awaiting version download cancel: " + e.getMessage());
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private static Set<String> collectManifestIdsForVersion(WorkManager workManager, String version) {
|
|
181
|
+
Set<String> downloadIds = new HashSet<>();
|
|
182
|
+
try {
|
|
183
|
+
List<WorkInfo> workInfos = workManager.getWorkInfosByTag(version).get();
|
|
184
|
+
for (WorkInfo workInfo : workInfos) {
|
|
185
|
+
for (String tag : workInfo.getTags()) {
|
|
186
|
+
if (!"capacitor_updater_download".equals(tag) && !version.equals(tag)) {
|
|
187
|
+
downloadIds.add(tag);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch (InterruptedException e) {
|
|
192
|
+
Thread.currentThread().interrupt();
|
|
193
|
+
throw new IllegalStateException("Interrupted while collecting manifest ids before version cancel", e);
|
|
194
|
+
} catch (Exception e) {
|
|
195
|
+
logger.error("Error collecting manifest ids before version cancel: " + e.getMessage());
|
|
196
|
+
}
|
|
197
|
+
return downloadIds;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private static void clearManifestIds(Set<String> downloadIds) {
|
|
201
|
+
for (String downloadId : downloadIds) {
|
|
202
|
+
DataManager.getInstance().clearManifest(downloadId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private static void cancelVersionDownloadInternal(Context context, String version, boolean awaitFinished) {
|
|
207
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
211
|
+
Set<String> downloadIds = collectManifestIdsForVersion(workManager, version);
|
|
212
|
+
workManager.cancelAllWorkByTag(version);
|
|
213
|
+
clearManifestIds(downloadIds);
|
|
214
|
+
if (awaitFinished) {
|
|
215
|
+
awaitVersionWorkFinished(workManager, version);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private static void awaitVersionWorkFinished(WorkManager workManager, String version) {
|
|
220
|
+
for (int i = 0; i < 100; i++) {
|
|
221
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
222
|
+
throw new IllegalStateException("Interrupted while waiting for version download cancel");
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
boolean anyActive = workManager
|
|
226
|
+
.getWorkInfosByTag(version)
|
|
227
|
+
.get()
|
|
228
|
+
.stream()
|
|
229
|
+
.anyMatch((workInfo) -> !workInfo.getState().isFinished());
|
|
230
|
+
if (!anyActive) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
Thread.sleep(100);
|
|
234
|
+
} catch (InterruptedException e) {
|
|
235
|
+
Thread.currentThread().interrupt();
|
|
236
|
+
throw new IllegalStateException("Interrupted while waiting for download cancel", e);
|
|
237
|
+
} catch (Exception e) {
|
|
238
|
+
throw new IllegalStateException("Error waiting for download cancel: " + e.getMessage(), e);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
throw new IllegalStateException("Timed out waiting for version download cancel: " + version);
|
|
146
242
|
}
|
|
147
243
|
|
|
148
244
|
public static void cancelBundleDownload(Context context, String id, String version) {
|
|
149
245
|
String uniqueWorkName = "bundle_" + id + "_" + version;
|
|
150
246
|
initializeIfNeeded(context.getApplicationContext());
|
|
151
|
-
WorkManager.getInstance(context)
|
|
247
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
248
|
+
workManager.cancelUniqueWork(uniqueWorkName);
|
|
249
|
+
DataManager.getInstance().clearManifest(id);
|
|
152
250
|
}
|
|
153
251
|
|
|
154
252
|
public static void cancelAllDownloads(Context context) {
|
|
155
253
|
initializeIfNeeded(context.getApplicationContext());
|
|
156
|
-
WorkManager.getInstance(context)
|
|
254
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
255
|
+
workManager.cancelAllWorkByTag("capacitor_updater_download");
|
|
256
|
+
DataManager.getInstance().clearAllManifests();
|
|
157
257
|
}
|
|
158
258
|
}
|
package/dist/docs.json
CHANGED
|
@@ -4583,7 +4583,7 @@
|
|
|
4583
4583
|
"name": "example"
|
|
4584
4584
|
}
|
|
4585
4585
|
],
|
|
4586
|
-
"docs": "Configure the number of seconds the native plugin should wait before considering
|
|
4586
|
+
"docs": "Configure the number of seconds the native plugin should wait before considering an HTTP timeout.\nApplies to update checks and file downloads. On Android these are idle connect/read/write\ntimeouts and do not cap total download time; on iOS the request timeout also bounds the\ntotal download duration.\n\nOnly available for Android and iOS.",
|
|
4587
4587
|
"complexTypes": [],
|
|
4588
4588
|
"type": "number | undefined"
|
|
4589
4589
|
},
|
|
@@ -15,7 +15,10 @@ declare module '@capacitor/cli' {
|
|
|
15
15
|
*/
|
|
16
16
|
appReadyTimeout?: number;
|
|
17
17
|
/**
|
|
18
|
-
* Configure the number of seconds the native plugin should wait before considering
|
|
18
|
+
* Configure the number of seconds the native plugin should wait before considering an HTTP timeout.
|
|
19
|
+
* Applies to update checks and file downloads. On Android these are idle connect/read/write
|
|
20
|
+
* timeouts and do not cap total download time; on iOS the request timeout also bounds the
|
|
21
|
+
* total download duration.
|
|
19
22
|
*
|
|
20
23
|
* Only available for Android and iOS.
|
|
21
24
|
*
|