@capawesome/capacitor-live-update 7.3.0 → 7.4.0-dev.f18299f8.1767510052

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.
Files changed (31) hide show
  1. package/README.md +221 -2
  2. package/android/build.gradle +2 -0
  3. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +519 -246
  4. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +42 -5
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +134 -3
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +25 -0
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +2 -2
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetConfigOptions.java +19 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBlockedBundlesResult.java +27 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetConfigResult.java +40 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetDownloadedBundlesResult.java +27 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/IsSyncingResult.java +26 -0
  13. package/dist/docs.json +319 -0
  14. package/dist/esm/definitions.d.ts +165 -0
  15. package/dist/esm/definitions.js.map +1 -1
  16. package/dist/esm/web.d.ts +8 -1
  17. package/dist/esm/web.js +21 -0
  18. package/dist/esm/web.js.map +1 -1
  19. package/dist/plugin.cjs.js +21 -0
  20. package/dist/plugin.cjs.js.map +1 -1
  21. package/dist/plugin.js +21 -0
  22. package/dist/plugin.js.map +1 -1
  23. package/ios/Plugin/Classes/Options/SetConfigOptions.swift +14 -0
  24. package/ios/Plugin/Classes/Results/GetBlockedBundlesResult.swift +16 -0
  25. package/ios/Plugin/Classes/Results/GetConfigResult.swift +19 -0
  26. package/ios/Plugin/Classes/Results/GetDownloadedBundlesResult.swift +16 -0
  27. package/ios/Plugin/Classes/Results/IsSyncingResult.swift +16 -0
  28. package/ios/Plugin/LiveUpdate.swift +77 -4
  29. package/ios/Plugin/LiveUpdatePlugin.swift +78 -1
  30. package/ios/Plugin/LiveUpdatePreferences.swift +44 -2
  31. package/package.json +1 -1
@@ -22,18 +22,23 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundle
22
22
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
23
23
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
24
24
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
25
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetConfigOptions;
25
26
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
26
27
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetNextBundleOptions;
27
28
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SyncOptions;
28
29
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
30
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBlockedBundlesResult;
29
31
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundlesResult;
30
32
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetChannelResult;
33
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetConfigResult;
31
34
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCurrentBundleResult;
32
35
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCustomIdResult;
33
36
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdResult;
37
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDownloadedBundlesResult;
34
38
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetNextBundleResult;
35
39
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
36
40
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
41
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingResult;
37
42
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.ReadyResult;
38
43
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
39
44
  import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
@@ -55,8 +60,12 @@ import java.util.ArrayList;
55
60
  import java.util.Arrays;
56
61
  import java.util.List;
57
62
  import java.util.UUID;
63
+ import java.util.concurrent.CountDownLatch;
64
+ import java.util.concurrent.atomic.AtomicBoolean;
65
+ import java.util.concurrent.atomic.AtomicLong;
58
66
  import java.util.concurrent.atomic.AtomicReference;
59
67
  import net.lingala.zip4j.ZipFile;
68
+ import okhttp3.Call;
60
69
  import okhttp3.HttpUrl;
61
70
  import okhttp3.Response;
62
71
  import okhttp3.ResponseBody;
@@ -103,11 +112,18 @@ public class LiveUpdate {
103
112
  this.preferences = new LiveUpdatePreferences(plugin.getContext());
104
113
  this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
105
114
 
115
+ // Check version and reset config if version changed
116
+ checkAndResetConfigIfVersionChanged();
117
+
106
118
  // Start the rollback timer to rollback to the default bundle
107
119
  // if the app is not ready after a certain time
108
120
  startRollbackTimer();
109
121
  }
110
122
 
123
+ public void clearBlockedBundles() {
124
+ preferences.setBlockedBundleIds(null);
125
+ }
126
+
111
127
  public void deleteBundle(@NonNull DeleteBundleOptions options, @NonNull EmptyCallback callback) {
112
128
  String bundleId = options.getBundleId();
113
129
 
@@ -121,7 +137,7 @@ public class LiveUpdate {
121
137
  callback.success();
122
138
  }
123
139
 
124
- public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) throws Exception {
140
+ public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) {
125
141
  ArtifactType artifactType = options.getArtifactType();
126
142
  String bundleId = options.getBundleId();
127
143
  String checksum = options.getChecksum();
@@ -137,44 +153,80 @@ public class LiveUpdate {
137
153
 
138
154
  // Download the bundle
139
155
  if (artifactType == ArtifactType.MANIFEST) {
140
- downloadBundleOfTypeManifest(bundleId, url);
156
+ downloadBundleOfTypeManifest(bundleId, url, callback);
141
157
  } else {
142
- downloadBundleOfTypeZip(bundleId, checksum, signature, url);
158
+ downloadBundleOfTypeZip(bundleId, checksum, signature, url, callback);
143
159
  }
144
- callback.success();
145
160
  }
146
161
 
147
- public void fetchLatestBundle(@NonNull FetchLatestBundleOptions options, @NonNull NonEmptyCallback callback) throws Exception {
148
- GetLatestBundleResponse response = fetchLatestBundle(options);
149
- ArtifactType artifactType = response == null ? null : response.getArtifactType();
150
- String bundleId = response == null ? null : response.getBundleId();
151
- String checksum = response == null ? null : response.getChecksum();
152
- JSONObject customProperties = response == null ? null : response.getCustomProperties();
153
- String downloadUrl = response == null ? null : response.getUrl();
154
- String signature = response == null ? null : response.getSignature();
155
- FetchLatestBundleResult result = new FetchLatestBundleResult(
156
- artifactType,
157
- bundleId,
158
- checksum,
159
- customProperties,
160
- downloadUrl,
161
- signature
162
+ public void fetchLatestBundle(@NonNull FetchLatestBundleOptions options, @NonNull NonEmptyCallback<FetchLatestBundleResult> callback) {
163
+ fetchLatestBundleInternal(
164
+ options,
165
+ new NonEmptyCallback<GetLatestBundleResponse>() {
166
+ @Override
167
+ public void success(@Nullable GetLatestBundleResponse response) {
168
+ ArtifactType artifactType = response == null ? null : response.getArtifactType();
169
+ String bundleId = response == null ? null : response.getBundleId();
170
+ String checksum = response == null ? null : response.getChecksum();
171
+ JSONObject customProperties = response == null ? null : response.getCustomProperties();
172
+ String downloadUrl = response == null ? null : response.getUrl();
173
+ String signature = response == null ? null : response.getSignature();
174
+ FetchLatestBundleResult result = new FetchLatestBundleResult(
175
+ artifactType,
176
+ bundleId,
177
+ checksum,
178
+ customProperties,
179
+ downloadUrl,
180
+ signature
181
+ );
182
+ callback.success(result);
183
+ }
184
+
185
+ @Override
186
+ public void error(@NonNull Exception exception) {
187
+ callback.error(exception);
188
+ }
189
+ }
162
190
  );
191
+ }
192
+
193
+ public void getBlockedBundles(@NonNull NonEmptyCallback<GetBlockedBundlesResult> callback) {
194
+ String blockedIds = preferences.getBlockedBundleIds();
195
+ String[] bundleIds;
196
+ if (blockedIds == null || blockedIds.isEmpty()) {
197
+ bundleIds = new String[0];
198
+ } else {
199
+ bundleIds = blockedIds.split(",");
200
+ }
201
+ GetBlockedBundlesResult result = new GetBlockedBundlesResult(bundleIds);
163
202
  callback.success(result);
164
203
  }
165
204
 
166
205
  public void getBundles(@NonNull NonEmptyCallback callback) {
167
- String[] bundleIds = getBundleIds();
206
+ String[] bundleIds = getDownloadedBundleIds();
168
207
  GetBundlesResult result = new GetBundlesResult(bundleIds);
169
208
  callback.success(result);
170
209
  }
171
210
 
211
+ public void getDownloadedBundles(@NonNull NonEmptyCallback<GetDownloadedBundlesResult> callback) {
212
+ String[] bundleIds = getDownloadedBundleIds();
213
+ GetDownloadedBundlesResult result = new GetDownloadedBundlesResult(bundleIds);
214
+ callback.success(result);
215
+ }
216
+
172
217
  public void getChannel(@NonNull NonEmptyCallback callback) {
173
218
  String channel = getChannel();
174
219
  GetChannelResult result = new GetChannelResult(channel);
175
220
  callback.success(result);
176
221
  }
177
222
 
223
+ public void getConfig(@NonNull NonEmptyCallback<GetConfigResult> callback) {
224
+ String appId = getAppId();
225
+ String autoUpdateStrategy = config.getAutoUpdateStrategy();
226
+ GetConfigResult result = new GetConfigResult(appId, autoUpdateStrategy);
227
+ callback.success(result);
228
+ }
229
+
178
230
  public void getCurrentBundle(@NonNull NonEmptyCallback<GetCurrentBundleResult> callback) {
179
231
  String bundleId = getCurrentBundleId();
180
232
  GetCurrentBundleResult result = new GetCurrentBundleResult(bundleId);
@@ -200,7 +252,7 @@ public class LiveUpdate {
200
252
  }
201
253
 
202
254
  public void getVersionCode(@NonNull NonEmptyCallback callback) throws PackageManager.NameNotFoundException {
203
- String versionCode = getVersionCode();
255
+ String versionCode = getVersionCodeAsString();
204
256
  GetVersionCodeResult result = new GetVersionCodeResult(versionCode);
205
257
  callback.success(result);
206
258
  }
@@ -211,6 +263,11 @@ public class LiveUpdate {
211
263
  callback.success(result);
212
264
  }
213
265
 
266
+ public void isSyncing(@NonNull NonEmptyCallback<IsSyncingResult> callback) {
267
+ IsSyncingResult result = new IsSyncingResult(syncInProgress);
268
+ callback.success(result);
269
+ }
270
+
214
271
  public void handleOnPageLoaded() {
215
272
  // Wait for initial page load to perform auto update to make sure the WebViewLocalServer is initialized.
216
273
  // Otherwise, a NullPointerException may occur for `com.getcapacitor.WebViewLocalServer.getBasePath()`.
@@ -263,6 +320,10 @@ public class LiveUpdate {
263
320
  setNextBundleById(null);
264
321
  }
265
322
 
323
+ public void resetConfig() {
324
+ preferences.setAppId(null);
325
+ }
326
+
266
327
  public void setChannel(@NonNull SetChannelOptions options, @NonNull EmptyCallback callback) {
267
328
  String channel = options.getChannel();
268
329
 
@@ -270,6 +331,11 @@ public class LiveUpdate {
270
331
  callback.success();
271
332
  }
272
333
 
334
+ public void setConfig(@NonNull SetConfigOptions options) {
335
+ String appId = options.getAppId();
336
+ preferences.setAppId(appId);
337
+ }
338
+
273
339
  public void setCustomId(@NonNull SetCustomIdOptions options, @NonNull EmptyCallback callback) {
274
340
  String customId = options.getCustomId();
275
341
 
@@ -294,7 +360,7 @@ public class LiveUpdate {
294
360
  callback.success();
295
361
  }
296
362
 
297
- public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback) throws Exception {
363
+ public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback) {
298
364
  if (syncInProgress) {
299
365
  Exception exception = new Exception(LiveUpdatePlugin.ERROR_SYNC_IN_PROGRESS);
300
366
  callback.error(exception);
@@ -302,55 +368,91 @@ public class LiveUpdate {
302
368
  }
303
369
  syncInProgress = true;
304
370
 
305
- try {
306
- String channel = options.getChannel();
307
- // Fetch the latest bundle
308
- FetchLatestBundleOptions fetchLatestBundleOptions = new FetchLatestBundleOptions(channel);
309
- GetLatestBundleResponse response = fetchLatestBundle(fetchLatestBundleOptions);
310
- if (response == null) {
311
- Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
312
- SyncResult syncResult = new SyncResult(null);
313
- callback.success(syncResult);
314
- return;
315
- }
316
- ArtifactType artifactType = response.getArtifactType();
317
- String latestBundleId = response.getBundleId();
318
- String checksum = response.getChecksum();
319
- String signature = response.getSignature();
320
- String url = response.getUrl();
321
- // Check if the bundle is blocked
322
- if (isBlockedBundleId(latestBundleId)) {
323
- Logger.warn(LiveUpdatePlugin.TAG, "Bundle is blocked and will not be downloaded.");
324
- SyncResult syncResult = new SyncResult(null);
325
- callback.success(syncResult);
326
- return;
327
- }
328
- // Check if the bundle already exists
329
- if (hasBundleById(latestBundleId)) {
330
- String nextBundleId = null;
331
- String currentBundleId = getCurrentBundleId();
332
- if (!latestBundleId.equals(currentBundleId)) {
333
- // Set the next bundle
334
- setNextBundleById(latestBundleId);
335
- nextBundleId = latestBundleId;
371
+ String channel = options.getChannel();
372
+ // Fetch the latest bundle
373
+ FetchLatestBundleOptions fetchLatestBundleOptions = new FetchLatestBundleOptions(channel);
374
+ fetchLatestBundleInternal(
375
+ fetchLatestBundleOptions,
376
+ new NonEmptyCallback<GetLatestBundleResponse>() {
377
+ @Override
378
+ public void success(@Nullable GetLatestBundleResponse response) {
379
+ try {
380
+ if (response == null) {
381
+ Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
382
+ syncInProgress = false;
383
+ SyncResult syncResult = new SyncResult(null);
384
+ callback.success(syncResult);
385
+ return;
386
+ }
387
+ ArtifactType artifactType = response.getArtifactType();
388
+ String latestBundleId = response.getBundleId();
389
+ String checksum = response.getChecksum();
390
+ String signature = response.getSignature();
391
+ String url = response.getUrl();
392
+ // Check if the bundle is blocked
393
+ if (isBlockedBundleId(latestBundleId)) {
394
+ Logger.warn(LiveUpdatePlugin.TAG, "Bundle is blocked and will not be downloaded.");
395
+ syncInProgress = false;
396
+ SyncResult syncResult = new SyncResult(null);
397
+ callback.success(syncResult);
398
+ return;
399
+ }
400
+ // Check if the bundle already exists
401
+ if (hasBundleById(latestBundleId)) {
402
+ String nextBundleId = null;
403
+ String currentBundleId = getCurrentBundleId();
404
+ if (!latestBundleId.equals(currentBundleId)) {
405
+ // Set the next bundle
406
+ setNextBundleById(latestBundleId);
407
+ nextBundleId = latestBundleId;
408
+ }
409
+ syncInProgress = false;
410
+ SyncResult syncResult = new SyncResult(nextBundleId);
411
+ callback.success(syncResult);
412
+ return;
413
+ }
414
+
415
+ // Download the bundle
416
+ EmptyCallback downloadCallback = new EmptyCallback() {
417
+ @Override
418
+ public void success() {
419
+ try {
420
+ // Set the next bundle
421
+ setNextBundleById(latestBundleId);
422
+ syncInProgress = false;
423
+ SyncResult syncResult = new SyncResult(latestBundleId);
424
+ callback.success(syncResult);
425
+ } catch (Exception e) {
426
+ syncInProgress = false;
427
+ callback.error(e);
428
+ }
429
+ }
430
+
431
+ @Override
432
+ public void error(@NonNull Exception exception) {
433
+ syncInProgress = false;
434
+ callback.error(exception);
435
+ }
436
+ };
437
+
438
+ if (artifactType == ArtifactType.MANIFEST) {
439
+ downloadBundleOfTypeManifest(latestBundleId, url, downloadCallback);
440
+ } else {
441
+ downloadBundleOfTypeZip(latestBundleId, checksum, signature, url, downloadCallback);
442
+ }
443
+ } catch (Exception e) {
444
+ syncInProgress = false;
445
+ callback.error(e);
446
+ }
447
+ }
448
+
449
+ @Override
450
+ public void error(@NonNull Exception exception) {
451
+ syncInProgress = false;
452
+ callback.error(exception);
336
453
  }
337
- SyncResult syncResult = new SyncResult(nextBundleId);
338
- callback.success(syncResult);
339
- return;
340
- }
341
- // Download the bundle
342
- if (artifactType == ArtifactType.MANIFEST) {
343
- downloadBundleOfTypeManifest(latestBundleId, url);
344
- } else {
345
- downloadBundleOfTypeZip(latestBundleId, checksum, signature, url);
346
454
  }
347
- // Set the next bundle
348
- setNextBundleById(latestBundleId);
349
- SyncResult syncResult = new SyncResult(latestBundleId);
350
- callback.success(syncResult);
351
- } finally {
352
- syncInProgress = false;
353
- }
455
+ );
354
456
  }
355
457
 
356
458
  private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
@@ -508,7 +610,7 @@ public class LiveUpdate {
508
610
  }
509
611
 
510
612
  private void deleteUnusedBundles() {
511
- String[] bundleIds = getBundleIds();
613
+ String[] bundleIds = getDownloadedBundleIds();
512
614
  for (String bundleId : bundleIds) {
513
615
  if (!isBundleInUse(bundleId)) {
514
616
  deleteBundleById(bundleId);
@@ -516,202 +618,349 @@ public class LiveUpdate {
516
618
  }
517
619
  }
518
620
 
519
- private void downloadAndVerifyFile(
621
+ private Call downloadAndVerifyFile(
520
622
  @NonNull String url,
521
623
  @NonNull File file,
522
624
  @Nullable String checksum,
523
625
  @Nullable String signature,
524
- @Nullable DownloadProgressCallback callback
525
- ) throws Exception {
526
- Response response = httpClient.execute(url);
527
- ResponseBody responseBody = response.body();
528
- if (response.isSuccessful()) {
529
- LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file, callback);
530
- // Verify the file
531
- if (checksum == null) {
532
- checksum = LiveUpdateHttpClient.getChecksumFromResponse(response);
533
- }
534
- if (signature == null) {
535
- signature = LiveUpdateHttpClient.getSignatureFromResponse(response);
626
+ @Nullable DownloadProgressCallback progressCallback,
627
+ @NonNull EmptyCallback completionCallback
628
+ ) {
629
+ return httpClient.enqueue(
630
+ url,
631
+ new NonEmptyCallback<Response>() {
632
+ @Override
633
+ public void success(@NonNull Response response) {
634
+ try {
635
+ if (response.isSuccessful()) {
636
+ ResponseBody responseBody = response.body();
637
+ LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file, progressCallback);
638
+
639
+ // Extract checksum/signature from headers
640
+ String finalChecksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
641
+ String finalSignature = signature == null ? LiveUpdateHttpClient.getSignatureFromResponse(response) : signature;
642
+
643
+ // Verify file
644
+ verifyFile(file, finalChecksum, finalSignature);
645
+ completionCallback.success();
646
+ } else {
647
+ String errorMessage = response.body().string();
648
+ Exception exception = new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
649
+ Logger.error(LiveUpdatePlugin.TAG, errorMessage, exception);
650
+ completionCallback.error(exception);
651
+ }
652
+ } catch (Exception e) {
653
+ completionCallback.error(e);
654
+ }
655
+ }
656
+
657
+ @Override
658
+ public void error(@NonNull Exception exception) {
659
+ completionCallback.error(exception);
660
+ }
536
661
  }
537
- verifyFile(file, checksum, signature);
538
- } else {
539
- Exception exception = new Exception(responseBody.string());
540
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
541
- throw new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
542
- }
662
+ );
543
663
  }
544
664
 
545
- private File downloadBundleFile(
665
+ private Call downloadBundleFile(
546
666
  @NonNull String baseUrl,
547
667
  @NonNull String href,
548
668
  @NonNull File destinationDirectory,
549
- @Nullable DownloadProgressCallback callback
550
- ) throws Exception {
669
+ @Nullable DownloadProgressCallback progressCallback,
670
+ @NonNull EmptyCallback completionCallback
671
+ ) {
551
672
  HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
552
673
  urlBuilder.addQueryParameter("href", href);
553
674
  String url = urlBuilder.build().toString();
554
675
 
555
676
  File destinationFile = new File(destinationDirectory, href);
556
677
  destinationFile.getParentFile().mkdirs();
557
- downloadAndVerifyFile(url, destinationFile, null, null, callback);
558
- return destinationFile;
678
+ return downloadAndVerifyFile(url, destinationFile, null, null, progressCallback, completionCallback);
559
679
  }
560
680
 
561
681
  private void downloadBundleFiles(
562
682
  @NonNull String baseUrl,
563
683
  @NonNull List<ManifestItem> filesToDownload,
564
684
  @NonNull File destinationDirectory,
565
- @Nullable DownloadProgressCallback callback
566
- ) throws Exception {
567
- // Track the total downloaded bytes and total bytes to download for the progress callback
568
- final long[] totalBytesDownloaded = { 0 };
685
+ @Nullable DownloadProgressCallback progressCallback,
686
+ @NonNull EmptyCallback completionCallback
687
+ ) {
688
+ if (filesToDownload.isEmpty()) {
689
+ if (progressCallback != null) {
690
+ progressCallback.onProgress(0, 0);
691
+ }
692
+ completionCallback.success();
693
+ return;
694
+ }
695
+
696
+ // Thread-safe progress tracking
697
+ AtomicLong totalBytesDownloaded = new AtomicLong(0);
569
698
  long totalBytesToDownload = 0;
570
- for (ManifestItem fileToDownload : filesToDownload) {
571
- totalBytesToDownload += fileToDownload.getSizeInBytes();
699
+ for (ManifestItem item : filesToDownload) {
700
+ totalBytesToDownload += item.getSizeInBytes();
572
701
  }
573
- // Limit the number of threads to twice the number of processors
574
- int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
575
- List<Thread> threads = new ArrayList<>();
576
- AtomicReference<Exception> exceptionReference = new AtomicReference<>();
577
- for (ManifestItem fileToDownload : filesToDownload) {
578
- // Wait until a thread is available
579
- while (threads.size() >= maxThreads) {
580
- for (Thread thread : threads) {
581
- if (!thread.isAlive()) {
582
- threads.remove(thread);
583
- break;
702
+ final long finalTotalBytesToDownload = totalBytesToDownload;
703
+
704
+ // Coordination primitives
705
+ CountDownLatch latch = new CountDownLatch(filesToDownload.size());
706
+ AtomicReference<Exception> firstError = new AtomicReference<>();
707
+ AtomicBoolean completionHandled = new AtomicBoolean(false);
708
+ List<Call> activeCalls = new ArrayList<>();
709
+
710
+ // Start all downloads asynchronously (OkHttp's dispatcher handles parallelization)
711
+ for (ManifestItem item : filesToDownload) {
712
+ Call call = downloadBundleFile(
713
+ baseUrl,
714
+ item.getHref(),
715
+ destinationDirectory,
716
+ (downloadedBytes, totalBytes) -> {
717
+ // Per-file progress
718
+ if (progressCallback != null) {
719
+ long totalProgress = totalBytesDownloaded.get() + downloadedBytes;
720
+ progressCallback.onProgress(totalProgress, finalTotalBytesToDownload);
584
721
  }
585
- }
586
- // Wait a bit before checking again
587
- Thread.sleep(10);
588
- }
589
- // Stop if an exception occurred
590
- if (exceptionReference.get() != null) {
591
- break;
592
- }
593
- // Final variable that can be accessed from within inner class
594
- long finalTotalBytesToDownload = totalBytesToDownload;
595
- // Start a new thread
596
- Thread thread = new Thread(() -> {
597
- try {
598
- downloadBundleFile(baseUrl, fileToDownload.getHref(), destinationDirectory, (downloadedBytes, totalBytes) -> {
599
- if (callback != null) {
600
- callback.onProgress(totalBytesDownloaded[0] + downloadedBytes, finalTotalBytesToDownload);
722
+ },
723
+ new EmptyCallback() {
724
+ @Override
725
+ public void success() {
726
+ // Update total progress
727
+ totalBytesDownloaded.addAndGet(item.getSizeInBytes());
728
+ if (progressCallback != null) {
729
+ progressCallback.onProgress(totalBytesDownloaded.get(), finalTotalBytesToDownload);
730
+ }
731
+ latch.countDown();
732
+
733
+ // Check if this was the last download to complete
734
+ if (latch.getCount() == 0 && completionHandled.compareAndSet(false, true)) {
735
+ Exception error = firstError.get();
736
+ if (error != null) {
737
+ completionCallback.error(error);
738
+ } else {
739
+ // Final progress update
740
+ if (progressCallback != null) {
741
+ progressCallback.onProgress(finalTotalBytesToDownload, finalTotalBytesToDownload);
742
+ }
743
+ completionCallback.success();
744
+ }
745
+ }
746
+ }
747
+
748
+ @Override
749
+ public void error(@NonNull Exception e) {
750
+ // Capture first error and cancel all remaining downloads
751
+ if (firstError.compareAndSet(null, e)) {
752
+ Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + item.getHref(), e);
753
+ // Cancel all in-flight downloads (fail-fast)
754
+ synchronized (activeCalls) {
755
+ for (Call activeCall : activeCalls) {
756
+ if (!activeCall.isCanceled() && !activeCall.isExecuted()) {
757
+ activeCall.cancel();
758
+ }
759
+ }
760
+ }
761
+ }
762
+ latch.countDown();
763
+
764
+ // Check if this was the last download to complete
765
+ if (latch.getCount() == 0 && completionHandled.compareAndSet(false, true)) {
766
+ completionCallback.error(firstError.get());
601
767
  }
602
- });
603
- if (callback != null) {
604
- totalBytesDownloaded[0] += fileToDownload.getSizeInBytes();
605
- callback.onProgress(totalBytesDownloaded[0], finalTotalBytesToDownload);
606
768
  }
607
- } catch (Exception exception) {
608
- exceptionReference.set(exception);
609
- Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + fileToDownload.getHref(), exception);
610
769
  }
611
- });
612
- threads.add(thread);
613
- thread.start();
614
- }
615
- // Wait for all threads to finish
616
- for (Thread thread : threads) {
617
- thread.join();
618
- }
619
- // Throw the exception if one occurred
620
- Exception exception = exceptionReference.get();
621
- if (exception != null) {
622
- throw exception;
623
- }
624
- // Call the callback one last time to make sure the progress is at 100%
625
- if (callback != null) {
626
- callback.onProgress(totalBytesToDownload, totalBytesToDownload);
770
+ );
771
+ synchronized (activeCalls) {
772
+ activeCalls.add(call);
773
+ }
627
774
  }
628
775
  }
629
776
 
630
- private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
631
- // Create a temporary directory
632
- File temporaryDirectory = createTemporaryDirectory();
633
- // Download the latest manifest
634
- File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory, null);
635
- Manifest latestManifest = loadManifest(latestManifestFile);
636
- // Load the current manifest
637
- Manifest currentManifest = loadCurrentManifest();
638
- // Compare the manifests
639
- List<ManifestItem> itemsToCopy = new ArrayList<>();
640
- List<ManifestItem> itemsToDownload = new ArrayList<>();
641
- if (currentManifest == null) {
642
- itemsToDownload.addAll(latestManifest.getItems());
643
- } else {
644
- itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
645
- itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
646
- }
647
- // Copy the files
648
- List<ManifestItem> missingItems = copyCurrentBundleFilesAndReturnFailures(itemsToCopy, temporaryDirectory);
649
- // If items could not be copied, add them to the list of items to download
650
- if (!missingItems.isEmpty()) {
651
- itemsToDownload.addAll(missingItems);
777
+ private void downloadBundleOfTypeManifest(
778
+ @NonNull String bundleId,
779
+ @NonNull String downloadUrl,
780
+ @NonNull EmptyCallback completionCallback
781
+ ) {
782
+ try {
783
+ // Create a temporary directory
784
+ File temporaryDirectory = createTemporaryDirectory();
785
+
786
+ // Download the latest manifest
787
+ downloadBundleFile(
788
+ downloadUrl,
789
+ manifestFileName,
790
+ temporaryDirectory,
791
+ null,
792
+ new EmptyCallback() {
793
+ @Override
794
+ public void success() {
795
+ try {
796
+ File latestManifestFile = new File(temporaryDirectory, manifestFileName);
797
+ Manifest latestManifest = loadManifest(latestManifestFile);
798
+ // Load the current manifest
799
+ Manifest currentManifest = loadCurrentManifest();
800
+ // Compare the manifests
801
+ List<ManifestItem> itemsToCopy = new ArrayList<>();
802
+ List<ManifestItem> itemsToDownload = new ArrayList<>();
803
+ if (currentManifest == null) {
804
+ itemsToDownload.addAll(latestManifest.getItems());
805
+ } else {
806
+ itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
807
+ itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
808
+ }
809
+ // Copy the files
810
+ List<ManifestItem> missingItems = copyCurrentBundleFilesAndReturnFailures(itemsToCopy, temporaryDirectory);
811
+ // If items could not be copied, add them to the list of items to download
812
+ if (!missingItems.isEmpty()) {
813
+ itemsToDownload.addAll(missingItems);
814
+ }
815
+
816
+ // Download the files
817
+ downloadBundleFiles(
818
+ downloadUrl,
819
+ itemsToDownload,
820
+ temporaryDirectory,
821
+ (downloadedBytes, totalBytes) -> {
822
+ DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(
823
+ bundleId,
824
+ downloadedBytes,
825
+ totalBytes
826
+ );
827
+ notifyDownloadBundleProgressListeners(event);
828
+ },
829
+ new EmptyCallback() {
830
+ @Override
831
+ public void success() {
832
+ try {
833
+ // Add the bundle
834
+ addBundleOfTypeManifest(bundleId, temporaryDirectory);
835
+ completionCallback.success();
836
+ } catch (Exception e) {
837
+ completionCallback.error(e);
838
+ }
839
+ }
840
+
841
+ @Override
842
+ public void error(@NonNull Exception exception) {
843
+ completionCallback.error(exception);
844
+ }
845
+ }
846
+ );
847
+ } catch (Exception e) {
848
+ completionCallback.error(e);
849
+ }
850
+ }
851
+
852
+ @Override
853
+ public void error(@NonNull Exception exception) {
854
+ completionCallback.error(exception);
855
+ }
856
+ }
857
+ );
858
+ } catch (Exception e) {
859
+ completionCallback.error(e);
652
860
  }
653
- // Download the files
654
- downloadBundleFiles(downloadUrl, itemsToDownload, temporaryDirectory, (downloadedBytes, totalBytes) -> {
655
- DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
656
- notifyDownloadBundleProgressListeners(event);
657
- });
658
- // Add the bundle
659
- addBundleOfTypeManifest(bundleId, temporaryDirectory);
660
861
  }
661
862
 
662
863
  private void downloadBundleOfTypeZip(
663
864
  @NonNull String bundleId,
664
865
  @Nullable String checksum,
665
866
  @Nullable String signature,
666
- @NonNull String downloadUrl
667
- ) throws Exception {
867
+ @NonNull String downloadUrl,
868
+ @NonNull EmptyCallback completionCallback
869
+ ) {
668
870
  File file = buildTemporaryZipFile();
669
871
  // Download the bundle
670
- downloadAndVerifyFile(downloadUrl, file, checksum, signature, (downloadedBytes, totalBytes) -> {
671
- DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
672
- notifyDownloadBundleProgressListeners(event);
673
- });
674
- // Add the bundle
675
- addBundleOfTypeZip(bundleId, file);
676
- // Delete the temporary file
677
- file.delete();
872
+ downloadAndVerifyFile(
873
+ downloadUrl,
874
+ file,
875
+ checksum,
876
+ signature,
877
+ (downloadedBytes, totalBytes) -> {
878
+ DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
879
+ notifyDownloadBundleProgressListeners(event);
880
+ },
881
+ new EmptyCallback() {
882
+ @Override
883
+ public void success() {
884
+ try {
885
+ // Add the bundle
886
+ addBundleOfTypeZip(bundleId, file);
887
+ // Delete the temporary file
888
+ file.delete();
889
+ completionCallback.success();
890
+ } catch (Exception e) {
891
+ completionCallback.error(e);
892
+ }
893
+ }
894
+
895
+ @Override
896
+ public void error(@NonNull Exception exception) {
897
+ // Delete the temporary file on error
898
+ file.delete();
899
+ completionCallback.error(exception);
900
+ }
901
+ }
902
+ );
678
903
  }
679
904
 
680
- @Nullable
681
- private GetLatestBundleResponse fetchLatestBundle(@NonNull FetchLatestBundleOptions options) throws Exception {
682
- String channel = options.getChannel() == null ? getChannel() : options.getChannel();
683
- String url = new HttpUrl.Builder()
684
- .scheme("https")
685
- .host(config.getServerDomain())
686
- .addPathSegment("v1")
687
- .addPathSegment("apps")
688
- .addPathSegment(config.getAppId())
689
- .addPathSegment("bundles")
690
- .addPathSegment("latest")
691
- .addQueryParameter("appVersionCode", getVersionCode())
692
- .addQueryParameter("appVersionName", getVersionName())
693
- .addQueryParameter("bundleId", getCurrentBundleId())
694
- .addQueryParameter("channelName", channel)
695
- .addQueryParameter("customId", preferences.getCustomId())
696
- .addQueryParameter("deviceId", getDeviceId())
697
- .addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
698
- .addQueryParameter("platform", "0")
699
- .addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
700
- .build()
701
- .toString();
702
- Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
703
- Response response = httpClient.execute(url);
704
- String responseBodyString = response.body().string();
705
- Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
706
- if (response.isSuccessful()) {
707
- JSONObject responseJson = new JSONObject(responseBodyString);
708
- return new GetLatestBundleResponse(responseJson);
709
- } else {
710
- return null;
905
+ private void fetchLatestBundleInternal(
906
+ @NonNull FetchLatestBundleOptions options,
907
+ @NonNull NonEmptyCallback<GetLatestBundleResponse> callback
908
+ ) {
909
+ try {
910
+ String channel = options.getChannel() == null ? getChannel() : options.getChannel();
911
+ String url = new HttpUrl.Builder()
912
+ .scheme("https")
913
+ .host(config.getServerDomain())
914
+ .addPathSegment("v1")
915
+ .addPathSegment("apps")
916
+ .addPathSegment(getAppId())
917
+ .addPathSegment("bundles")
918
+ .addPathSegment("latest")
919
+ .addQueryParameter("appVersionCode", getVersionCodeAsString())
920
+ .addQueryParameter("appVersionName", getVersionName())
921
+ .addQueryParameter("bundleId", getCurrentBundleId())
922
+ .addQueryParameter("channelName", channel)
923
+ .addQueryParameter("customId", preferences.getCustomId())
924
+ .addQueryParameter("deviceId", getDeviceId())
925
+ .addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
926
+ .addQueryParameter("platform", "0")
927
+ .addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
928
+ .build()
929
+ .toString();
930
+ Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
931
+
932
+ httpClient.enqueue(
933
+ url,
934
+ new NonEmptyCallback<Response>() {
935
+ @Override
936
+ public void success(@NonNull Response response) {
937
+ try {
938
+ String responseBodyString = response.body().string();
939
+ Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
940
+ if (response.isSuccessful()) {
941
+ JSONObject responseJson = new JSONObject(responseBodyString);
942
+ GetLatestBundleResponse result = new GetLatestBundleResponse(responseJson);
943
+ callback.success(result);
944
+ } else {
945
+ callback.success(null);
946
+ }
947
+ } catch (Exception e) {
948
+ callback.error(e);
949
+ }
950
+ }
951
+
952
+ @Override
953
+ public void error(@NonNull Exception exception) {
954
+ callback.error(exception);
955
+ }
956
+ }
957
+ );
958
+ } catch (Exception e) {
959
+ callback.error(e);
711
960
  }
712
961
  }
713
962
 
714
- private String[] getBundleIds() {
963
+ private String[] getDownloadedBundleIds() {
715
964
  File bundlesDirectory = buildBundlesDirectory();
716
965
  File[] bundles = bundlesDirectory.listFiles();
717
966
  if (bundles == null) {
@@ -751,6 +1000,15 @@ public class LiveUpdate {
751
1000
  return checksum.toString();
752
1001
  }
753
1002
 
1003
+ @Nullable
1004
+ private String getAppId() {
1005
+ String appId = preferences.getAppId();
1006
+ if (appId != null) {
1007
+ return appId;
1008
+ }
1009
+ return config.getAppId();
1010
+ }
1011
+
754
1012
  @Nullable
755
1013
  private String getChannel() {
756
1014
  String channel = null;
@@ -784,10 +1042,10 @@ public class LiveUpdate {
784
1042
 
785
1043
  @NonNull
786
1044
  private String getDeviceId() {
787
- String deviceId = preferences.getDeviceIdForApp(config.getAppId());
1045
+ String deviceId = preferences.getDeviceIdForApp(getAppId());
788
1046
  if (deviceId == null) {
789
1047
  deviceId = UUID.randomUUID().toString().toLowerCase();
790
- preferences.setDeviceIdForApp(config.getAppId(), deviceId);
1048
+ preferences.setDeviceIdForApp(getAppId(), deviceId);
791
1049
  }
792
1050
  return deviceId;
793
1051
  }
@@ -828,8 +1086,12 @@ public class LiveUpdate {
828
1086
  return preferences.getPreviousBundleId();
829
1087
  }
830
1088
 
831
- private String getVersionCode() throws PackageManager.NameNotFoundException {
832
- return String.valueOf(getPackageInfo().versionCode);
1089
+ private int getVersionCodeAsInt() throws PackageManager.NameNotFoundException {
1090
+ return getPackageInfo().versionCode;
1091
+ }
1092
+
1093
+ private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
1094
+ return String.valueOf(getVersionCodeAsInt());
833
1095
  }
834
1096
 
835
1097
  private String getVersionName() throws PackageManager.NameNotFoundException {
@@ -897,27 +1159,21 @@ public class LiveUpdate {
897
1159
  // Update the timestamp
898
1160
  lastAutoUpdateCheckTimestamp = now;
899
1161
 
900
- // Run sync in background thread
901
- new Thread(() -> {
902
- try {
903
- Logger.debug(LiveUpdatePlugin.TAG, "Auto-update started.");
904
- SyncOptions options = new SyncOptions((String) null);
905
- NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
906
- @Override
907
- public void success(Result result) {
908
- Logger.debug(LiveUpdatePlugin.TAG, "Auto-update completed successfully.");
909
- }
1162
+ // Run sync
1163
+ Logger.debug(LiveUpdatePlugin.TAG, "Auto-update started.");
1164
+ SyncOptions options = new SyncOptions((String) null);
1165
+ NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
1166
+ @Override
1167
+ public void success(@NonNull Result result) {
1168
+ Logger.debug(LiveUpdatePlugin.TAG, "Auto-update completed successfully.");
1169
+ }
910
1170
 
911
- @Override
912
- public void error(Exception exception) {
913
- Logger.error(LiveUpdatePlugin.TAG, "Auto-update failed: " + exception.getMessage(), exception);
914
- }
915
- };
916
- sync(options, callback);
917
- } catch (Exception exception) {
1171
+ @Override
1172
+ public void error(@NonNull Exception exception) {
918
1173
  Logger.error(LiveUpdatePlugin.TAG, "Auto-update failed: " + exception.getMessage(), exception);
919
1174
  }
920
- }).start();
1175
+ };
1176
+ sync(options, callback);
921
1177
  }
922
1178
 
923
1179
  private void rollback() {
@@ -981,6 +1237,8 @@ public class LiveUpdate {
981
1237
  this.plugin.getBridge().setServerBasePath(path);
982
1238
  }
983
1239
  this.plugin.getBridge().reload();
1240
+ // Notify listeners
1241
+ notifyReloadedListeners();
984
1242
  }
985
1243
 
986
1244
  /**
@@ -1003,6 +1261,10 @@ public class LiveUpdate {
1003
1261
  plugin.notifyNextBundleSetListeners(event);
1004
1262
  }
1005
1263
 
1264
+ private void notifyReloadedListeners() {
1265
+ plugin.notifyReloadedListeners();
1266
+ }
1267
+
1006
1268
  private void addBlockedBundleId(@NonNull String bundleId) {
1007
1269
  String blockedIds = preferences.getBlockedBundleIds();
1008
1270
  List<String> blockedList = new ArrayList<>();
@@ -1048,9 +1310,20 @@ public class LiveUpdate {
1048
1310
  this.webViewSettingsEditor.commit();
1049
1311
  }
1050
1312
 
1051
- /**
1052
- * @param bundleId The bundle ID to save as the previous bundle. If `null`, the value will be removed.
1053
- */
1313
+ private void checkAndResetConfigIfVersionChanged() throws PackageManager.NameNotFoundException {
1314
+ int currentVersionCode = getVersionCodeAsInt();
1315
+ int lastVersionCode = preferences.getLastVersionCode();
1316
+
1317
+ if (lastVersionCode == -1 || lastVersionCode != currentVersionCode) {
1318
+ Logger.debug(
1319
+ LiveUpdatePlugin.TAG,
1320
+ "App version changed (last: " + lastVersionCode + ", current: " + currentVersionCode + "), resetting config."
1321
+ );
1322
+ resetConfig();
1323
+ preferences.setLastVersionCode(currentVersionCode);
1324
+ }
1325
+ }
1326
+
1054
1327
  private void setPreviousBundleId(@Nullable String bundleId) {
1055
1328
  preferences.setPreviousBundleId(bundleId);
1056
1329
  }