@capawesome/capacitor-live-update 6.7.2 → 7.0.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 +91 -81
  4. package/android/build.gradle +9 -9
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +165 -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 +50 -78
  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 +9 -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 +1 -10
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +13 -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 +191 -158
  17. package/dist/esm/definitions.d.ts +107 -52
  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 +1 -7
  29. package/ios/Plugin/Classes/Results/{FetchLatestBundle.swift → FetchLatestBundleResult.swift} +6 -1
  30. package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
  31. package/ios/Plugin/LiveUpdate.swift +167 -154
  32. package/ios/Plugin/LiveUpdateConfig.swift +2 -4
  33. package/ios/Plugin/LiveUpdateHttpClient.swift +6 -2
  34. package/ios/Plugin/LiveUpdatePlugin.swift +44 -38
  35. package/ios/Plugin/LiveUpdatePreferences.swift +6 -20
  36. package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +1 -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
  }
@@ -129,11 +118,10 @@ public class LiveUpdate {
129
118
  public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) throws Exception {
130
119
  ArtifactType artifactType = options.getArtifactType();
131
120
  String bundleId = options.getBundleId();
132
- String checksum = options.getChecksum();
133
121
  String url = options.getUrl();
134
122
 
135
123
  // Check if the bundle already exists
136
- if (hasBundle(bundleId)) {
124
+ if (hasBundleById(bundleId)) {
137
125
  Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_EXISTS);
138
126
  callback.error(exception);
139
127
  return;
@@ -143,7 +131,7 @@ public class LiveUpdate {
143
131
  if (artifactType == ArtifactType.MANIFEST) {
144
132
  downloadBundleOfTypeManifest(bundleId, url);
145
133
  } else {
146
- downloadBundleOfTypeZip(bundleId, url, checksum);
134
+ downloadBundleOfTypeZip(bundleId, url);
147
135
  }
148
136
  callback.success();
149
137
  }
@@ -152,17 +140,9 @@ public class LiveUpdate {
152
140
  GetLatestBundleResponse response = fetchLatestBundle(options);
153
141
  ArtifactType artifactType = response == null ? null : response.getArtifactType();
154
142
  String bundleId = response == null ? null : response.getBundleId();
143
+ JSONObject customProperties = response == null ? null : response.getCustomProperties();
155
144
  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);
145
+ FetchLatestBundleResult result = new FetchLatestBundleResult(artifactType, bundleId, customProperties, downloadUrl);
166
146
  callback.success(result);
167
147
  }
168
148
 
@@ -180,9 +160,6 @@ public class LiveUpdate {
180
160
 
181
161
  public void getCurrentBundle(@NonNull NonEmptyCallback<GetCurrentBundleResult> callback) {
182
162
  String bundleId = getCurrentBundleId();
183
- if (bundleId.equals(defaultWebAssetDir)) {
184
- bundleId = null;
185
- }
186
163
  GetCurrentBundleResult result = new GetCurrentBundleResult(bundleId);
187
164
  callback.success(result);
188
165
  }
@@ -201,9 +178,6 @@ public class LiveUpdate {
201
178
 
202
179
  public void getNextBundle(@NonNull NonEmptyCallback<GetNextBundleResult> callback) {
203
180
  String bundleId = getNextBundleId();
204
- if (bundleId.equals(defaultWebAssetDir)) {
205
- bundleId = null;
206
- }
207
181
  GetNextBundleResult result = new GetNextBundleResult(bundleId);
208
182
  callback.success(result);
209
183
  }
@@ -220,14 +194,27 @@ public class LiveUpdate {
220
194
  callback.success(result);
221
195
  }
222
196
 
223
- public void ready() {
197
+ public void ready(@NonNull NonEmptyCallback callback) {
224
198
  Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
199
+ if (config.getReadyTimeout() <= 0) {
200
+ Logger.warn(LiveUpdatePlugin.TAG, "Ready timeout is set to 0. Automatic rollback is disabled.");
201
+ }
225
202
  // Stop the rollback timer
226
203
  stopRollbackTimer();
227
204
  // Delete unused bundles
228
205
  if (config.getAutoDeleteBundles()) {
229
206
  deleteUnusedBundles();
230
207
  }
208
+ // Get the current and previous bundle IDs
209
+ String currentBundleId = getCurrentBundleId();
210
+ String previousBundleId = getPreviousBundleId();
211
+ // Return the result
212
+ ReadyResult result = new ReadyResult(currentBundleId, previousBundleId, rollbackPerformed);
213
+ callback.success(result);
214
+ // Set the new previous bundle ID
215
+ setPreviousBundleId(currentBundleId);
216
+ // Reset the rollback flag
217
+ rollbackPerformed = false;
231
218
  }
232
219
 
233
220
  public void reload() {
@@ -237,20 +224,7 @@ public class LiveUpdate {
237
224
  }
238
225
 
239
226
  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();
227
+ setNextBundleById(null);
254
228
  }
255
229
 
256
230
  public void setChannel(@NonNull SetChannelOptions options, @NonNull EmptyCallback callback) {
@@ -273,8 +247,8 @@ public class LiveUpdate {
273
247
  if (bundleId == null) {
274
248
  reset();
275
249
  } else {
276
- if (hasBundle(bundleId)) {
277
- setNextBundle(bundleId);
250
+ if (hasBundleById(bundleId)) {
251
+ setNextBundleById(bundleId);
278
252
  } else {
279
253
  Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
280
254
  callback.error(exception);
@@ -299,12 +273,12 @@ public class LiveUpdate {
299
273
  String latestBundleId = response.getBundleId();
300
274
  String url = response.getUrl();
301
275
  // Check if the bundle already exists
302
- if (hasBundle(latestBundleId)) {
276
+ if (hasBundleById(latestBundleId)) {
303
277
  String nextBundleId = null;
304
278
  String currentBundleId = getCurrentBundleId();
305
279
  if (!latestBundleId.equals(currentBundleId)) {
306
280
  // Set the next bundle
307
- setNextBundle(latestBundleId);
281
+ setNextBundleById(latestBundleId);
308
282
  nextBundleId = latestBundleId;
309
283
  }
310
284
  SyncResult syncResult = new SyncResult(nextBundleId);
@@ -315,10 +289,10 @@ public class LiveUpdate {
315
289
  if (artifactType == ArtifactType.MANIFEST) {
316
290
  downloadBundleOfTypeManifest(latestBundleId, url);
317
291
  } else {
318
- downloadBundleOfTypeZip(latestBundleId, url, null);
292
+ downloadBundleOfTypeZip(latestBundleId, url);
319
293
  }
320
294
  // Set the next bundle
321
- setNextBundle(latestBundleId);
295
+ setNextBundleById(latestBundleId);
322
296
  SyncResult syncResult = new SyncResult(latestBundleId);
323
297
  callback.success(syncResult);
324
298
  }
@@ -367,9 +341,10 @@ public class LiveUpdate {
367
341
  return new File(plugin.getContext().getCacheDir(), fileName);
368
342
  }
369
343
 
370
- private void copyCurrentBundleFile(@NonNull String href, @NonNull File destinationDirectory) throws IOException {
344
+ private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
345
+ String href = fileToCopy.getHref();
371
346
  String currentBundleId = getCurrentBundleId();
372
- if (currentBundleId.equals(defaultWebAssetDir)) {
347
+ if (currentBundleId == null) {
373
348
  // Create the source input stream
374
349
  AssetManager assets = plugin.getContext().getAssets();
375
350
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
@@ -391,9 +366,9 @@ public class LiveUpdate {
391
366
  }
392
367
  }
393
368
 
394
- private void copyCurrentBundleFiles(@NonNull List<String> hrefs, @NonNull File destinationDirectory) throws IOException {
395
- for (String href : hrefs) {
396
- copyCurrentBundleFile(href, destinationDirectory);
369
+ private void copyCurrentBundleFiles(@NonNull List<ManifestItem> filesToCopy, @NonNull File destinationDirectory) throws IOException {
370
+ for (ManifestItem fileToCopy : filesToCopy) {
371
+ copyCurrentBundleFile(fileToCopy, destinationDirectory);
397
372
  }
398
373
  }
399
374
 
@@ -446,14 +421,14 @@ public class LiveUpdate {
446
421
  return file;
447
422
  }
448
423
 
449
- private void deleteBundle(@NonNull String bundleId) {
424
+ private void deleteBundleById(@NonNull String bundleId) {
450
425
  // Delete the bundle directory
451
426
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
452
427
  deleteFileRecursively(bundleDirectory);
453
428
  // Reset the next bundle if it is the deleted bundle
454
429
  String nextBundleId = getNextBundleId();
455
430
  if (bundleId.equals(nextBundleId)) {
456
- setNextCapacitorServerPathToDefaultWebAssetDir();
431
+ setNextBundleById(null);
457
432
  }
458
433
  }
459
434
 
@@ -470,18 +445,19 @@ public class LiveUpdate {
470
445
  String[] bundleIds = getBundleIds();
471
446
  for (String bundleId : bundleIds) {
472
447
  if (!isBundleInUse(bundleId)) {
473
- deleteBundle(bundleId);
448
+ deleteBundleById(bundleId);
474
449
  }
475
450
  }
476
451
  }
477
452
 
478
- private void downloadAndVerifyFile(@NonNull String url, @NonNull File file, @Nullable String checksum) throws Exception {
453
+ private void downloadAndVerifyFile(@NonNull String url, @NonNull File file, @Nullable DownloadProgressCallback callback)
454
+ throws Exception {
479
455
  Response response = httpClient.execute(url);
480
456
  ResponseBody responseBody = response.body();
481
457
  if (response.isSuccessful()) {
482
- LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file);
458
+ LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file, callback);
483
459
  // Verify the file
484
- checksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
460
+ String checksum = LiveUpdateHttpClient.getChecksumFromResponse(response);
485
461
  String signature = LiveUpdateHttpClient.getSignatureFromResponse(response);
486
462
  verifyFile(file, checksum, signature);
487
463
  } else {
@@ -491,24 +467,39 @@ public class LiveUpdate {
491
467
  }
492
468
  }
493
469
 
494
- private File downloadBundleFile(@NonNull String baseUrl, @NonNull String href, @NonNull File destinationDirectory) throws Exception {
470
+ private File downloadBundleFile(
471
+ @NonNull String baseUrl,
472
+ @NonNull String href,
473
+ @NonNull File destinationDirectory,
474
+ @Nullable DownloadProgressCallback callback
475
+ ) throws Exception {
495
476
  HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
496
477
  urlBuilder.addQueryParameter("href", href);
497
478
  String url = urlBuilder.build().toString();
498
479
 
499
480
  File destinationFile = new File(destinationDirectory, href);
500
481
  destinationFile.getParentFile().mkdirs();
501
- downloadAndVerifyFile(url, destinationFile, null);
482
+ downloadAndVerifyFile(url, destinationFile, callback);
502
483
  return destinationFile;
503
484
  }
504
485
 
505
- private void downloadBundleFiles(@NonNull String baseUrl, @NonNull List<String> hrefs, @NonNull File destinationDirectory)
506
- throws Exception {
486
+ private void downloadBundleFiles(
487
+ @NonNull String baseUrl,
488
+ @NonNull List<ManifestItem> filesToDownload,
489
+ @NonNull File destinationDirectory,
490
+ @Nullable DownloadProgressCallback callback
491
+ ) throws Exception {
492
+ // Track the total downloaded bytes and total bytes to download for the progress callback
493
+ final long[] totalBytesDownloaded = { 0 };
494
+ long totalBytesToDownload = 0;
495
+ for (ManifestItem fileToDownload : filesToDownload) {
496
+ totalBytesToDownload += fileToDownload.getSizeInBytes();
497
+ }
507
498
  // Limit the number of threads to twice the number of processors
508
499
  int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
509
500
  List<Thread> threads = new ArrayList<>();
510
501
  AtomicReference<Exception> exceptionReference = new AtomicReference<>();
511
- for (String href : hrefs) {
502
+ for (ManifestItem fileToDownload : filesToDownload) {
512
503
  // Wait until a thread is available
513
504
  while (threads.size() >= maxThreads) {
514
505
  for (Thread thread : threads) {
@@ -524,13 +515,23 @@ public class LiveUpdate {
524
515
  if (exceptionReference.get() != null) {
525
516
  break;
526
517
  }
518
+ // Final variable that can be accessed from within inner class
519
+ long finalTotalBytesToDownload = totalBytesToDownload;
527
520
  // Start a new thread
528
521
  Thread thread = new Thread(() -> {
529
522
  try {
530
- downloadBundleFile(baseUrl, href, destinationDirectory);
523
+ downloadBundleFile(baseUrl, fileToDownload.getHref(), destinationDirectory, (downloadedBytes, totalBytes) -> {
524
+ if (callback != null) {
525
+ callback.onProgress(totalBytesDownloaded[0] + downloadedBytes, finalTotalBytesToDownload);
526
+ }
527
+ });
528
+ if (callback != null) {
529
+ totalBytesDownloaded[0] += fileToDownload.getSizeInBytes();
530
+ callback.onProgress(totalBytesDownloaded[0], finalTotalBytesToDownload);
531
+ }
531
532
  } catch (Exception exception) {
532
533
  exceptionReference.set(exception);
533
- Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + href, exception);
534
+ Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + fileToDownload.getHref(), exception);
534
535
  }
535
536
  });
536
537
  threads.add(thread);
@@ -545,13 +546,17 @@ public class LiveUpdate {
545
546
  if (exception != null) {
546
547
  throw exception;
547
548
  }
549
+ // Call the callback one last time to make sure the progress is at 100%
550
+ if (callback != null) {
551
+ callback.onProgress(totalBytesToDownload, totalBytesToDownload);
552
+ }
548
553
  }
549
554
 
550
555
  private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
551
556
  // Create a temporary directory
552
557
  File temporaryDirectory = createTemporaryDirectory();
553
558
  // Download the latest manifest
554
- File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory);
559
+ File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory, null);
555
560
  Manifest latestManifest = loadManifest(latestManifestFile);
556
561
  // Load the current manifest
557
562
  Manifest currentManifest = loadCurrentManifest();
@@ -565,26 +570,23 @@ public class LiveUpdate {
565
570
  itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
566
571
  }
567
572
  // Copy the files
568
- List<String> hrefsToCopy = new ArrayList<>();
569
- for (ManifestItem item : itemsToCopy) {
570
- hrefsToCopy.add(item.getHref());
571
- }
572
- copyCurrentBundleFiles(hrefsToCopy, temporaryDirectory);
573
+ copyCurrentBundleFiles(itemsToCopy, temporaryDirectory);
573
574
  // 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);
575
+ downloadBundleFiles(downloadUrl, itemsToDownload, temporaryDirectory, (downloadedBytes, totalBytes) -> {
576
+ DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
577
+ notifyDownloadBundleProgressListeners(event);
578
+ });
579
579
  // Add the bundle
580
580
  addBundleOfTypeManifest(bundleId, temporaryDirectory);
581
581
  }
582
582
 
583
- private void downloadBundleOfTypeZip(@NonNull String bundleId, @NonNull String downloadUrl, @Nullable String checksum)
584
- throws Exception {
583
+ private void downloadBundleOfTypeZip(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
585
584
  File file = buildTemporaryZipFile();
586
585
  // Download the bundle
587
- downloadAndVerifyFile(downloadUrl, file, checksum);
586
+ downloadAndVerifyFile(downloadUrl, file, (downloadedBytes, totalBytes) -> {
587
+ DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
588
+ notifyDownloadBundleProgressListeners(event);
589
+ });
588
590
  // Add the bundle
589
591
  addBundleOfTypeZip(bundleId, file);
590
592
  // Delete the temporary file
@@ -596,7 +598,7 @@ public class LiveUpdate {
596
598
  String channel = options.getChannel() == null ? getChannel() : options.getChannel();
597
599
  String url = new HttpUrl.Builder()
598
600
  .scheme("https")
599
- .host(host)
601
+ .host(config.getServerDomain())
600
602
  .addPathSegment("v1")
601
603
  .addPathSegment("apps")
602
604
  .addPathSegment(config.getAppId())
@@ -678,12 +680,13 @@ public class LiveUpdate {
678
680
  }
679
681
 
680
682
  /**
681
- * @return The current bundle ID (`public` for the built-in bundle).
683
+ * @return The current bundle ID or `null` if the default bundle is in use.
682
684
  */
685
+ @Nullable
683
686
  private String getCurrentBundleId() {
684
687
  String currentPath = getCurrentCapacitorServerPath();
685
688
  if (currentPath.equals(defaultWebAssetDir)) {
686
- return defaultWebAssetDir;
689
+ return null;
687
690
  }
688
691
  return new File(currentPath).getName();
689
692
  }
@@ -706,13 +709,13 @@ public class LiveUpdate {
706
709
  }
707
710
 
708
711
  /**
709
- * @return The next bundle ID (`public` for the built-in bundle).
712
+ * @return The next bundle ID or `null` if the default bundle will be used.
710
713
  */
711
- @NonNull
714
+ @Nullable
712
715
  private String getNextBundleId() {
713
716
  String nextPath = getNextCapacitorServerPath();
714
717
  if (nextPath.equals(defaultWebAssetDir)) {
715
- return defaultWebAssetDir;
718
+ return null;
716
719
  }
717
720
  return new File(nextPath).getName();
718
721
  }
@@ -728,6 +731,14 @@ public class LiveUpdate {
728
731
  .getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
729
732
  }
730
733
 
734
+ /**
735
+ * @return The previous bundle ID or `null` if the default bundle was used.
736
+ */
737
+ @Nullable
738
+ private String getPreviousBundleId() {
739
+ return preferences.getPreviousBundleId();
740
+ }
741
+
731
742
  private String getVersionCode() throws PackageManager.NameNotFoundException {
732
743
  return String.valueOf(getPackageInfo().versionCode);
733
744
  }
@@ -736,17 +747,23 @@ public class LiveUpdate {
736
747
  return getPackageInfo().versionName;
737
748
  }
738
749
 
739
- private boolean hasBundle(@NonNull String bundleId) {
750
+ private boolean hasBundleById(@NonNull String bundleId) {
740
751
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
741
752
  return bundleDirectory.exists();
742
753
  }
743
754
 
755
+ private boolean isBundleInUse(@NonNull String bundleId) {
756
+ String currentBundleId = getCurrentBundleId();
757
+ String nextBundleId = getNextBundleId();
758
+ return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
759
+ }
760
+
744
761
  @Nullable
745
762
  private Manifest loadCurrentManifest() throws Exception {
746
763
  String currentBundleId = getCurrentBundleId();
747
- if (currentBundleId.equals(defaultWebAssetDir)) {
764
+ if (currentBundleId == null) {
748
765
  AssetManager assets = plugin.getContext().getAssets();
749
- Boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
766
+ boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
750
767
  if (manifestFileExists) {
751
768
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
752
769
  BufferedSource source = Okio.buffer(Okio.source(inputStream));
@@ -776,25 +793,25 @@ public class LiveUpdate {
776
793
  return loadManifest(source);
777
794
  }
778
795
 
779
- private boolean isBundleInUse(@NonNull String bundleId) {
780
- String currentBundleId = getCurrentBundleId();
781
- String nextBundleId = getNextBundleId();
782
- return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
796
+ public void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
797
+ plugin.notifyDownloadBundleProgressListeners(event);
783
798
  }
784
799
 
785
800
  private void rollback() {
786
- if (getCurrentBundleId() == defaultWebAssetDir) {
801
+ // Set the rollback flag
802
+ rollbackPerformed = true;
803
+ // Set the new previous bundle ID
804
+ String currentBundleId = getCurrentBundleId();
805
+ setPreviousBundleId(currentBundleId);
806
+ // Log the rollback result
807
+ if (currentBundleId == null) {
787
808
  Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Default bundle is already in use.");
788
- return;
809
+ } else {
810
+ Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Rolling back to default bundle.");
811
+ // Rollback to the default bundle
812
+ setNextBundleById(null);
813
+ setCurrentBundleById(null);
789
814
  }
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
815
  }
799
816
 
800
817
  @Nullable
@@ -822,6 +839,18 @@ public class LiveUpdate {
822
839
  return null;
823
840
  }
824
841
 
842
+ /**
843
+ * @param bundleId The bundle ID to set as the current bundle. If `null`, the default bundle will be used.
844
+ */
845
+ private void setCurrentBundleById(@Nullable String bundleId) {
846
+ if (bundleId == null) {
847
+ setCurrentCapacitorServerPath(defaultWebAssetDir);
848
+ } else {
849
+ File bundleDirectory = buildBundleDirectoryFor(bundleId);
850
+ setCurrentCapacitorServerPath(bundleDirectory.getPath());
851
+ }
852
+ }
853
+
825
854
  private void setCurrentCapacitorServerPath(@NonNull String path) {
826
855
  if (path.equals(defaultWebAssetDir)) {
827
856
  this.plugin.getBridge().setServerAssetPath(path);
@@ -831,13 +860,16 @@ public class LiveUpdate {
831
860
  this.plugin.getBridge().reload();
832
861
  }
833
862
 
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());
863
+ /**
864
+ * @param bundleId The bundle ID to set as the next bundle. If `null`, the default bundle will be used.
865
+ */
866
+ private void setNextBundleById(@Nullable String bundleId) {
867
+ if (bundleId == null) {
868
+ setNextCapacitorServerPath(defaultWebAssetDir);
869
+ } else {
870
+ File bundleDirectory = buildBundleDirectoryFor(bundleId);
871
+ setNextCapacitorServerPath(bundleDirectory.getPath());
872
+ }
841
873
  }
842
874
 
843
875
  private void setNextCapacitorServerPath(@NonNull String path) {
@@ -845,11 +877,17 @@ public class LiveUpdate {
845
877
  this.webViewSettingsEditor.commit();
846
878
  }
847
879
 
848
- private void setNextCapacitorServerPathToDefaultWebAssetDir() {
849
- setNextCapacitorServerPath(defaultWebAssetDir);
880
+ /**
881
+ * @param bundleId The bundle ID to save as the previous bundle. If `null`, the value will be removed.
882
+ */
883
+ private void setPreviousBundleId(@Nullable String bundleId) {
884
+ preferences.setPreviousBundleId(bundleId);
850
885
  }
851
886
 
852
887
  private void startRollbackTimer() {
888
+ if (config.getReadyTimeout() <= 0) {
889
+ return;
890
+ }
853
891
  stopRollbackTimer();
854
892
  rollbackHandler.postDelayed(() -> rollback(), config.getReadyTimeout());
855
893
  }
@@ -916,12 +954,6 @@ public class LiveUpdate {
916
954
  }
917
955
  }
918
956
 
919
- private boolean wasUpdated() throws PackageManager.NameNotFoundException {
920
- int lastVersionCode = preferences.getLastVersionCode();
921
- int currentVersionCode = getPackageInfo().versionCode;
922
- return lastVersionCode != currentVersionCode;
923
- }
924
-
925
957
  private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
926
958
  String packageName = this.plugin.getContext().getPackageName();
927
959
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {