@otakit/capacitor-updater 2.0.1 → 2.1.1

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/{UpdatekitUpdater.podspec → OtaKitUpdater.podspec} +1 -1
  2. package/README.md +165 -130
  3. package/android/build.gradle +1 -1
  4. package/android/src/main/java/com/{updatekit → otakit}/updater/BundleInfo.java +1 -1
  5. package/android/src/main/java/com/{updatekit → otakit}/updater/BundleStatus.java +1 -1
  6. package/android/src/main/java/com/{updatekit → otakit}/updater/BundleStore.java +38 -9
  7. package/android/src/main/java/com/{updatekit → otakit}/updater/DateUtils.java +1 -1
  8. package/android/src/main/java/com/{updatekit → otakit}/updater/DeviceEventClient.java +3 -4
  9. package/android/src/main/java/com/{updatekit → otakit}/updater/HashUtils.java +1 -1
  10. package/android/src/main/java/com/{updatekit → otakit}/updater/HostedManifestKeys.java +1 -1
  11. package/android/src/main/java/com/{updatekit → otakit}/updater/ManifestClient.java +32 -15
  12. package/android/src/main/java/com/{updatekit → otakit}/updater/ManifestVerifier.java +1 -1
  13. package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +726 -0
  14. package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +1191 -0
  15. package/android/src/main/java/com/{updatekit → otakit}/updater/ZipUtils.java +1 -1
  16. package/dist/esm/definitions.d.ts +48 -105
  17. package/dist/esm/definitions.d.ts.map +1 -1
  18. package/dist/esm/definitions.js.map +1 -1
  19. package/dist/esm/index.d.ts.map +1 -1
  20. package/dist/esm/index.js +6 -60
  21. package/dist/esm/index.js.map +1 -1
  22. package/dist/esm/web.d.ts +7 -11
  23. package/dist/esm/web.d.ts.map +1 -1
  24. package/dist/esm/web.js +8 -14
  25. package/dist/esm/web.js.map +1 -1
  26. package/dist/plugin.cjs.js +14 -74
  27. package/dist/plugin.cjs.js.map +1 -1
  28. package/dist/plugin.js +14 -74
  29. package/dist/plugin.js.map +1 -1
  30. package/ios/Sources/UpdaterPlugin/BundleStore.swift +28 -6
  31. package/ios/Sources/UpdaterPlugin/Downloader.swift +1 -1
  32. package/ios/Sources/UpdaterPlugin/ManifestClient.swift +8 -4
  33. package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +635 -0
  34. package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +3 -5
  35. package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +506 -551
  36. package/package.json +2 -2
  37. package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +0 -1191
@@ -0,0 +1,726 @@
1
+ package com.otakit.updater;
2
+
3
+ import java.io.File;
4
+ import java.util.ArrayList;
5
+ import java.util.LinkedHashSet;
6
+ import java.util.List;
7
+ import java.util.Set;
8
+ import java.util.concurrent.atomic.AtomicBoolean;
9
+ import java.util.concurrent.locks.ReentrantLock;
10
+
11
+ final class UpdaterCoordinator {
12
+
13
+ interface BundlePredicate {
14
+ boolean test(BundleInfo bundle);
15
+ }
16
+
17
+ static final class StateSnapshot {
18
+
19
+ final BundleInfo current;
20
+ final BundleInfo fallback;
21
+ final BundleInfo staged;
22
+ final String builtinVersion;
23
+
24
+ StateSnapshot(
25
+ BundleInfo current,
26
+ BundleInfo fallback,
27
+ BundleInfo staged,
28
+ String builtinVersion
29
+ ) {
30
+ this.current = current;
31
+ this.fallback = fallback;
32
+ this.staged = staged;
33
+ this.builtinVersion = builtinVersion;
34
+ }
35
+ }
36
+
37
+ static final class DeviceEventPayload {
38
+
39
+ final String action;
40
+ final String bundleVersion;
41
+ final String runtimeVersion;
42
+ final String channel;
43
+ final String releaseId;
44
+ final String detail;
45
+
46
+ DeviceEventPayload(
47
+ String action,
48
+ String bundleVersion,
49
+ String runtimeVersion,
50
+ String channel,
51
+ String releaseId,
52
+ String detail
53
+ ) {
54
+ this.action = action;
55
+ this.bundleVersion = bundleVersion;
56
+ this.runtimeVersion = runtimeVersion;
57
+ this.channel = channel;
58
+ this.releaseId = releaseId;
59
+ this.detail = detail;
60
+ }
61
+ }
62
+
63
+ static final class LatestManifestClassification {
64
+
65
+ final String kind;
66
+ final BundleInfo bundle;
67
+
68
+ private LatestManifestClassification(String kind, BundleInfo bundle) {
69
+ this.kind = kind;
70
+ this.bundle = bundle;
71
+ }
72
+
73
+ static LatestManifestClassification noUpdate() {
74
+ return new LatestManifestClassification("no_update", null);
75
+ }
76
+
77
+ static LatestManifestClassification alreadyStaged(BundleInfo bundle) {
78
+ return new LatestManifestClassification("already_staged", bundle);
79
+ }
80
+
81
+ static LatestManifestClassification updateAvailable() {
82
+ return new LatestManifestClassification("update_available", null);
83
+ }
84
+ }
85
+
86
+ static final class StartupPreparation {
87
+
88
+ final String activationPath;
89
+ final String trialBundleId;
90
+ final List<String> cleanupBundleIds;
91
+ final DeviceEventPayload eventPayload;
92
+
93
+ StartupPreparation(
94
+ String activationPath,
95
+ String trialBundleId,
96
+ List<String> cleanupBundleIds,
97
+ DeviceEventPayload eventPayload
98
+ ) {
99
+ this.activationPath = activationPath;
100
+ this.trialBundleId = trialBundleId;
101
+ this.cleanupBundleIds = cleanupBundleIds;
102
+ this.eventPayload = eventPayload;
103
+ }
104
+ }
105
+
106
+ static final class ApplyPreparation {
107
+
108
+ final String activationPath;
109
+ final String trialBundleId;
110
+ final List<String> cleanupBundleIds;
111
+
112
+ ApplyPreparation(String activationPath, String trialBundleId, List<String> cleanupBundleIds) {
113
+ this.activationPath = activationPath;
114
+ this.trialBundleId = trialBundleId;
115
+ this.cleanupBundleIds = cleanupBundleIds;
116
+ }
117
+
118
+ boolean didApply() {
119
+ return activationPath != null;
120
+ }
121
+ }
122
+
123
+ static final class NotifyReadyPreparation {
124
+
125
+ final DeviceEventPayload eventPayload;
126
+ final List<String> cleanupBundleIds;
127
+
128
+ NotifyReadyPreparation(DeviceEventPayload eventPayload, List<String> cleanupBundleIds) {
129
+ this.eventPayload = eventPayload;
130
+ this.cleanupBundleIds = cleanupBundleIds;
131
+ }
132
+ }
133
+
134
+ static final class RollbackPreparation {
135
+
136
+ final boolean didRollback;
137
+ final String activationPath;
138
+ final DeviceEventPayload eventPayload;
139
+ final List<String> cleanupBundleIds;
140
+
141
+ RollbackPreparation(
142
+ boolean didRollback,
143
+ String activationPath,
144
+ DeviceEventPayload eventPayload,
145
+ List<String> cleanupBundleIds
146
+ ) {
147
+ this.didRollback = didRollback;
148
+ this.activationPath = activationPath;
149
+ this.eventPayload = eventPayload;
150
+ this.cleanupBundleIds = cleanupBundleIds;
151
+ }
152
+ }
153
+
154
+ private static final class LockedRollbackResult {
155
+
156
+ final boolean didRollback;
157
+ final String activationPath;
158
+ final DeviceEventPayload eventPayload;
159
+
160
+ LockedRollbackResult(
161
+ boolean didRollback,
162
+ String activationPath,
163
+ DeviceEventPayload eventPayload
164
+ ) {
165
+ this.didRollback = didRollback;
166
+ this.activationPath = activationPath;
167
+ this.eventPayload = eventPayload;
168
+ }
169
+ }
170
+
171
+ @FunctionalInterface
172
+ private interface LockedSupplier<T> {
173
+ T get() throws Exception;
174
+ }
175
+
176
+ private final BundleStore store;
177
+ private final AtomicBoolean operationInProgress = new AtomicBoolean(false);
178
+ private final ReentrantLock stateLock = new ReentrantLock();
179
+
180
+ UpdaterCoordinator(BundleStore store) {
181
+ this.store = store;
182
+ }
183
+
184
+ String getNativeBuild() {
185
+ return store.getNativeBuild();
186
+ }
187
+
188
+ File bundleDirectory(String id) {
189
+ return store.bundleDirectory(id);
190
+ }
191
+
192
+ boolean tryBeginOperation() {
193
+ return operationInProgress.compareAndSet(false, true);
194
+ }
195
+
196
+ void endOperation() {
197
+ operationInProgress.set(false);
198
+ }
199
+
200
+ StateSnapshot snapshotState(BundlePredicate isStagedBundleUsable) {
201
+ return withStateLock(() ->
202
+ new StateSnapshot(
203
+ store.getCurrentBundle(),
204
+ store.getFallbackBundle(),
205
+ readStagedBundleLocked(null, isStagedBundleUsable),
206
+ store.getBuiltinVersion()
207
+ )
208
+ );
209
+ }
210
+
211
+ BundleInfo lastFailure() {
212
+ return withStateLock(store::getLastFailedBundle);
213
+ }
214
+
215
+ List<String> pruneIncompatibleBundles(BundlePredicate isCompatibleRuntime) {
216
+ return withStateLock(() -> {
217
+ Set<String> cleanupBundleIds = new LinkedHashSet<>();
218
+
219
+ for (BundleInfo bundle : store.listDownloadedBundleInfos()) {
220
+ if (!isCompatibleRuntime.test(bundle)) {
221
+ detachBundleReferencesLocked(bundle.id);
222
+ cleanupBundleIds.add(bundle.id);
223
+ }
224
+ }
225
+
226
+ BundleInfo failed = store.getLastFailedBundle();
227
+ if (failed != null && !isCompatibleRuntime.test(failed)) {
228
+ store.setLastFailedBundle(null);
229
+ }
230
+
231
+ return new ArrayList<>(cleanupBundleIds);
232
+ });
233
+ }
234
+
235
+ StartupPreparation normalizeStartupState(BundlePredicate isBundleUsable) {
236
+ return withStateLock(() -> {
237
+ Set<String> cleanupBundleIds = new LinkedHashSet<>();
238
+ clearStaleBundlePointersLocked();
239
+ normalizeStagedPointerLocked(isBundleUsable, cleanupBundleIds);
240
+ normalizeFallbackPointerLocked(isBundleUsable, cleanupBundleIds);
241
+
242
+ DeviceEventPayload eventPayload = null;
243
+ BundleInfo current = store.getCurrentBundle();
244
+ if (!current.isBuiltin() && current.status == BundleStatus.TRIAL) {
245
+ LockedRollbackResult rollback = rollbackLocked(
246
+ "app_restarted_before_notify",
247
+ isBundleUsable,
248
+ cleanupBundleIds
249
+ );
250
+ eventPayload = rollback.eventPayload;
251
+ }
252
+
253
+ BundleInfo normalizedCurrent = store.getCurrentBundle();
254
+ if (!normalizedCurrent.isBuiltin() && !isBundleUsable.test(normalizedCurrent)) {
255
+ cleanupBundleIds.add(normalizedCurrent.id);
256
+ normalizedCurrent = restoreFallbackOrBuiltinLocked(isBundleUsable, cleanupBundleIds);
257
+ }
258
+
259
+ String trialBundleId = null;
260
+ if (!normalizedCurrent.isBuiltin() && normalizedCurrent.status == BundleStatus.PENDING) {
261
+ normalizedCurrent = updateStatusLocked(normalizedCurrent, BundleStatus.TRIAL);
262
+ trialBundleId = normalizedCurrent.id;
263
+ }
264
+
265
+ return new StartupPreparation(
266
+ normalizedCurrent.isBuiltin() ? null : normalizedCurrent.path,
267
+ trialBundleId,
268
+ new ArrayList<>(cleanupBundleIds),
269
+ eventPayload
270
+ );
271
+ });
272
+ }
273
+
274
+ boolean isRuntimeUnresolved(String currentRuntimeKey) {
275
+ return withStateLock(() ->
276
+ !java.util.Objects.equals(store.getLastResolvedRuntimeKey(), currentRuntimeKey)
277
+ );
278
+ }
279
+
280
+ void resolveRuntimeKey(String currentRuntimeKey) {
281
+ withStateLock(() -> {
282
+ store.setLastResolvedRuntimeKey(currentRuntimeKey);
283
+ return null;
284
+ });
285
+ }
286
+
287
+ LatestManifestClassification classifyLatestManifest(
288
+ ManifestClient.LatestManifest manifest,
289
+ String targetChannel,
290
+ BundlePredicate isStagedBundleUsable
291
+ ) {
292
+ return withStateLock(() -> {
293
+ BundleInfo current = store.getCurrentBundle();
294
+ if (doesBundleMatchLatest(current, manifest, targetChannel)) {
295
+ return LatestManifestClassification.noUpdate();
296
+ }
297
+
298
+ BundleInfo failed = store.getLastFailedBundle();
299
+ if (failed != null && doesFailedBundleMatchLatest(failed, manifest, targetChannel)) {
300
+ return LatestManifestClassification.noUpdate();
301
+ }
302
+
303
+ BundleInfo staged = readStagedBundleLocked(
304
+ bundle ->
305
+ java.util.Objects.equals(
306
+ trimToNull(bundle.runtimeVersion),
307
+ trimToNull(manifest.runtimeVersion)
308
+ ),
309
+ isStagedBundleUsable
310
+ );
311
+ if (staged != null && doesBundleMatchLatest(staged, manifest, targetChannel)) {
312
+ return LatestManifestClassification.alreadyStaged(staged);
313
+ }
314
+
315
+ return LatestManifestClassification.updateAvailable();
316
+ });
317
+ }
318
+
319
+ List<String> stageDownloadedBundle(BundleInfo bundle) throws Exception {
320
+ return withStateLock(() -> {
321
+ Set<String> cleanupBundleIds = new LinkedHashSet<>();
322
+ String previousStagedId = store.getStagedBundleId();
323
+ store.saveBundle(bundle);
324
+ store.setStagedBundleId(bundle.id);
325
+
326
+ if (
327
+ previousStagedId != null &&
328
+ !previousStagedId.equals(bundle.id) &&
329
+ !"builtin".equals(previousStagedId) &&
330
+ !previousStagedId.equals(store.getCurrentBundleId()) &&
331
+ !previousStagedId.equals(store.getFallbackBundleId())
332
+ ) {
333
+ cleanupBundleIds.add(previousStagedId);
334
+ }
335
+
336
+ return new ArrayList<>(cleanupBundleIds);
337
+ });
338
+ }
339
+
340
+ ApplyPreparation prepareApplyStaged(
341
+ BundlePredicate isCompatibleRuntime,
342
+ BundlePredicate isBundleUsable
343
+ ) {
344
+ return withStateLock(() -> {
345
+ Set<String> cleanupBundleIds = new LinkedHashSet<>();
346
+ BundleInfo staged = stagedBundleLocked(
347
+ true,
348
+ isCompatibleRuntime,
349
+ isBundleUsable,
350
+ cleanupBundleIds
351
+ );
352
+ if (staged == null) {
353
+ return new ApplyPreparation(null, null, new ArrayList<>(cleanupBundleIds));
354
+ }
355
+
356
+ BundleInfo previousCurrent = store.getCurrentBundle();
357
+ if (previousCurrent.isBuiltin()) {
358
+ store.setFallbackBundleId(null);
359
+ } else {
360
+ store.setFallbackBundleId(previousCurrent.id);
361
+ }
362
+
363
+ store.setCurrentBundleId(staged.id);
364
+ store.setStagedBundleId(null);
365
+
366
+ String trialBundleId = null;
367
+ if (staged.status == BundleStatus.PENDING) {
368
+ staged = updateStatusLocked(staged, BundleStatus.TRIAL);
369
+ trialBundleId = staged.id;
370
+ } else if (staged.status == BundleStatus.TRIAL) {
371
+ trialBundleId = staged.id;
372
+ }
373
+
374
+ return new ApplyPreparation(staged.path, trialBundleId, new ArrayList<>(cleanupBundleIds));
375
+ });
376
+ }
377
+
378
+ NotifyReadyPreparation prepareNotifyAppReady() {
379
+ return withStateLock(() -> {
380
+ BundleInfo current = store.getCurrentBundle();
381
+ if (current.isBuiltin() || current.status != BundleStatus.TRIAL) {
382
+ return new NotifyReadyPreparation(null, new ArrayList<>());
383
+ }
384
+
385
+ String oldFallbackId = store.getFallbackBundleId();
386
+ updateStatusLocked(current, BundleStatus.SUCCESS);
387
+ store.setFallbackBundleId(current.id);
388
+
389
+ Set<String> cleanupBundleIds = new LinkedHashSet<>();
390
+ if (oldFallbackId != null && !oldFallbackId.equals(current.id)) {
391
+ cleanupBundleIds.add(oldFallbackId);
392
+ }
393
+
394
+ return new NotifyReadyPreparation(
395
+ new DeviceEventPayload(
396
+ "applied",
397
+ current.version,
398
+ current.runtimeVersion,
399
+ current.channel,
400
+ current.releaseId,
401
+ null
402
+ ),
403
+ new ArrayList<>(cleanupBundleIds)
404
+ );
405
+ });
406
+ }
407
+
408
+ RollbackPreparation prepareRollback(String reason, BundlePredicate isBundleUsable) {
409
+ return withStateLock(() -> {
410
+ Set<String> cleanupBundleIds = new LinkedHashSet<>();
411
+ LockedRollbackResult rollback = rollbackLocked(reason, isBundleUsable, cleanupBundleIds);
412
+ return new RollbackPreparation(
413
+ rollback.didRollback,
414
+ rollback.activationPath,
415
+ rollback.eventPayload,
416
+ new ArrayList<>(cleanupBundleIds)
417
+ );
418
+ });
419
+ }
420
+
421
+ boolean isCurrentTrialBundle(String bundleId) {
422
+ return withStateLock(() -> {
423
+ BundleInfo current = store.getCurrentBundle();
424
+ return current.id.equals(bundleId) && current.status == BundleStatus.TRIAL;
425
+ });
426
+ }
427
+
428
+ void cleanupBundles(List<String> bundleIds) {
429
+ LinkedHashSet<String> uniqueIds = new LinkedHashSet<>();
430
+ for (String bundleId : bundleIds) {
431
+ if (bundleId != null && !bundleId.isEmpty() && !"builtin".equals(bundleId)) {
432
+ uniqueIds.add(bundleId);
433
+ }
434
+ }
435
+
436
+ for (String bundleId : uniqueIds) {
437
+ deleteRecursivelyQuietly(store.bundleDirectory(bundleId));
438
+ }
439
+ }
440
+
441
+ private <T> T withStateLock(LockedSupplier<T> work) {
442
+ stateLock.lock();
443
+ try {
444
+ return work.get();
445
+ } catch (RuntimeException e) {
446
+ throw e;
447
+ } catch (Exception e) {
448
+ throw new IllegalStateException(e);
449
+ } finally {
450
+ stateLock.unlock();
451
+ }
452
+ }
453
+
454
+ private void clearStaleBundlePointersLocked() {
455
+ String currentId = store.getCurrentBundleId();
456
+ if (currentId != null && store.getBundle(currentId) == null) {
457
+ store.setCurrentBundleId(null);
458
+ }
459
+
460
+ String fallbackId = store.getFallbackBundleId();
461
+ if (fallbackId != null && store.getBundle(fallbackId) == null) {
462
+ store.setFallbackBundleId(null);
463
+ }
464
+
465
+ String stagedId = store.getStagedBundleId();
466
+ if (stagedId != null && store.getBundle(stagedId) == null) {
467
+ store.setStagedBundleId(null);
468
+ }
469
+ }
470
+
471
+ private void normalizeFallbackPointerLocked(
472
+ BundlePredicate isBundleUsable,
473
+ Set<String> cleanupBundleIds
474
+ ) {
475
+ String fallbackId = store.getFallbackBundleId();
476
+ if (fallbackId == null) {
477
+ return;
478
+ }
479
+
480
+ BundleInfo fallback = store.getBundle(fallbackId);
481
+ if (fallback == null || fallback.isBuiltin()) {
482
+ return;
483
+ }
484
+
485
+ if (!isBundleUsable.test(fallback)) {
486
+ store.setFallbackBundleId(null);
487
+ cleanupBundleIds.add(fallback.id);
488
+ }
489
+ }
490
+
491
+ private void normalizeStagedPointerLocked(
492
+ BundlePredicate isBundleUsable,
493
+ Set<String> cleanupBundleIds
494
+ ) {
495
+ String stagedId = store.getStagedBundleId();
496
+ if (stagedId == null) {
497
+ return;
498
+ }
499
+
500
+ BundleInfo staged = store.getBundle(stagedId);
501
+ if (staged == null) {
502
+ store.setStagedBundleId(null);
503
+ return;
504
+ }
505
+
506
+ if (!isBundleUsable.test(staged)) {
507
+ store.setStagedBundleId(null);
508
+ cleanupBundleIds.add(staged.id);
509
+ }
510
+ }
511
+
512
+ private BundleInfo readStagedBundleLocked(
513
+ BundlePredicate isCompatibleRuntime,
514
+ BundlePredicate isUsable
515
+ ) {
516
+ return stagedBundleLocked(false, isCompatibleRuntime, isUsable, new LinkedHashSet<>());
517
+ }
518
+
519
+ private BundleInfo stagedBundleLocked(
520
+ boolean cleanInvalid,
521
+ BundlePredicate isCompatibleRuntime,
522
+ BundlePredicate isUsable,
523
+ Set<String> cleanupBundleIds
524
+ ) {
525
+ String stagedId = store.getStagedBundleId();
526
+ if (stagedId == null) {
527
+ return null;
528
+ }
529
+
530
+ BundleInfo staged = store.getBundle(stagedId);
531
+ if (staged == null) {
532
+ if (cleanInvalid) {
533
+ store.setStagedBundleId(null);
534
+ }
535
+ return null;
536
+ }
537
+
538
+ if (isCompatibleRuntime != null && !isCompatibleRuntime.test(staged)) {
539
+ if (cleanInvalid) {
540
+ store.setStagedBundleId(null);
541
+ cleanupBundleIds.add(staged.id);
542
+ }
543
+ return null;
544
+ }
545
+
546
+ if (!isUsable.test(staged)) {
547
+ if (cleanInvalid) {
548
+ store.setStagedBundleId(null);
549
+ cleanupBundleIds.add(staged.id);
550
+ }
551
+ return null;
552
+ }
553
+
554
+ return staged;
555
+ }
556
+
557
+ private void detachBundleReferencesLocked(String bundleId) {
558
+ if (bundleId.equals(store.getCurrentBundleId())) {
559
+ store.setCurrentBundleId(null);
560
+ }
561
+ if (bundleId.equals(store.getFallbackBundleId())) {
562
+ store.setFallbackBundleId(null);
563
+ }
564
+ if (bundleId.equals(store.getStagedBundleId())) {
565
+ store.setStagedBundleId(null);
566
+ }
567
+ }
568
+
569
+ private LockedRollbackResult rollbackLocked(
570
+ String reason,
571
+ BundlePredicate isBundleUsable,
572
+ Set<String> cleanupBundleIds
573
+ ) {
574
+ BundleInfo current = store.getCurrentBundle();
575
+ if (current.isBuiltin()) {
576
+ return new LockedRollbackResult(false, null, null);
577
+ }
578
+
579
+ BundleInfo failed = updateStatusLocked(current, BundleStatus.ERROR);
580
+ store.setLastFailedBundle(failed);
581
+ store.setStagedBundleId(null);
582
+
583
+ BundleInfo fallback = restoreFallbackOrBuiltinLocked(isBundleUsable, cleanupBundleIds);
584
+ cleanupBundleIds.add(current.id);
585
+
586
+ return new LockedRollbackResult(
587
+ true,
588
+ fallback.isBuiltin() ? null : fallback.path,
589
+ new DeviceEventPayload(
590
+ "rollback",
591
+ current.version,
592
+ current.runtimeVersion,
593
+ current.channel,
594
+ current.releaseId,
595
+ reason
596
+ )
597
+ );
598
+ }
599
+
600
+ private BundleInfo restoreFallbackOrBuiltinLocked(
601
+ BundlePredicate isBundleUsable,
602
+ Set<String> cleanupBundleIds
603
+ ) {
604
+ String fallbackId = store.getFallbackBundleId();
605
+ if (fallbackId != null) {
606
+ BundleInfo fallback = store.getBundle(fallbackId);
607
+ if (fallback != null && !fallback.isBuiltin() && isBundleUsable.test(fallback)) {
608
+ store.setCurrentBundleId(fallback.id);
609
+ return fallback;
610
+ }
611
+ }
612
+
613
+ if (fallbackId != null) {
614
+ store.setFallbackBundleId(null);
615
+ if (!"builtin".equals(fallbackId)) {
616
+ cleanupBundleIds.add(fallbackId);
617
+ }
618
+ }
619
+
620
+ store.setCurrentBundleId(null);
621
+ return store.builtinBundle();
622
+ }
623
+
624
+ private BundleInfo updateStatusLocked(BundleInfo bundle, BundleStatus status) {
625
+ BundleInfo updated = bundle.withStatus(status);
626
+ try {
627
+ store.saveBundle(updated);
628
+ } catch (Exception ignored) {}
629
+ return updated;
630
+ }
631
+
632
+ private boolean doesFailedBundleMatchLatest(
633
+ BundleInfo failed,
634
+ ManifestClient.LatestManifest latest,
635
+ String targetChannel
636
+ ) {
637
+ if (!java.util.Objects.equals(trimToNull(failed.channel), targetChannel)) {
638
+ return false;
639
+ }
640
+ if (
641
+ !java.util.Objects.equals(
642
+ trimToNull(failed.runtimeVersion),
643
+ trimToNull(latest.runtimeVersion)
644
+ )
645
+ ) {
646
+ return false;
647
+ }
648
+
649
+ String latestReleaseId = trimToNull(latest.releaseId);
650
+ String failedReleaseId = trimToNull(failed.releaseId);
651
+ if (latestReleaseId != null && latestReleaseId.equals(failedReleaseId)) {
652
+ return true;
653
+ }
654
+
655
+ String latestSha = trimToNull(latest.sha256);
656
+ String failedSha = trimToNull(failed.sha256);
657
+ return latestSha != null && latestSha.equals(failedSha);
658
+ }
659
+
660
+ private boolean doesBundleMatchLatest(
661
+ BundleInfo bundle,
662
+ ManifestClient.LatestManifest latest,
663
+ String targetChannel
664
+ ) {
665
+ if (!java.util.Objects.equals(trimToNull(bundle.channel), targetChannel)) {
666
+ return false;
667
+ }
668
+
669
+ if (
670
+ !java.util.Objects.equals(
671
+ trimToNull(bundle.runtimeVersion),
672
+ trimToNull(latest.runtimeVersion)
673
+ )
674
+ ) {
675
+ return false;
676
+ }
677
+
678
+ String latestReleaseId = trimToNull(latest.releaseId);
679
+ String bundleReleaseId = trimToNull(bundle.releaseId);
680
+ if (latestReleaseId != null && latestReleaseId.equals(bundleReleaseId)) {
681
+ return true;
682
+ }
683
+
684
+ String latestSha = trimToNull(latest.sha256);
685
+ String bundleSha = trimToNull(bundle.sha256);
686
+ if (latestSha != null && latestSha.equals(bundleSha)) {
687
+ return true;
688
+ }
689
+
690
+ if (
691
+ latestReleaseId != null || bundleReleaseId != null || latestSha != null || bundleSha != null
692
+ ) {
693
+ return false;
694
+ }
695
+
696
+ return latest.version != null && latest.version.equals(bundle.version);
697
+ }
698
+
699
+ private static String trimToNull(String value) {
700
+ if (value == null) {
701
+ return null;
702
+ }
703
+ String trimmed = value.trim();
704
+ return trimmed.isEmpty() ? null : trimmed;
705
+ }
706
+
707
+ private static void deleteRecursivelyQuietly(File target) {
708
+ if (target == null || !target.exists()) {
709
+ return;
710
+ }
711
+
712
+ if (target.isDirectory()) {
713
+ File[] children = target.listFiles();
714
+ if (children != null) {
715
+ for (File child : children) {
716
+ deleteRecursivelyQuietly(child);
717
+ }
718
+ }
719
+ }
720
+
721
+ try {
722
+ //noinspection ResultOfMethodCallIgnored
723
+ target.delete();
724
+ } catch (Throwable ignored) {}
725
+ }
726
+ }