@capawesome/capacitor-live-update 7.2.2 → 7.4.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 (37) hide show
  1. package/README.md +284 -26
  2. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +623 -220
  3. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +23 -0
  4. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +33 -5
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +185 -15
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +42 -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/events/NextBundleSetEvent.java +26 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetConfigOptions.java +19 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SyncOptions.java +4 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBlockedBundlesResult.java +27 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetConfigResult.java +40 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetDownloadedBundlesResult.java +27 -0
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/IsSyncingResult.java +26 -0
  15. package/dist/docs.json +428 -1
  16. package/dist/esm/definitions.d.ts +230 -2
  17. package/dist/esm/definitions.js.map +1 -1
  18. package/dist/esm/web.d.ts +8 -1
  19. package/dist/esm/web.js +21 -0
  20. package/dist/esm/web.js.map +1 -1
  21. package/dist/plugin.cjs.js +21 -0
  22. package/dist/plugin.cjs.js.map +1 -1
  23. package/dist/plugin.js +21 -0
  24. package/dist/plugin.js.map +1 -1
  25. package/ios/Plugin/Classes/Events/NextBundleSetEvent.swift +16 -0
  26. package/ios/Plugin/Classes/Options/SetConfigOptions.swift +14 -0
  27. package/ios/Plugin/Classes/Options/SyncOptions.swift +4 -0
  28. package/ios/Plugin/Classes/Results/GetBlockedBundlesResult.swift +16 -0
  29. package/ios/Plugin/Classes/Results/GetConfigResult.swift +19 -0
  30. package/ios/Plugin/Classes/Results/GetDownloadedBundlesResult.swift +16 -0
  31. package/ios/Plugin/Classes/Results/IsSyncingResult.swift +16 -0
  32. package/ios/Plugin/LiveUpdate.swift +178 -4
  33. package/ios/Plugin/LiveUpdateConfig.swift +2 -0
  34. package/ios/Plugin/LiveUpdateHttpClient.swift +4 -2
  35. package/ios/Plugin/LiveUpdatePlugin.swift +106 -11
  36. package/ios/Plugin/LiveUpdatePreferences.swift +56 -0
  37. package/package.json +1 -1
@@ -17,22 +17,28 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.Manifest;
17
17
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.ManifestItem;
18
18
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
19
19
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.DownloadBundleProgressEvent;
20
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.NextBundleSetEvent;
20
21
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundleOptions;
21
22
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
22
23
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
23
24
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
25
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetConfigOptions;
24
26
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
25
27
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetNextBundleOptions;
26
28
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SyncOptions;
27
29
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
30
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBlockedBundlesResult;
28
31
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundlesResult;
29
32
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetChannelResult;
33
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetConfigResult;
30
34
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCurrentBundleResult;
31
35
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCustomIdResult;
32
36
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdResult;
37
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDownloadedBundlesResult;
33
38
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetNextBundleResult;
34
39
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
35
40
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
41
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingResult;
36
42
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.ReadyResult;
37
43
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
38
44
  import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
@@ -54,8 +60,12 @@ import java.util.ArrayList;
54
60
  import java.util.Arrays;
55
61
  import java.util.List;
56
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;
57
66
  import java.util.concurrent.atomic.AtomicReference;
58
67
  import net.lingala.zip4j.ZipFile;
68
+ import okhttp3.Call;
59
69
  import okhttp3.HttpUrl;
60
70
  import okhttp3.Response;
61
71
  import okhttp3.ResponseBody;
@@ -67,6 +77,8 @@ import org.json.JSONObject;
67
77
 
68
78
  public class LiveUpdate {
69
79
 
80
+ private final long autoUpdateIntervalMs = 15 * 60 * 1000; // 15 minutes
81
+
70
82
  @NonNull
71
83
  private final LiveUpdateConfig config;
72
84
 
@@ -88,7 +100,10 @@ public class LiveUpdate {
88
100
  private final Handler rollbackHandler = new Handler();
89
101
  private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
90
102
 
103
+ private boolean initialPageLoaded = false;
104
+ private long lastAutoUpdateCheckTimestamp = 0;
91
105
  private boolean rollbackPerformed = false;
106
+ private boolean syncInProgress = false;
92
107
 
93
108
  public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
94
109
  this.config = config;
@@ -97,11 +112,18 @@ public class LiveUpdate {
97
112
  this.preferences = new LiveUpdatePreferences(plugin.getContext());
98
113
  this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
99
114
 
115
+ // Check version and reset config if version changed
116
+ checkAndResetConfigIfVersionChanged();
117
+
100
118
  // Start the rollback timer to rollback to the default bundle
101
119
  // if the app is not ready after a certain time
102
120
  startRollbackTimer();
103
121
  }
104
122
 
123
+ public void clearBlockedBundles() {
124
+ preferences.setBlockedBundleIds(null);
125
+ }
126
+
105
127
  public void deleteBundle(@NonNull DeleteBundleOptions options, @NonNull EmptyCallback callback) {
106
128
  String bundleId = options.getBundleId();
107
129
 
@@ -115,7 +137,7 @@ public class LiveUpdate {
115
137
  callback.success();
116
138
  }
117
139
 
118
- public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) throws Exception {
140
+ public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) {
119
141
  ArtifactType artifactType = options.getArtifactType();
120
142
  String bundleId = options.getBundleId();
121
143
  String checksum = options.getChecksum();
@@ -131,44 +153,80 @@ public class LiveUpdate {
131
153
 
132
154
  // Download the bundle
133
155
  if (artifactType == ArtifactType.MANIFEST) {
134
- downloadBundleOfTypeManifest(bundleId, url);
156
+ downloadBundleOfTypeManifest(bundleId, url, callback);
135
157
  } else {
136
- downloadBundleOfTypeZip(bundleId, checksum, signature, url);
158
+ downloadBundleOfTypeZip(bundleId, checksum, signature, url, callback);
137
159
  }
138
- callback.success();
139
160
  }
140
161
 
141
- public void fetchLatestBundle(@NonNull FetchLatestBundleOptions options, @NonNull NonEmptyCallback callback) throws Exception {
142
- GetLatestBundleResponse response = fetchLatestBundle(options);
143
- ArtifactType artifactType = response == null ? null : response.getArtifactType();
144
- String bundleId = response == null ? null : response.getBundleId();
145
- String checksum = response == null ? null : response.getChecksum();
146
- JSONObject customProperties = response == null ? null : response.getCustomProperties();
147
- String downloadUrl = response == null ? null : response.getUrl();
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
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
+ }
156
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);
157
202
  callback.success(result);
158
203
  }
159
204
 
160
205
  public void getBundles(@NonNull NonEmptyCallback callback) {
161
- String[] bundleIds = getBundleIds();
206
+ String[] bundleIds = getDownloadedBundleIds();
162
207
  GetBundlesResult result = new GetBundlesResult(bundleIds);
163
208
  callback.success(result);
164
209
  }
165
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
+
166
217
  public void getChannel(@NonNull NonEmptyCallback callback) {
167
218
  String channel = getChannel();
168
219
  GetChannelResult result = new GetChannelResult(channel);
169
220
  callback.success(result);
170
221
  }
171
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
+
172
230
  public void getCurrentBundle(@NonNull NonEmptyCallback<GetCurrentBundleResult> callback) {
173
231
  String bundleId = getCurrentBundleId();
174
232
  GetCurrentBundleResult result = new GetCurrentBundleResult(bundleId);
@@ -194,7 +252,7 @@ public class LiveUpdate {
194
252
  }
195
253
 
196
254
  public void getVersionCode(@NonNull NonEmptyCallback callback) throws PackageManager.NameNotFoundException {
197
- String versionCode = getVersionCode();
255
+ String versionCode = getVersionCodeAsString();
198
256
  GetVersionCodeResult result = new GetVersionCodeResult(versionCode);
199
257
  callback.success(result);
200
258
  }
@@ -205,6 +263,26 @@ public class LiveUpdate {
205
263
  callback.success(result);
206
264
  }
207
265
 
266
+ public void isSyncing(@NonNull NonEmptyCallback<IsSyncingResult> callback) {
267
+ IsSyncingResult result = new IsSyncingResult(syncInProgress);
268
+ callback.success(result);
269
+ }
270
+
271
+ public void handleOnPageLoaded() {
272
+ // Wait for initial page load to perform auto update to make sure the WebViewLocalServer is initialized.
273
+ // Otherwise, a NullPointerException may occur for `com.getcapacitor.WebViewLocalServer.getBasePath()`.
274
+ if ("background".equals(config.getAutoUpdateStrategy()) && !initialPageLoaded) {
275
+ initialPageLoaded = true;
276
+ performAutoUpdate();
277
+ }
278
+ }
279
+
280
+ public void handleOnResume() {
281
+ if ("background".equals(config.getAutoUpdateStrategy()) && initialPageLoaded) {
282
+ performAutoUpdate();
283
+ }
284
+ }
285
+
208
286
  public void ready(@NonNull NonEmptyCallback callback) {
209
287
  Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
210
288
  if (config.getReadyTimeout() <= 0) {
@@ -219,6 +297,10 @@ public class LiveUpdate {
219
297
  // Get the current and previous bundle IDs
220
298
  String currentBundleId = getCurrentBundleId();
221
299
  String previousBundleId = getPreviousBundleId();
300
+ // Block the rolled back bundle if enabled
301
+ if (config.getAutoBlockRolledBackBundles() && rollbackPerformed && previousBundleId != null) {
302
+ addBlockedBundleId(previousBundleId);
303
+ }
222
304
  // Return the result
223
305
  ReadyResult result = new ReadyResult(currentBundleId, previousBundleId, rollbackPerformed);
224
306
  callback.success(result);
@@ -238,6 +320,10 @@ public class LiveUpdate {
238
320
  setNextBundleById(null);
239
321
  }
240
322
 
323
+ public void resetConfig() {
324
+ preferences.setAppId(null);
325
+ }
326
+
241
327
  public void setChannel(@NonNull SetChannelOptions options, @NonNull EmptyCallback callback) {
242
328
  String channel = options.getChannel();
243
329
 
@@ -245,6 +331,11 @@ public class LiveUpdate {
245
331
  callback.success();
246
332
  }
247
333
 
334
+ public void setConfig(@NonNull SetConfigOptions options) {
335
+ String appId = options.getAppId();
336
+ preferences.setAppId(appId);
337
+ }
338
+
248
339
  public void setCustomId(@NonNull SetCustomIdOptions options, @NonNull EmptyCallback callback) {
249
340
  String customId = options.getCustomId();
250
341
 
@@ -269,45 +360,99 @@ public class LiveUpdate {
269
360
  callback.success();
270
361
  }
271
362
 
272
- public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback) throws Exception {
363
+ public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback) {
364
+ if (syncInProgress) {
365
+ Exception exception = new Exception(LiveUpdatePlugin.ERROR_SYNC_IN_PROGRESS);
366
+ callback.error(exception);
367
+ return;
368
+ }
369
+ syncInProgress = true;
370
+
273
371
  String channel = options.getChannel();
274
372
  // Fetch the latest bundle
275
373
  FetchLatestBundleOptions fetchLatestBundleOptions = new FetchLatestBundleOptions(channel);
276
- GetLatestBundleResponse response = fetchLatestBundle(fetchLatestBundleOptions);
277
- if (response == null) {
278
- Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
279
- SyncResult syncResult = new SyncResult(null);
280
- callback.success(syncResult);
281
- return;
282
- }
283
- ArtifactType artifactType = response.getArtifactType();
284
- String latestBundleId = response.getBundleId();
285
- String checksum = response.getChecksum();
286
- String signature = response.getSignature();
287
- String url = response.getUrl();
288
- // Check if the bundle already exists
289
- if (hasBundleById(latestBundleId)) {
290
- String nextBundleId = null;
291
- String currentBundleId = getCurrentBundleId();
292
- if (!latestBundleId.equals(currentBundleId)) {
293
- // Set the next bundle
294
- setNextBundleById(latestBundleId);
295
- nextBundleId = latestBundleId;
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);
453
+ }
296
454
  }
297
- SyncResult syncResult = new SyncResult(nextBundleId);
298
- callback.success(syncResult);
299
- return;
300
- }
301
- // Download the bundle
302
- if (artifactType == ArtifactType.MANIFEST) {
303
- downloadBundleOfTypeManifest(latestBundleId, url);
304
- } else {
305
- downloadBundleOfTypeZip(latestBundleId, checksum, signature, url);
306
- }
307
- // Set the next bundle
308
- setNextBundleById(latestBundleId);
309
- SyncResult syncResult = new SyncResult(latestBundleId);
310
- callback.success(syncResult);
455
+ );
311
456
  }
312
457
 
313
458
  private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
@@ -465,7 +610,7 @@ public class LiveUpdate {
465
610
  }
466
611
 
467
612
  private void deleteUnusedBundles() {
468
- String[] bundleIds = getBundleIds();
613
+ String[] bundleIds = getDownloadedBundleIds();
469
614
  for (String bundleId : bundleIds) {
470
615
  if (!isBundleInUse(bundleId)) {
471
616
  deleteBundleById(bundleId);
@@ -473,202 +618,349 @@ public class LiveUpdate {
473
618
  }
474
619
  }
475
620
 
476
- private void downloadAndVerifyFile(
621
+ private Call downloadAndVerifyFile(
477
622
  @NonNull String url,
478
623
  @NonNull File file,
479
624
  @Nullable String checksum,
480
625
  @Nullable String signature,
481
- @Nullable DownloadProgressCallback callback
482
- ) throws Exception {
483
- Response response = httpClient.execute(url);
484
- ResponseBody responseBody = response.body();
485
- if (response.isSuccessful()) {
486
- LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file, callback);
487
- // Verify the file
488
- if (checksum == null) {
489
- checksum = LiveUpdateHttpClient.getChecksumFromResponse(response);
490
- }
491
- if (signature == null) {
492
- 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
+ }
493
661
  }
494
- verifyFile(file, checksum, signature);
495
- } else {
496
- Exception exception = new Exception(responseBody.string());
497
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
498
- throw new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
499
- }
662
+ );
500
663
  }
501
664
 
502
- private File downloadBundleFile(
665
+ private Call downloadBundleFile(
503
666
  @NonNull String baseUrl,
504
667
  @NonNull String href,
505
668
  @NonNull File destinationDirectory,
506
- @Nullable DownloadProgressCallback callback
507
- ) throws Exception {
669
+ @Nullable DownloadProgressCallback progressCallback,
670
+ @NonNull EmptyCallback completionCallback
671
+ ) {
508
672
  HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
509
673
  urlBuilder.addQueryParameter("href", href);
510
674
  String url = urlBuilder.build().toString();
511
675
 
512
676
  File destinationFile = new File(destinationDirectory, href);
513
677
  destinationFile.getParentFile().mkdirs();
514
- downloadAndVerifyFile(url, destinationFile, null, null, callback);
515
- return destinationFile;
678
+ return downloadAndVerifyFile(url, destinationFile, null, null, progressCallback, completionCallback);
516
679
  }
517
680
 
518
681
  private void downloadBundleFiles(
519
682
  @NonNull String baseUrl,
520
683
  @NonNull List<ManifestItem> filesToDownload,
521
684
  @NonNull File destinationDirectory,
522
- @Nullable DownloadProgressCallback callback
523
- ) throws Exception {
524
- // Track the total downloaded bytes and total bytes to download for the progress callback
525
- 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);
526
698
  long totalBytesToDownload = 0;
527
- for (ManifestItem fileToDownload : filesToDownload) {
528
- totalBytesToDownload += fileToDownload.getSizeInBytes();
529
- }
530
- // Limit the number of threads to twice the number of processors
531
- int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
532
- List<Thread> threads = new ArrayList<>();
533
- AtomicReference<Exception> exceptionReference = new AtomicReference<>();
534
- for (ManifestItem fileToDownload : filesToDownload) {
535
- // Wait until a thread is available
536
- while (threads.size() >= maxThreads) {
537
- for (Thread thread : threads) {
538
- if (!thread.isAlive()) {
539
- threads.remove(thread);
540
- break;
699
+ for (ManifestItem item : filesToDownload) {
700
+ totalBytesToDownload += item.getSizeInBytes();
701
+ }
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);
721
+ }
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());
767
+ }
541
768
  }
542
769
  }
543
- // Wait a bit before checking again
544
- Thread.sleep(10);
545
- }
546
- // Stop if an exception occurred
547
- if (exceptionReference.get() != null) {
548
- break;
770
+ );
771
+ synchronized (activeCalls) {
772
+ activeCalls.add(call);
549
773
  }
550
- // Final variable that can be accessed from within inner class
551
- long finalTotalBytesToDownload = totalBytesToDownload;
552
- // Start a new thread
553
- Thread thread = new Thread(() -> {
554
- try {
555
- downloadBundleFile(baseUrl, fileToDownload.getHref(), destinationDirectory, (downloadedBytes, totalBytes) -> {
556
- if (callback != null) {
557
- callback.onProgress(totalBytesDownloaded[0] + downloadedBytes, finalTotalBytesToDownload);
774
+ }
775
+ }
776
+
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);
558
849
  }
559
- });
560
- if (callback != null) {
561
- totalBytesDownloaded[0] += fileToDownload.getSizeInBytes();
562
- callback.onProgress(totalBytesDownloaded[0], finalTotalBytesToDownload);
563
850
  }
564
- } catch (Exception exception) {
565
- exceptionReference.set(exception);
566
- Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + fileToDownload.getHref(), exception);
851
+
852
+ @Override
853
+ public void error(@NonNull Exception exception) {
854
+ completionCallback.error(exception);
855
+ }
567
856
  }
568
- });
569
- threads.add(thread);
570
- thread.start();
571
- }
572
- // Wait for all threads to finish
573
- for (Thread thread : threads) {
574
- thread.join();
575
- }
576
- // Throw the exception if one occurred
577
- Exception exception = exceptionReference.get();
578
- if (exception != null) {
579
- throw exception;
580
- }
581
- // Call the callback one last time to make sure the progress is at 100%
582
- if (callback != null) {
583
- callback.onProgress(totalBytesToDownload, totalBytesToDownload);
584
- }
585
- }
586
-
587
- private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
588
- // Create a temporary directory
589
- File temporaryDirectory = createTemporaryDirectory();
590
- // Download the latest manifest
591
- File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory, null);
592
- Manifest latestManifest = loadManifest(latestManifestFile);
593
- // Load the current manifest
594
- Manifest currentManifest = loadCurrentManifest();
595
- // Compare the manifests
596
- List<ManifestItem> itemsToCopy = new ArrayList<>();
597
- List<ManifestItem> itemsToDownload = new ArrayList<>();
598
- if (currentManifest == null) {
599
- itemsToDownload.addAll(latestManifest.getItems());
600
- } else {
601
- itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
602
- itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
603
- }
604
- // Copy the files
605
- List<ManifestItem> missingItems = copyCurrentBundleFilesAndReturnFailures(itemsToCopy, temporaryDirectory);
606
- // If items could not be copied, add them to the list of items to download
607
- if (!missingItems.isEmpty()) {
608
- itemsToDownload.addAll(missingItems);
609
- }
610
- // Download the files
611
- downloadBundleFiles(downloadUrl, itemsToDownload, temporaryDirectory, (downloadedBytes, totalBytes) -> {
612
- DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
613
- notifyDownloadBundleProgressListeners(event);
614
- });
615
- // Add the bundle
616
- addBundleOfTypeManifest(bundleId, temporaryDirectory);
857
+ );
858
+ } catch (Exception e) {
859
+ completionCallback.error(e);
860
+ }
617
861
  }
618
862
 
619
863
  private void downloadBundleOfTypeZip(
620
864
  @NonNull String bundleId,
621
865
  @Nullable String checksum,
622
866
  @Nullable String signature,
623
- @NonNull String downloadUrl
624
- ) throws Exception {
867
+ @NonNull String downloadUrl,
868
+ @NonNull EmptyCallback completionCallback
869
+ ) {
625
870
  File file = buildTemporaryZipFile();
626
871
  // Download the bundle
627
- downloadAndVerifyFile(downloadUrl, file, checksum, signature, (downloadedBytes, totalBytes) -> {
628
- DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
629
- notifyDownloadBundleProgressListeners(event);
630
- });
631
- // Add the bundle
632
- addBundleOfTypeZip(bundleId, file);
633
- // Delete the temporary file
634
- 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
+ );
635
903
  }
636
904
 
637
- @Nullable
638
- private GetLatestBundleResponse fetchLatestBundle(@NonNull FetchLatestBundleOptions options) throws Exception {
639
- String channel = options.getChannel() == null ? getChannel() : options.getChannel();
640
- String url = new HttpUrl.Builder()
641
- .scheme("https")
642
- .host(config.getServerDomain())
643
- .addPathSegment("v1")
644
- .addPathSegment("apps")
645
- .addPathSegment(config.getAppId())
646
- .addPathSegment("bundles")
647
- .addPathSegment("latest")
648
- .addQueryParameter("appVersionCode", getVersionCode())
649
- .addQueryParameter("appVersionName", getVersionName())
650
- .addQueryParameter("bundleId", getCurrentBundleId())
651
- .addQueryParameter("channelName", channel)
652
- .addQueryParameter("customId", preferences.getCustomId())
653
- .addQueryParameter("deviceId", getDeviceId())
654
- .addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
655
- .addQueryParameter("platform", "0")
656
- .addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
657
- .build()
658
- .toString();
659
- Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
660
- Response response = httpClient.execute(url);
661
- String responseBodyString = response.body().string();
662
- Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
663
- if (response.isSuccessful()) {
664
- JSONObject responseJson = new JSONObject(responseBodyString);
665
- return new GetLatestBundleResponse(responseJson);
666
- } else {
667
- 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);
668
960
  }
669
961
  }
670
962
 
671
- private String[] getBundleIds() {
963
+ private String[] getDownloadedBundleIds() {
672
964
  File bundlesDirectory = buildBundlesDirectory();
673
965
  File[] bundles = bundlesDirectory.listFiles();
674
966
  if (bundles == null) {
@@ -708,6 +1000,15 @@ public class LiveUpdate {
708
1000
  return checksum.toString();
709
1001
  }
710
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
+
711
1012
  @Nullable
712
1013
  private String getChannel() {
713
1014
  String channel = null;
@@ -741,10 +1042,10 @@ public class LiveUpdate {
741
1042
 
742
1043
  @NonNull
743
1044
  private String getDeviceId() {
744
- String deviceId = preferences.getDeviceIdForApp(config.getAppId());
1045
+ String deviceId = preferences.getDeviceIdForApp(getAppId());
745
1046
  if (deviceId == null) {
746
1047
  deviceId = UUID.randomUUID().toString().toLowerCase();
747
- preferences.setDeviceIdForApp(config.getAppId(), deviceId);
1048
+ preferences.setDeviceIdForApp(getAppId(), deviceId);
748
1049
  }
749
1050
  return deviceId;
750
1051
  }
@@ -766,10 +1067,15 @@ public class LiveUpdate {
766
1067
  */
767
1068
  @NonNull
768
1069
  private String getNextCapacitorServerPath() {
769
- return plugin
1070
+ String path = plugin
770
1071
  .getContext()
771
1072
  .getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
772
1073
  .getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
1074
+ // Empty path means default path
1075
+ if (path.isEmpty()) {
1076
+ path = defaultWebAssetDir;
1077
+ }
1078
+ return path;
773
1079
  }
774
1080
 
775
1081
  /**
@@ -780,8 +1086,12 @@ public class LiveUpdate {
780
1086
  return preferences.getPreviousBundleId();
781
1087
  }
782
1088
 
783
- private String getVersionCode() throws PackageManager.NameNotFoundException {
784
- 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());
785
1095
  }
786
1096
 
787
1097
  private String getVersionName() throws PackageManager.NameNotFoundException {
@@ -834,10 +1144,38 @@ public class LiveUpdate {
834
1144
  return loadManifest(source);
835
1145
  }
836
1146
 
837
- public void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
1147
+ private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
838
1148
  plugin.notifyDownloadBundleProgressListeners(event);
839
1149
  }
840
1150
 
1151
+ private void performAutoUpdate() {
1152
+ // Check if enough time has passed since the last check
1153
+ long now = System.currentTimeMillis();
1154
+ if (lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs) {
1155
+ Logger.debug(LiveUpdatePlugin.TAG, "Auto-update skipped. Last check was less than 15 minutes ago.");
1156
+ return;
1157
+ }
1158
+
1159
+ // Update the timestamp
1160
+ lastAutoUpdateCheckTimestamp = now;
1161
+
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
+ }
1170
+
1171
+ @Override
1172
+ public void error(@NonNull Exception exception) {
1173
+ Logger.error(LiveUpdatePlugin.TAG, "Auto-update failed: " + exception.getMessage(), exception);
1174
+ }
1175
+ };
1176
+ sync(options, callback);
1177
+ }
1178
+
841
1179
  private void rollback() {
842
1180
  // Set the rollback flag
843
1181
  rollbackPerformed = true;
@@ -899,6 +1237,8 @@ public class LiveUpdate {
899
1237
  this.plugin.getBridge().setServerBasePath(path);
900
1238
  }
901
1239
  this.plugin.getBridge().reload();
1240
+ // Notify listeners
1241
+ notifyReloadedListeners();
902
1242
  }
903
1243
 
904
1244
  /**
@@ -911,6 +1251,58 @@ public class LiveUpdate {
911
1251
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
912
1252
  setNextCapacitorServerPath(bundleDirectory.getPath());
913
1253
  }
1254
+
1255
+ // Notify listeners
1256
+ notifyNextBundleSetListeners(bundleId);
1257
+ }
1258
+
1259
+ private void notifyNextBundleSetListeners(@Nullable String bundleId) {
1260
+ NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
1261
+ plugin.notifyNextBundleSetListeners(event);
1262
+ }
1263
+
1264
+ private void notifyReloadedListeners() {
1265
+ plugin.notifyReloadedListeners();
1266
+ }
1267
+
1268
+ private void addBlockedBundleId(@NonNull String bundleId) {
1269
+ String blockedIds = preferences.getBlockedBundleIds();
1270
+ List<String> blockedList = new ArrayList<>();
1271
+
1272
+ // Parse existing blocked IDs
1273
+ if (blockedIds != null && !blockedIds.isEmpty()) {
1274
+ String[] ids = blockedIds.split(",");
1275
+ blockedList.addAll(Arrays.asList(ids));
1276
+ }
1277
+
1278
+ // Skip if already blocked
1279
+ if (blockedList.contains(bundleId)) {
1280
+ return;
1281
+ }
1282
+
1283
+ // Remove oldest if limit reached
1284
+ if (blockedList.size() >= 100) {
1285
+ blockedList.remove(0);
1286
+ }
1287
+
1288
+ // Add new bundle
1289
+ blockedList.add(bundleId);
1290
+
1291
+ // Save back to preferences
1292
+ String newBlockedIds = String.join(",", blockedList);
1293
+ preferences.setBlockedBundleIds(newBlockedIds);
1294
+
1295
+ Logger.debug(LiveUpdatePlugin.TAG, "Bundle blocked: " + bundleId);
1296
+ }
1297
+
1298
+ private boolean isBlockedBundleId(@NonNull String bundleId) {
1299
+ String blockedIds = preferences.getBlockedBundleIds();
1300
+ if (blockedIds == null || blockedIds.isEmpty()) {
1301
+ return false;
1302
+ }
1303
+
1304
+ String[] ids = blockedIds.split(",");
1305
+ return Arrays.asList(ids).contains(bundleId);
914
1306
  }
915
1307
 
916
1308
  private void setNextCapacitorServerPath(@NonNull String path) {
@@ -918,9 +1310,20 @@ public class LiveUpdate {
918
1310
  this.webViewSettingsEditor.commit();
919
1311
  }
920
1312
 
921
- /**
922
- * @param bundleId The bundle ID to save as the previous bundle. If `null`, the value will be removed.
923
- */
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
+
924
1327
  private void setPreviousBundleId(@Nullable String bundleId) {
925
1328
  preferences.setPreviousBundleId(bundleId);
926
1329
  }