@capawesome/capacitor-live-update 6.7.2 → 7.1.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.
Files changed (43) hide show
  1. package/CapawesomeCapacitorLiveUpdate.podspec +2 -2
  2. package/Package.swift +32 -0
  3. package/README.md +102 -88
  4. package/android/build.gradle +9 -9
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +192 -133
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +6 -27
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +7 -2
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +51 -77
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +12 -5
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +27 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/DownloadBundleProgressEvent.java +32 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +16 -1
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +29 -1
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ReadyResult.java +32 -0
  15. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/DownloadProgressCallback.java +5 -0
  16. package/dist/docs.json +228 -147
  17. package/dist/esm/definitions.d.ts +140 -49
  18. package/dist/esm/definitions.js.map +1 -1
  19. package/dist/esm/index.js +1 -1
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/web.d.ts +2 -2
  22. package/dist/esm/web.js.map +1 -1
  23. package/dist/plugin.cjs.js +1 -3
  24. package/dist/plugin.cjs.js.map +1 -1
  25. package/dist/plugin.js +1 -3
  26. package/dist/plugin.js.map +1 -1
  27. package/ios/Plugin/Classes/Events/DownloadBundleProgressEvent.swift +25 -0
  28. package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +7 -1
  29. package/ios/Plugin/Classes/Results/{FetchLatestBundle.swift → FetchLatestBundleResult.swift} +16 -1
  30. package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
  31. package/ios/Plugin/LiveUpdate.swift +171 -154
  32. package/ios/Plugin/LiveUpdateConfig.swift +2 -4
  33. package/ios/Plugin/LiveUpdateHttpClient.swift +6 -2
  34. package/ios/Plugin/LiveUpdatePlugin.swift +45 -37
  35. package/ios/Plugin/LiveUpdatePreferences.swift +6 -20
  36. package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +3 -0
  37. package/package.json +17 -18
  38. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetBundleOptions.java +0 -18
  39. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBundleResult.java +0 -24
  40. package/ios/Plugin/Classes/Options/SetBundleOptions.swift +0 -14
  41. package/ios/Plugin/Classes/Results/GetBundleResult.swift +0 -16
  42. package/ios/Plugin/LiveUpdatePlugin.h +0 -10
  43. package/ios/Plugin/LiveUpdatePlugin.m +0 -27
@@ -10,21 +10,21 @@ import android.os.Handler;
10
10
  import android.util.Base64;
11
11
  import androidx.annotation.NonNull;
12
12
  import androidx.annotation.Nullable;
13
+ import com.getcapacitor.Bridge;
13
14
  import com.getcapacitor.Logger;
14
15
  import com.getcapacitor.plugin.WebView;
15
16
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.Manifest;
16
17
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.ManifestItem;
17
18
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
19
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.DownloadBundleProgressEvent;
18
20
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundleOptions;
19
21
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
20
22
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
21
- import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetBundleOptions;
22
23
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
23
24
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
24
25
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetNextBundleOptions;
25
26
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SyncOptions;
26
27
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
27
- import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundleResult;
28
28
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundlesResult;
29
29
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetChannelResult;
30
30
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCurrentBundleResult;
@@ -33,8 +33,10 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdR
33
33
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetNextBundleResult;
34
34
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
35
35
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
36
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.ReadyResult;
36
37
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
37
38
  import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
39
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.DownloadProgressCallback;
38
40
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
39
41
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
40
42
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
@@ -68,10 +70,7 @@ public class LiveUpdate {
68
70
  @NonNull
69
71
  private final LiveUpdateConfig config;
70
72
 
71
- private final String defaultWebAssetDir;
72
-
73
- @NonNull
74
- private final String host;
73
+ private final String defaultWebAssetDir = Bridge.DEFAULT_WEB_ASSET_DIR;
75
74
 
76
75
  @NonNull
77
76
  private final LiveUpdateHttpClient httpClient;
@@ -89,39 +88,29 @@ public class LiveUpdate {
89
88
  private final Handler rollbackHandler = new Handler();
90
89
  private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
91
90
 
91
+ private boolean rollbackPerformed = false;
92
+
92
93
  public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
93
94
  this.config = config;
94
- this.defaultWebAssetDir = plugin.getBridge().DEFAULT_WEB_ASSET_DIR;
95
- if (config.getLocation() != null && config.getLocation().equals("eu")) {
96
- this.host = "api.cloud.capawesome.eu";
97
- } else {
98
- this.host = "api.cloud.capawesome.io";
99
- }
100
95
  this.httpClient = new LiveUpdateHttpClient(config);
101
96
  this.plugin = plugin;
102
97
  this.preferences = new LiveUpdatePreferences(plugin.getContext());
103
98
  this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
104
99
 
105
- if (config.getEnabled()) {
106
- if (wasUpdated() && config.getResetOnUpdate()) {
107
- Logger.debug(LiveUpdatePlugin.TAG, "App was updated. Resetting to default bundle.");
108
- reset();
109
- } else {
110
- startRollbackTimer();
111
- }
112
- saveCurrentVersionCode();
113
- }
100
+ // Start the rollback timer to rollback to the default bundle
101
+ // if the app is not ready after a certain time
102
+ startRollbackTimer();
114
103
  }
115
104
 
116
105
  public void deleteBundle(@NonNull DeleteBundleOptions options, @NonNull EmptyCallback callback) {
117
106
  String bundleId = options.getBundleId();
118
107
 
119
- if (!hasBundle(bundleId)) {
108
+ if (!hasBundleById(bundleId)) {
120
109
  Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
121
110
  callback.error(exception);
122
111
  return;
123
112
  }
124
- deleteBundle(bundleId);
113
+ deleteBundleById(bundleId);
125
114
 
126
115
  callback.success();
127
116
  }
@@ -130,10 +119,11 @@ public class LiveUpdate {
130
119
  ArtifactType artifactType = options.getArtifactType();
131
120
  String bundleId = options.getBundleId();
132
121
  String checksum = options.getChecksum();
122
+ String signature = options.getSignature();
133
123
  String url = options.getUrl();
134
124
 
135
125
  // Check if the bundle already exists
136
- if (hasBundle(bundleId)) {
126
+ if (hasBundleById(bundleId)) {
137
127
  Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_EXISTS);
138
128
  callback.error(exception);
139
129
  return;
@@ -143,7 +133,7 @@ public class LiveUpdate {
143
133
  if (artifactType == ArtifactType.MANIFEST) {
144
134
  downloadBundleOfTypeManifest(bundleId, url);
145
135
  } else {
146
- downloadBundleOfTypeZip(bundleId, url, checksum);
136
+ downloadBundleOfTypeZip(bundleId, checksum, signature, url);
147
137
  }
148
138
  callback.success();
149
139
  }
@@ -152,17 +142,18 @@ public class LiveUpdate {
152
142
  GetLatestBundleResponse response = fetchLatestBundle(options);
153
143
  ArtifactType artifactType = response == null ? null : response.getArtifactType();
154
144
  String bundleId = response == null ? null : response.getBundleId();
145
+ String checksum = response == null ? null : response.getChecksum();
146
+ JSONObject customProperties = response == null ? null : response.getCustomProperties();
155
147
  String downloadUrl = response == null ? null : response.getUrl();
156
- FetchLatestBundleResult result = new FetchLatestBundleResult(artifactType, bundleId, downloadUrl);
157
- callback.success(result);
158
- }
159
-
160
- public void getBundle(@NonNull NonEmptyCallback callback) {
161
- String bundleId = getCurrentBundleId();
162
- if (bundleId.equals(defaultWebAssetDir)) {
163
- bundleId = null;
164
- }
165
- GetBundleResult result = new GetBundleResult(bundleId);
148
+ String signature = response == null ? null : response.getSignature();
149
+ FetchLatestBundleResult result = new FetchLatestBundleResult(
150
+ artifactType,
151
+ bundleId,
152
+ checksum,
153
+ customProperties,
154
+ downloadUrl,
155
+ signature
156
+ );
166
157
  callback.success(result);
167
158
  }
168
159
 
@@ -180,9 +171,6 @@ public class LiveUpdate {
180
171
 
181
172
  public void getCurrentBundle(@NonNull NonEmptyCallback<GetCurrentBundleResult> callback) {
182
173
  String bundleId = getCurrentBundleId();
183
- if (bundleId.equals(defaultWebAssetDir)) {
184
- bundleId = null;
185
- }
186
174
  GetCurrentBundleResult result = new GetCurrentBundleResult(bundleId);
187
175
  callback.success(result);
188
176
  }
@@ -201,9 +189,6 @@ public class LiveUpdate {
201
189
 
202
190
  public void getNextBundle(@NonNull NonEmptyCallback<GetNextBundleResult> callback) {
203
191
  String bundleId = getNextBundleId();
204
- if (bundleId.equals(defaultWebAssetDir)) {
205
- bundleId = null;
206
- }
207
192
  GetNextBundleResult result = new GetNextBundleResult(bundleId);
208
193
  callback.success(result);
209
194
  }
@@ -220,14 +205,27 @@ public class LiveUpdate {
220
205
  callback.success(result);
221
206
  }
222
207
 
223
- public void ready() {
208
+ public void ready(@NonNull NonEmptyCallback callback) {
224
209
  Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
210
+ if (config.getReadyTimeout() <= 0) {
211
+ Logger.warn(LiveUpdatePlugin.TAG, "Ready timeout is set to 0. Automatic rollback is disabled.");
212
+ }
225
213
  // Stop the rollback timer
226
214
  stopRollbackTimer();
227
215
  // Delete unused bundles
228
216
  if (config.getAutoDeleteBundles()) {
229
217
  deleteUnusedBundles();
230
218
  }
219
+ // Get the current and previous bundle IDs
220
+ String currentBundleId = getCurrentBundleId();
221
+ String previousBundleId = getPreviousBundleId();
222
+ // Return the result
223
+ ReadyResult result = new ReadyResult(currentBundleId, previousBundleId, rollbackPerformed);
224
+ callback.success(result);
225
+ // Set the new previous bundle ID
226
+ setPreviousBundleId(currentBundleId);
227
+ // Reset the rollback flag
228
+ rollbackPerformed = false;
231
229
  }
232
230
 
233
231
  public void reload() {
@@ -237,20 +235,7 @@ public class LiveUpdate {
237
235
  }
238
236
 
239
237
  public void reset() {
240
- setNextCapacitorServerPathToDefaultWebAssetDir();
241
- }
242
-
243
- public void setBundle(@NonNull SetBundleOptions options, @NonNull EmptyCallback callback) {
244
- String bundleId = options.getBundleId();
245
-
246
- if (!hasBundle(bundleId)) {
247
- Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
248
- callback.error(exception);
249
- return;
250
- }
251
-
252
- setNextBundle(bundleId);
253
- callback.success();
238
+ setNextBundleById(null);
254
239
  }
255
240
 
256
241
  public void setChannel(@NonNull SetChannelOptions options, @NonNull EmptyCallback callback) {
@@ -273,8 +258,8 @@ public class LiveUpdate {
273
258
  if (bundleId == null) {
274
259
  reset();
275
260
  } else {
276
- if (hasBundle(bundleId)) {
277
- setNextBundle(bundleId);
261
+ if (hasBundleById(bundleId)) {
262
+ setNextBundleById(bundleId);
278
263
  } else {
279
264
  Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
280
265
  callback.error(exception);
@@ -297,14 +282,16 @@ public class LiveUpdate {
297
282
  }
298
283
  ArtifactType artifactType = response.getArtifactType();
299
284
  String latestBundleId = response.getBundleId();
285
+ String checksum = response.getChecksum();
286
+ String signature = response.getSignature();
300
287
  String url = response.getUrl();
301
288
  // Check if the bundle already exists
302
- if (hasBundle(latestBundleId)) {
289
+ if (hasBundleById(latestBundleId)) {
303
290
  String nextBundleId = null;
304
291
  String currentBundleId = getCurrentBundleId();
305
292
  if (!latestBundleId.equals(currentBundleId)) {
306
293
  // Set the next bundle
307
- setNextBundle(latestBundleId);
294
+ setNextBundleById(latestBundleId);
308
295
  nextBundleId = latestBundleId;
309
296
  }
310
297
  SyncResult syncResult = new SyncResult(nextBundleId);
@@ -315,10 +302,10 @@ public class LiveUpdate {
315
302
  if (artifactType == ArtifactType.MANIFEST) {
316
303
  downloadBundleOfTypeManifest(latestBundleId, url);
317
304
  } else {
318
- downloadBundleOfTypeZip(latestBundleId, url, null);
305
+ downloadBundleOfTypeZip(latestBundleId, checksum, signature, url);
319
306
  }
320
307
  // Set the next bundle
321
- setNextBundle(latestBundleId);
308
+ setNextBundleById(latestBundleId);
322
309
  SyncResult syncResult = new SyncResult(latestBundleId);
323
310
  callback.success(syncResult);
324
311
  }
@@ -367,9 +354,10 @@ public class LiveUpdate {
367
354
  return new File(plugin.getContext().getCacheDir(), fileName);
368
355
  }
369
356
 
370
- private void copyCurrentBundleFile(@NonNull String href, @NonNull File destinationDirectory) throws IOException {
357
+ private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
358
+ String href = fileToCopy.getHref();
371
359
  String currentBundleId = getCurrentBundleId();
372
- if (currentBundleId.equals(defaultWebAssetDir)) {
360
+ if (currentBundleId == null) {
373
361
  // Create the source input stream
374
362
  AssetManager assets = plugin.getContext().getAssets();
375
363
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
@@ -391,9 +379,9 @@ public class LiveUpdate {
391
379
  }
392
380
  }
393
381
 
394
- private void copyCurrentBundleFiles(@NonNull List<String> hrefs, @NonNull File destinationDirectory) throws IOException {
395
- for (String href : hrefs) {
396
- copyCurrentBundleFile(href, destinationDirectory);
382
+ private void copyCurrentBundleFiles(@NonNull List<ManifestItem> filesToCopy, @NonNull File destinationDirectory) throws IOException {
383
+ for (ManifestItem fileToCopy : filesToCopy) {
384
+ copyCurrentBundleFile(fileToCopy, destinationDirectory);
397
385
  }
398
386
  }
399
387
 
@@ -446,14 +434,14 @@ public class LiveUpdate {
446
434
  return file;
447
435
  }
448
436
 
449
- private void deleteBundle(@NonNull String bundleId) {
437
+ private void deleteBundleById(@NonNull String bundleId) {
450
438
  // Delete the bundle directory
451
439
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
452
440
  deleteFileRecursively(bundleDirectory);
453
441
  // Reset the next bundle if it is the deleted bundle
454
442
  String nextBundleId = getNextBundleId();
455
443
  if (bundleId.equals(nextBundleId)) {
456
- setNextCapacitorServerPathToDefaultWebAssetDir();
444
+ setNextBundleById(null);
457
445
  }
458
446
  }
459
447
 
@@ -470,19 +458,29 @@ public class LiveUpdate {
470
458
  String[] bundleIds = getBundleIds();
471
459
  for (String bundleId : bundleIds) {
472
460
  if (!isBundleInUse(bundleId)) {
473
- deleteBundle(bundleId);
461
+ deleteBundleById(bundleId);
474
462
  }
475
463
  }
476
464
  }
477
465
 
478
- private void downloadAndVerifyFile(@NonNull String url, @NonNull File file, @Nullable String checksum) throws Exception {
466
+ private void downloadAndVerifyFile(
467
+ @NonNull String url,
468
+ @NonNull File file,
469
+ @Nullable String checksum,
470
+ @Nullable String signature,
471
+ @Nullable DownloadProgressCallback callback
472
+ ) throws Exception {
479
473
  Response response = httpClient.execute(url);
480
474
  ResponseBody responseBody = response.body();
481
475
  if (response.isSuccessful()) {
482
- LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file);
476
+ LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file, callback);
483
477
  // Verify the file
484
- checksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
485
- String signature = LiveUpdateHttpClient.getSignatureFromResponse(response);
478
+ if (checksum == null) {
479
+ checksum = LiveUpdateHttpClient.getChecksumFromResponse(response);
480
+ }
481
+ if (signature == null) {
482
+ signature = LiveUpdateHttpClient.getSignatureFromResponse(response);
483
+ }
486
484
  verifyFile(file, checksum, signature);
487
485
  } else {
488
486
  Exception exception = new Exception(responseBody.string());
@@ -491,24 +489,39 @@ public class LiveUpdate {
491
489
  }
492
490
  }
493
491
 
494
- private File downloadBundleFile(@NonNull String baseUrl, @NonNull String href, @NonNull File destinationDirectory) throws Exception {
492
+ private File downloadBundleFile(
493
+ @NonNull String baseUrl,
494
+ @NonNull String href,
495
+ @NonNull File destinationDirectory,
496
+ @Nullable DownloadProgressCallback callback
497
+ ) throws Exception {
495
498
  HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
496
499
  urlBuilder.addQueryParameter("href", href);
497
500
  String url = urlBuilder.build().toString();
498
501
 
499
502
  File destinationFile = new File(destinationDirectory, href);
500
503
  destinationFile.getParentFile().mkdirs();
501
- downloadAndVerifyFile(url, destinationFile, null);
504
+ downloadAndVerifyFile(url, destinationFile, null, null, callback);
502
505
  return destinationFile;
503
506
  }
504
507
 
505
- private void downloadBundleFiles(@NonNull String baseUrl, @NonNull List<String> hrefs, @NonNull File destinationDirectory)
506
- throws Exception {
508
+ private void downloadBundleFiles(
509
+ @NonNull String baseUrl,
510
+ @NonNull List<ManifestItem> filesToDownload,
511
+ @NonNull File destinationDirectory,
512
+ @Nullable DownloadProgressCallback callback
513
+ ) throws Exception {
514
+ // Track the total downloaded bytes and total bytes to download for the progress callback
515
+ final long[] totalBytesDownloaded = { 0 };
516
+ long totalBytesToDownload = 0;
517
+ for (ManifestItem fileToDownload : filesToDownload) {
518
+ totalBytesToDownload += fileToDownload.getSizeInBytes();
519
+ }
507
520
  // Limit the number of threads to twice the number of processors
508
521
  int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
509
522
  List<Thread> threads = new ArrayList<>();
510
523
  AtomicReference<Exception> exceptionReference = new AtomicReference<>();
511
- for (String href : hrefs) {
524
+ for (ManifestItem fileToDownload : filesToDownload) {
512
525
  // Wait until a thread is available
513
526
  while (threads.size() >= maxThreads) {
514
527
  for (Thread thread : threads) {
@@ -524,13 +537,23 @@ public class LiveUpdate {
524
537
  if (exceptionReference.get() != null) {
525
538
  break;
526
539
  }
540
+ // Final variable that can be accessed from within inner class
541
+ long finalTotalBytesToDownload = totalBytesToDownload;
527
542
  // Start a new thread
528
543
  Thread thread = new Thread(() -> {
529
544
  try {
530
- downloadBundleFile(baseUrl, href, destinationDirectory);
545
+ downloadBundleFile(baseUrl, fileToDownload.getHref(), destinationDirectory, (downloadedBytes, totalBytes) -> {
546
+ if (callback != null) {
547
+ callback.onProgress(totalBytesDownloaded[0] + downloadedBytes, finalTotalBytesToDownload);
548
+ }
549
+ });
550
+ if (callback != null) {
551
+ totalBytesDownloaded[0] += fileToDownload.getSizeInBytes();
552
+ callback.onProgress(totalBytesDownloaded[0], finalTotalBytesToDownload);
553
+ }
531
554
  } catch (Exception exception) {
532
555
  exceptionReference.set(exception);
533
- Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + href, exception);
556
+ Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + fileToDownload.getHref(), exception);
534
557
  }
535
558
  });
536
559
  threads.add(thread);
@@ -545,13 +568,17 @@ public class LiveUpdate {
545
568
  if (exception != null) {
546
569
  throw exception;
547
570
  }
571
+ // Call the callback one last time to make sure the progress is at 100%
572
+ if (callback != null) {
573
+ callback.onProgress(totalBytesToDownload, totalBytesToDownload);
574
+ }
548
575
  }
549
576
 
550
577
  private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
551
578
  // Create a temporary directory
552
579
  File temporaryDirectory = createTemporaryDirectory();
553
580
  // Download the latest manifest
554
- File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory);
581
+ File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory, null);
555
582
  Manifest latestManifest = loadManifest(latestManifestFile);
556
583
  // Load the current manifest
557
584
  Manifest currentManifest = loadCurrentManifest();
@@ -565,26 +592,28 @@ public class LiveUpdate {
565
592
  itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
566
593
  }
567
594
  // Copy the files
568
- List<String> hrefsToCopy = new ArrayList<>();
569
- for (ManifestItem item : itemsToCopy) {
570
- hrefsToCopy.add(item.getHref());
571
- }
572
- copyCurrentBundleFiles(hrefsToCopy, temporaryDirectory);
595
+ copyCurrentBundleFiles(itemsToCopy, temporaryDirectory);
573
596
  // Download the files
574
- List<String> hrefsToDownload = new ArrayList<>();
575
- for (ManifestItem item : itemsToDownload) {
576
- hrefsToDownload.add(item.getHref());
577
- }
578
- downloadBundleFiles(downloadUrl, hrefsToDownload, temporaryDirectory);
597
+ downloadBundleFiles(downloadUrl, itemsToDownload, temporaryDirectory, (downloadedBytes, totalBytes) -> {
598
+ DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
599
+ notifyDownloadBundleProgressListeners(event);
600
+ });
579
601
  // Add the bundle
580
602
  addBundleOfTypeManifest(bundleId, temporaryDirectory);
581
603
  }
582
604
 
583
- private void downloadBundleOfTypeZip(@NonNull String bundleId, @NonNull String downloadUrl, @Nullable String checksum)
584
- throws Exception {
605
+ private void downloadBundleOfTypeZip(
606
+ @NonNull String bundleId,
607
+ @Nullable String checksum,
608
+ @Nullable String signature,
609
+ @NonNull String downloadUrl
610
+ ) throws Exception {
585
611
  File file = buildTemporaryZipFile();
586
612
  // Download the bundle
587
- downloadAndVerifyFile(downloadUrl, file, checksum);
613
+ downloadAndVerifyFile(downloadUrl, file, checksum, signature, (downloadedBytes, totalBytes) -> {
614
+ DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
615
+ notifyDownloadBundleProgressListeners(event);
616
+ });
588
617
  // Add the bundle
589
618
  addBundleOfTypeZip(bundleId, file);
590
619
  // Delete the temporary file
@@ -596,7 +625,7 @@ public class LiveUpdate {
596
625
  String channel = options.getChannel() == null ? getChannel() : options.getChannel();
597
626
  String url = new HttpUrl.Builder()
598
627
  .scheme("https")
599
- .host(host)
628
+ .host(config.getServerDomain())
600
629
  .addPathSegment("v1")
601
630
  .addPathSegment("apps")
602
631
  .addPathSegment(config.getAppId())
@@ -678,12 +707,13 @@ public class LiveUpdate {
678
707
  }
679
708
 
680
709
  /**
681
- * @return The current bundle ID (`public` for the built-in bundle).
710
+ * @return The current bundle ID or `null` if the default bundle is in use.
682
711
  */
712
+ @Nullable
683
713
  private String getCurrentBundleId() {
684
714
  String currentPath = getCurrentCapacitorServerPath();
685
715
  if (currentPath.equals(defaultWebAssetDir)) {
686
- return defaultWebAssetDir;
716
+ return null;
687
717
  }
688
718
  return new File(currentPath).getName();
689
719
  }
@@ -706,13 +736,13 @@ public class LiveUpdate {
706
736
  }
707
737
 
708
738
  /**
709
- * @return The next bundle ID (`public` for the built-in bundle).
739
+ * @return The next bundle ID or `null` if the default bundle will be used.
710
740
  */
711
- @NonNull
741
+ @Nullable
712
742
  private String getNextBundleId() {
713
743
  String nextPath = getNextCapacitorServerPath();
714
744
  if (nextPath.equals(defaultWebAssetDir)) {
715
- return defaultWebAssetDir;
745
+ return null;
716
746
  }
717
747
  return new File(nextPath).getName();
718
748
  }
@@ -728,6 +758,14 @@ public class LiveUpdate {
728
758
  .getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
729
759
  }
730
760
 
761
+ /**
762
+ * @return The previous bundle ID or `null` if the default bundle was used.
763
+ */
764
+ @Nullable
765
+ private String getPreviousBundleId() {
766
+ return preferences.getPreviousBundleId();
767
+ }
768
+
731
769
  private String getVersionCode() throws PackageManager.NameNotFoundException {
732
770
  return String.valueOf(getPackageInfo().versionCode);
733
771
  }
@@ -736,17 +774,23 @@ public class LiveUpdate {
736
774
  return getPackageInfo().versionName;
737
775
  }
738
776
 
739
- private boolean hasBundle(@NonNull String bundleId) {
777
+ private boolean hasBundleById(@NonNull String bundleId) {
740
778
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
741
779
  return bundleDirectory.exists();
742
780
  }
743
781
 
782
+ private boolean isBundleInUse(@NonNull String bundleId) {
783
+ String currentBundleId = getCurrentBundleId();
784
+ String nextBundleId = getNextBundleId();
785
+ return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
786
+ }
787
+
744
788
  @Nullable
745
789
  private Manifest loadCurrentManifest() throws Exception {
746
790
  String currentBundleId = getCurrentBundleId();
747
- if (currentBundleId.equals(defaultWebAssetDir)) {
791
+ if (currentBundleId == null) {
748
792
  AssetManager assets = plugin.getContext().getAssets();
749
- Boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
793
+ boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
750
794
  if (manifestFileExists) {
751
795
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
752
796
  BufferedSource source = Okio.buffer(Okio.source(inputStream));
@@ -776,25 +820,25 @@ public class LiveUpdate {
776
820
  return loadManifest(source);
777
821
  }
778
822
 
779
- private boolean isBundleInUse(@NonNull String bundleId) {
780
- String currentBundleId = getCurrentBundleId();
781
- String nextBundleId = getNextBundleId();
782
- return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
823
+ public void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
824
+ plugin.notifyDownloadBundleProgressListeners(event);
783
825
  }
784
826
 
785
827
  private void rollback() {
786
- if (getCurrentBundleId() == defaultWebAssetDir) {
828
+ // Set the rollback flag
829
+ rollbackPerformed = true;
830
+ // Set the new previous bundle ID
831
+ String currentBundleId = getCurrentBundleId();
832
+ setPreviousBundleId(currentBundleId);
833
+ // Log the rollback result
834
+ if (currentBundleId == null) {
787
835
  Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Default bundle is already in use.");
788
- return;
836
+ } else {
837
+ Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Rolling back to default bundle.");
838
+ // Rollback to the default bundle
839
+ setNextBundleById(null);
840
+ setCurrentBundleById(null);
789
841
  }
790
- Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Rolling back to default bundle.");
791
- setNextCapacitorServerPathToDefaultWebAssetDir();
792
- setCurrentCapacitorServerPathToDefaultWebAssetDir();
793
- }
794
-
795
- private void saveCurrentVersionCode() throws PackageManager.NameNotFoundException {
796
- int currentVersionCode = getPackageInfo().versionCode;
797
- preferences.setLastVersionCode(currentVersionCode);
798
842
  }
799
843
 
800
844
  @Nullable
@@ -822,6 +866,18 @@ public class LiveUpdate {
822
866
  return null;
823
867
  }
824
868
 
869
+ /**
870
+ * @param bundleId The bundle ID to set as the current bundle. If `null`, the default bundle will be used.
871
+ */
872
+ private void setCurrentBundleById(@Nullable String bundleId) {
873
+ if (bundleId == null) {
874
+ setCurrentCapacitorServerPath(defaultWebAssetDir);
875
+ } else {
876
+ File bundleDirectory = buildBundleDirectoryFor(bundleId);
877
+ setCurrentCapacitorServerPath(bundleDirectory.getPath());
878
+ }
879
+ }
880
+
825
881
  private void setCurrentCapacitorServerPath(@NonNull String path) {
826
882
  if (path.equals(defaultWebAssetDir)) {
827
883
  this.plugin.getBridge().setServerAssetPath(path);
@@ -831,13 +887,16 @@ public class LiveUpdate {
831
887
  this.plugin.getBridge().reload();
832
888
  }
833
889
 
834
- private void setCurrentCapacitorServerPathToDefaultWebAssetDir() {
835
- setCurrentCapacitorServerPath(defaultWebAssetDir);
836
- }
837
-
838
- private void setNextBundle(@NonNull String bundleId) {
839
- File bundleDirectory = buildBundleDirectoryFor(bundleId);
840
- setNextCapacitorServerPath(bundleDirectory.getPath());
890
+ /**
891
+ * @param bundleId The bundle ID to set as the next bundle. If `null`, the default bundle will be used.
892
+ */
893
+ private void setNextBundleById(@Nullable String bundleId) {
894
+ if (bundleId == null) {
895
+ setNextCapacitorServerPath(defaultWebAssetDir);
896
+ } else {
897
+ File bundleDirectory = buildBundleDirectoryFor(bundleId);
898
+ setNextCapacitorServerPath(bundleDirectory.getPath());
899
+ }
841
900
  }
842
901
 
843
902
  private void setNextCapacitorServerPath(@NonNull String path) {
@@ -845,11 +904,17 @@ public class LiveUpdate {
845
904
  this.webViewSettingsEditor.commit();
846
905
  }
847
906
 
848
- private void setNextCapacitorServerPathToDefaultWebAssetDir() {
849
- setNextCapacitorServerPath(defaultWebAssetDir);
907
+ /**
908
+ * @param bundleId The bundle ID to save as the previous bundle. If `null`, the value will be removed.
909
+ */
910
+ private void setPreviousBundleId(@Nullable String bundleId) {
911
+ preferences.setPreviousBundleId(bundleId);
850
912
  }
851
913
 
852
914
  private void startRollbackTimer() {
915
+ if (config.getReadyTimeout() <= 0) {
916
+ return;
917
+ }
853
918
  stopRollbackTimer();
854
919
  rollbackHandler.postDelayed(() -> rollback(), config.getReadyTimeout());
855
920
  }
@@ -916,12 +981,6 @@ public class LiveUpdate {
916
981
  }
917
982
  }
918
983
 
919
- private boolean wasUpdated() throws PackageManager.NameNotFoundException {
920
- int lastVersionCode = preferences.getLastVersionCode();
921
- int currentVersionCode = getPackageInfo().versionCode;
922
- return lastVersionCode != currentVersionCode;
923
- }
924
-
925
984
  private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
926
985
  String packageName = this.plugin.getContext().getPackageName();
927
986
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {