@capgo/capacitor-updater 8.51.0 → 8.51.2
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.
- package/README.md +50 -47
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +251 -85
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +289 -60
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +1 -1
- package/dist/docs.json +26 -2
- package/dist/esm/definitions.d.ts +23 -7
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +5 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +609 -69
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +187 -34
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +8 -0
- package/package.json +1 -1
|
@@ -39,7 +39,9 @@ import java.security.SecureRandom;
|
|
|
39
39
|
import java.util.ArrayList;
|
|
40
40
|
import java.util.Date;
|
|
41
41
|
import java.util.HashMap;
|
|
42
|
+
import java.util.HashSet;
|
|
42
43
|
import java.util.Iterator;
|
|
44
|
+
import java.util.LinkedHashSet;
|
|
43
45
|
import java.util.List;
|
|
44
46
|
import java.util.Map;
|
|
45
47
|
import java.util.Objects;
|
|
@@ -72,14 +74,20 @@ public class CapgoUpdater {
|
|
|
72
74
|
private static final String FALLBACK_VERSION = "pastVersion";
|
|
73
75
|
private static final String NEXT_VERSION = "nextVersion";
|
|
74
76
|
private static final String PREVIEW_FALLBACK_VERSION = "previewFallbackVersion";
|
|
77
|
+
private static final String PENDING_DELETE_IDS = "pendingDeleteIds";
|
|
75
78
|
private static final String bundleDirectory = "versions";
|
|
76
79
|
private static final String TEMP_UNZIP_PREFIX = "capgo_unzip_";
|
|
80
|
+
private static final long DELETE_PACE_MS = 75L;
|
|
81
|
+
private final Object deleteLock = new Object();
|
|
77
82
|
private static final String CAPACITOR_CONFIG_ASSET = "capacitor.config.json";
|
|
78
83
|
private static final String BACKGROUND_RUNNER_CONFIG_KEY = "BackgroundRunner";
|
|
79
84
|
private static final String BACKGROUND_RUNNER_WORKER_CLASS = "io.ionic.backgroundrunner.plugin.RunnerWorker";
|
|
80
85
|
|
|
81
86
|
public static final String TAG = "Capacitor-updater";
|
|
82
87
|
public SharedPreferences.Editor editor;
|
|
88
|
+
|
|
89
|
+
/** Optional gate run before any download touches disk (e.g. wait for launch cleanup). */
|
|
90
|
+
public Runnable downloadGate = null;
|
|
83
91
|
public SharedPreferences prefs;
|
|
84
92
|
|
|
85
93
|
public File documentsDir;
|
|
@@ -136,7 +144,7 @@ public class CapgoUpdater {
|
|
|
136
144
|
|
|
137
145
|
private final FilenameFilter filter = (f, name) -> {
|
|
138
146
|
// ignore directories generated by mac os x
|
|
139
|
-
return
|
|
147
|
+
return !name.startsWith("__MACOSX") && !name.startsWith(".") && !name.startsWith(".DS_Store");
|
|
140
148
|
};
|
|
141
149
|
|
|
142
150
|
private boolean isProd() {
|
|
@@ -927,6 +935,11 @@ public class CapgoUpdater {
|
|
|
927
935
|
|
|
928
936
|
try {
|
|
929
937
|
this.deleteDirectory(entry, threadToCheck);
|
|
938
|
+
if (entry.exists()) {
|
|
939
|
+
logger.error("Orphan bundle directory still present after delete");
|
|
940
|
+
logger.debug("Bundle ID: " + id);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
930
943
|
this.removeBundleInfo(id);
|
|
931
944
|
logger.info("Deleted orphan bundle directory");
|
|
932
945
|
logger.debug("Bundle ID: " + id);
|
|
@@ -938,6 +951,43 @@ public class CapgoUpdater {
|
|
|
938
951
|
}
|
|
939
952
|
}
|
|
940
953
|
|
|
954
|
+
public Set<String> allowedBundleIdsForCleanup() {
|
|
955
|
+
final Set<String> allowedIds = new HashSet<>();
|
|
956
|
+
for (final BundleInfo info : this.list(true)) {
|
|
957
|
+
if (info == null || info.getId() == null || info.getId().isEmpty()) {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
// DELETED tombstones must not protect leftover folders.
|
|
961
|
+
// DELETING stays protected so drainPendingDeletes owns the removal.
|
|
962
|
+
if (info.isDeleted()) {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
allowedIds.add(info.getId());
|
|
966
|
+
}
|
|
967
|
+
final String currentId = this.getCurrentBundleId();
|
|
968
|
+
if (currentId != null && !currentId.isEmpty()) {
|
|
969
|
+
allowedIds.add(currentId);
|
|
970
|
+
}
|
|
971
|
+
final BundleInfo fallback = this.getFallbackBundle();
|
|
972
|
+
if (fallback != null && fallback.getId() != null && !fallback.getId().isEmpty() && !fallback.isDeleting()) {
|
|
973
|
+
allowedIds.add(fallback.getId());
|
|
974
|
+
}
|
|
975
|
+
final BundleInfo next = this.getNextBundle();
|
|
976
|
+
if (next != null && next.getId() != null && !next.getId().isEmpty() && !next.isDeleting()) {
|
|
977
|
+
allowedIds.add(next.getId());
|
|
978
|
+
}
|
|
979
|
+
final BundleInfo previewFallback = this.getPreviewFallbackBundle();
|
|
980
|
+
if (
|
|
981
|
+
previewFallback != null &&
|
|
982
|
+
previewFallback.getId() != null &&
|
|
983
|
+
!previewFallback.getId().isEmpty() &&
|
|
984
|
+
!previewFallback.isDeleting()
|
|
985
|
+
) {
|
|
986
|
+
allowedIds.add(previewFallback.getId());
|
|
987
|
+
}
|
|
988
|
+
return allowedIds;
|
|
989
|
+
}
|
|
990
|
+
|
|
941
991
|
public void cleanupOrphanedTempFolders(final Thread threadToCheck) {
|
|
942
992
|
if (this.documentsDir == null) {
|
|
943
993
|
logger.warn("Documents directory is null, skipping temp folder cleanup");
|
|
@@ -1221,6 +1271,27 @@ public class CapgoUpdater {
|
|
|
1221
1271
|
);
|
|
1222
1272
|
}
|
|
1223
1273
|
|
|
1274
|
+
private void runDownloadGate() throws IOException {
|
|
1275
|
+
if (this.downloadGate == null) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
this.downloadGate.run();
|
|
1280
|
+
} catch (final RuntimeException e) {
|
|
1281
|
+
throw new IOException(e.getMessage() == null ? "Download gate failed" : e.getMessage(), e);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
private boolean runDownloadGateQuiet() {
|
|
1286
|
+
try {
|
|
1287
|
+
this.runDownloadGate();
|
|
1288
|
+
return true;
|
|
1289
|
+
} catch (final IOException e) {
|
|
1290
|
+
logger.error("Download blocked by cleanup gate: " + e.getMessage());
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1224
1295
|
public void downloadBackground(
|
|
1225
1296
|
final String url,
|
|
1226
1297
|
final String version,
|
|
@@ -1239,6 +1310,9 @@ public class CapgoUpdater {
|
|
|
1239
1310
|
final JSONArray manifest,
|
|
1240
1311
|
final boolean setNext
|
|
1241
1312
|
) {
|
|
1313
|
+
if (!this.runDownloadGateQuiet()) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1242
1316
|
final String id = this.randomString();
|
|
1243
1317
|
|
|
1244
1318
|
// Check if version is already downloading, but allow retry if previous download failed
|
|
@@ -1263,9 +1337,10 @@ public class CapgoUpdater {
|
|
|
1263
1337
|
}
|
|
1264
1338
|
|
|
1265
1339
|
public BundleInfo download(final String url, final String version, final String sessionKey, final String checksum) throws IOException {
|
|
1340
|
+
this.runDownloadGate();
|
|
1266
1341
|
// Check for existing bundle with same version and clean up if in error state
|
|
1267
1342
|
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
1268
|
-
if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted())) {
|
|
1343
|
+
if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted() || existingBundle.isDeleting())) {
|
|
1269
1344
|
logger.info("Found existing failed bundle for version " + version + ", deleting before retry");
|
|
1270
1345
|
this.delete(existingBundle.getId(), true);
|
|
1271
1346
|
}
|
|
@@ -1311,13 +1386,14 @@ public class CapgoUpdater {
|
|
|
1311
1386
|
final String checksum,
|
|
1312
1387
|
final JSONArray manifest
|
|
1313
1388
|
) throws IOException {
|
|
1389
|
+
this.runDownloadGate();
|
|
1314
1390
|
if (manifest == null) {
|
|
1315
1391
|
return download(url, version, sessionKey, checksum);
|
|
1316
1392
|
}
|
|
1317
1393
|
|
|
1318
1394
|
// Check for existing bundle with same version and clean up if in error state
|
|
1319
1395
|
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
1320
|
-
if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted())) {
|
|
1396
|
+
if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted() || existingBundle.isDeleting())) {
|
|
1321
1397
|
logger.info("Found existing failed bundle for version " + version + ", deleting before retry");
|
|
1322
1398
|
this.delete(existingBundle.getId(), true);
|
|
1323
1399
|
}
|
|
@@ -1384,51 +1460,93 @@ public class CapgoUpdater {
|
|
|
1384
1460
|
}
|
|
1385
1461
|
|
|
1386
1462
|
public Boolean delete(final String id, final Boolean removeInfo) throws IOException {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
previewFallback.
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1463
|
+
return this.delete(id, removeInfo, true);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
public Boolean delete(final String id, final Boolean removeInfo, final boolean cancelActiveDownload) throws IOException {
|
|
1467
|
+
synchronized (this.deleteLock) {
|
|
1468
|
+
final BundleInfo deleted = this.getBundleInfo(id);
|
|
1469
|
+
if (deleted.isBuiltin() || this.getCurrentBundleId().equals(id)) {
|
|
1470
|
+
logger.error("Cannot delete current or builtin bundle");
|
|
1471
|
+
logger.debug("Bundle ID: " + id);
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
final BundleInfo previewFallback = this.getPreviewFallbackBundle();
|
|
1475
|
+
if (
|
|
1476
|
+
previewFallback != null &&
|
|
1477
|
+
!previewFallback.isDeleted() &&
|
|
1478
|
+
!previewFallback.isErrorStatus() &&
|
|
1479
|
+
!previewFallback.isDeleting() &&
|
|
1480
|
+
previewFallback.getId().equals(id)
|
|
1481
|
+
) {
|
|
1482
|
+
logger.error("Cannot delete the preview fallback bundle");
|
|
1483
|
+
logger.debug("Bundle ID: " + id);
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
final BundleInfo next = this.getNextBundle();
|
|
1487
|
+
if (next != null && !next.isDeleted() && !next.isErrorStatus() && !next.isDeleting() && next.getId().equals(id)) {
|
|
1488
|
+
logger.error("Cannot delete the next bundle");
|
|
1489
|
+
logger.debug("Bundle ID: " + id);
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
final File bundle = this.getBundleDirectory(id);
|
|
1494
|
+
final boolean hadRegistry = this.hasStoredBundleInfo(id);
|
|
1495
|
+
final boolean hadFolder = bundle.exists();
|
|
1496
|
+
if (!hadRegistry && !hadFolder) {
|
|
1497
|
+
logger.error("Cannot delete unknown bundle");
|
|
1498
|
+
logger.debug("Bundle ID: " + id);
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// Persist DELETING before touching disk so kill/OOM can resume on next launch.
|
|
1503
|
+
if (!deleted.isDeleting()) {
|
|
1504
|
+
if (!this.saveBundleInfo(id, deleted.setStatus(BundleStatus.DELETING))) {
|
|
1505
|
+
logger.error("Failed to persist DELETING marker, aborting disk delete");
|
|
1506
|
+
logger.debug("Bundle ID: " + id);
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Cancel download for this version if active
|
|
1512
|
+
if (cancelActiveDownload && this.activity != null) {
|
|
1513
|
+
DownloadWorkerManager.cancelVersionDownload(this.activity, deleted.getVersionName());
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
if (bundle.exists()) {
|
|
1517
|
+
try {
|
|
1518
|
+
this.deleteDirectory(bundle);
|
|
1519
|
+
} catch (final IOException e) {
|
|
1520
|
+
logger.error("Failed to delete bundle folder, will retry later");
|
|
1521
|
+
logger.debug("Bundle ID: " + id + ", Error: " + e.getMessage());
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Only drop registry after the folder is confirmed gone.
|
|
1527
|
+
if (bundle.exists()) {
|
|
1528
|
+
logger.error("Bundle folder still present after delete, will retry later");
|
|
1529
|
+
logger.debug("Bundle ID: " + id);
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
final boolean finalized;
|
|
1534
|
+
if (Boolean.FALSE.equals(removeInfo)) {
|
|
1535
|
+
finalized = this.saveBundleInfo(id, deleted.setStatus(BundleStatus.DELETED));
|
|
1419
1536
|
} else {
|
|
1420
|
-
this.
|
|
1537
|
+
finalized = this.saveBundleInfo(id, null);
|
|
1538
|
+
}
|
|
1539
|
+
if (!finalized) {
|
|
1540
|
+
logger.error("Failed to finalize delete registry update, will retry later");
|
|
1541
|
+
logger.debug("Bundle ID: " + id);
|
|
1542
|
+
return false;
|
|
1421
1543
|
}
|
|
1544
|
+
this.sendStats("delete", deleted.getVersionName());
|
|
1545
|
+
this.dequeuePendingDelete(id);
|
|
1546
|
+
logger.info("Bundle deleted and confirmed gone");
|
|
1547
|
+
logger.debug("Bundle ID: " + id);
|
|
1422
1548
|
return true;
|
|
1423
1549
|
}
|
|
1424
|
-
logger.info("Bundle not found on disk");
|
|
1425
|
-
logger.debug("Version: " + deleted.getVersionName());
|
|
1426
|
-
// perhaps we did not find the bundle in the files, but if the user requested a delete, we delete
|
|
1427
|
-
if (removeInfo) {
|
|
1428
|
-
this.removeBundleInfo(id);
|
|
1429
|
-
}
|
|
1430
|
-
this.sendStats("delete", deleted.getVersionName());
|
|
1431
|
-
return false;
|
|
1432
1550
|
}
|
|
1433
1551
|
|
|
1434
1552
|
public Boolean delete(final String id) {
|
|
@@ -1441,6 +1559,82 @@ public class CapgoUpdater {
|
|
|
1441
1559
|
}
|
|
1442
1560
|
}
|
|
1443
1561
|
|
|
1562
|
+
/**
|
|
1563
|
+
* Resume incomplete deletes one-by-one. Safe across app kill / OOM because
|
|
1564
|
+
* delete() marks DELETING before disk work and only clears registry after confirm.
|
|
1565
|
+
*/
|
|
1566
|
+
public void drainPendingDeletes() {
|
|
1567
|
+
final LinkedHashSet<String> pendingIds = new LinkedHashSet<>();
|
|
1568
|
+
for (final BundleInfo info : this.list(true)) {
|
|
1569
|
+
if (info != null && info.isDeleting() && info.getId() != null && !info.getId().isEmpty()) {
|
|
1570
|
+
pendingIds.add(info.getId());
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
pendingIds.addAll(this.getPendingDeleteIds());
|
|
1574
|
+
for (final String id : pendingIds) {
|
|
1575
|
+
try {
|
|
1576
|
+
logger.info("Resuming pending delete for bundle: " + id);
|
|
1577
|
+
if (Boolean.TRUE.equals(this.delete(id, true))) {
|
|
1578
|
+
this.dequeuePendingDelete(id);
|
|
1579
|
+
}
|
|
1580
|
+
} catch (final Exception e) {
|
|
1581
|
+
logger.error("Pending delete failed, will retry next launch");
|
|
1582
|
+
logger.debug("Bundle ID: " + id + ", Error: " + e.getMessage());
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
Thread.sleep(DELETE_PACE_MS);
|
|
1586
|
+
} catch (final InterruptedException ie) {
|
|
1587
|
+
Thread.currentThread().interrupt();
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
private Set<String> getPendingDeleteIds() {
|
|
1594
|
+
final Set<String> ids = new LinkedHashSet<>();
|
|
1595
|
+
if (this.prefs == null) {
|
|
1596
|
+
return ids;
|
|
1597
|
+
}
|
|
1598
|
+
final String raw = this.prefs.getString(PENDING_DELETE_IDS, "");
|
|
1599
|
+
if (raw == null || raw.isEmpty()) {
|
|
1600
|
+
return ids;
|
|
1601
|
+
}
|
|
1602
|
+
for (final String part : raw.split(",")) {
|
|
1603
|
+
if (part != null && !part.isEmpty()) {
|
|
1604
|
+
ids.add(part);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return ids;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
private void enqueuePendingDelete(final String id) {
|
|
1611
|
+
if (id == null || id.isEmpty() || this.editor == null || this.prefs == null) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
final Set<String> ids = this.getPendingDeleteIds();
|
|
1615
|
+
if (!ids.add(id)) {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
this.editor.putString(PENDING_DELETE_IDS, String.join(",", ids));
|
|
1619
|
+
this.editor.commit();
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
private void dequeuePendingDelete(final String id) {
|
|
1623
|
+
if (id == null || id.isEmpty() || this.editor == null || this.prefs == null) {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
final Set<String> ids = this.getPendingDeleteIds();
|
|
1627
|
+
if (!ids.remove(id)) {
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (ids.isEmpty()) {
|
|
1631
|
+
this.editor.remove(PENDING_DELETE_IDS);
|
|
1632
|
+
} else {
|
|
1633
|
+
this.editor.putString(PENDING_DELETE_IDS, String.join(",", ids));
|
|
1634
|
+
}
|
|
1635
|
+
this.editor.commit();
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1444
1638
|
private File getBundleDirectory(final String id) {
|
|
1445
1639
|
return new File(this.documentsDir, bundleDirectory + "/" + id);
|
|
1446
1640
|
}
|
|
@@ -1448,7 +1642,13 @@ public class CapgoUpdater {
|
|
|
1448
1642
|
private boolean bundleExists(final String id) {
|
|
1449
1643
|
final File bundle = this.getBundleDirectory(id);
|
|
1450
1644
|
final BundleInfo bundleInfo = this.getBundleInfo(id);
|
|
1451
|
-
return (
|
|
1645
|
+
return (
|
|
1646
|
+
bundle.isDirectory() &&
|
|
1647
|
+
bundle.exists() &&
|
|
1648
|
+
new File(bundle.getPath(), "/index.html").exists() &&
|
|
1649
|
+
!bundleInfo.isDeleted() &&
|
|
1650
|
+
!bundleInfo.isDeleting()
|
|
1651
|
+
);
|
|
1452
1652
|
}
|
|
1453
1653
|
|
|
1454
1654
|
static final class ResetState {
|
|
@@ -1627,21 +1827,51 @@ public class CapgoUpdater {
|
|
|
1627
1827
|
// Only attempt to delete when the fallback is a different bundle than the
|
|
1628
1828
|
// currently loaded one. Otherwise we spam logs with "Cannot delete <id>"
|
|
1629
1829
|
// because delete() protects the current bundle from removal.
|
|
1630
|
-
|
|
1631
|
-
|
|
1830
|
+
final String previousFallbackId = fallback.getId();
|
|
1831
|
+
final String previousFallbackVersion = fallback.getVersionName();
|
|
1832
|
+
final BundleInfo nextBundle = this.getNextBundle();
|
|
1833
|
+
final boolean previousIsNext =
|
|
1834
|
+
nextBundle != null &&
|
|
1835
|
+
previousFallbackId != null &&
|
|
1836
|
+
previousFallbackId.equals(nextBundle.getId()) &&
|
|
1837
|
+
!nextBundle.isDeleted() &&
|
|
1838
|
+
!nextBundle.isErrorStatus() &&
|
|
1839
|
+
!nextBundle.isDeleting();
|
|
1840
|
+
final boolean shouldDeletePrevious =
|
|
1841
|
+
Boolean.TRUE.equals(autoDeletePrevious) &&
|
|
1632
1842
|
!fallback.isBuiltin() &&
|
|
1633
|
-
|
|
1634
|
-
!
|
|
1635
|
-
!fallbackIsPreviewFallback
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1843
|
+
previousFallbackId != null &&
|
|
1844
|
+
!previousFallbackId.equals(bundle.getId()) &&
|
|
1845
|
+
!fallbackIsPreviewFallback &&
|
|
1846
|
+
!previousIsNext;
|
|
1847
|
+
if (shouldDeletePrevious) {
|
|
1848
|
+
// Cancel any in-flight download for the old version before the async boundary
|
|
1849
|
+
// so a later download of the same version name is not cancelled mid-flight.
|
|
1850
|
+
if (this.activity != null) {
|
|
1851
|
+
DownloadWorkerManager.cancelVersionDownload(this.activity, previousFallbackVersion);
|
|
1852
|
+
}
|
|
1853
|
+
// Mark durable intent before fallback switch so a kill mid-flight still retries.
|
|
1854
|
+
if (!this.saveBundleInfo(previousFallbackId, fallback.setStatus(BundleStatus.DELETING))) {
|
|
1855
|
+
logger.error("Failed to persist DELETING for previous bundle; queueing durable retry");
|
|
1856
|
+
logger.debug("Bundle ID: " + previousFallbackId);
|
|
1857
|
+
this.enqueuePendingDelete(previousFallbackId);
|
|
1642
1858
|
}
|
|
1643
1859
|
}
|
|
1644
1860
|
this.setFallbackBundle(bundle);
|
|
1861
|
+
if (shouldDeletePrevious) {
|
|
1862
|
+
new Thread(() -> {
|
|
1863
|
+
try {
|
|
1864
|
+
final Boolean res = this.delete(previousFallbackId, true, false);
|
|
1865
|
+
if (Boolean.TRUE.equals(res)) {
|
|
1866
|
+
logger.info("Deleted previous bundle: " + previousFallbackVersion);
|
|
1867
|
+
} else {
|
|
1868
|
+
logger.debug("Previous bundle delete incomplete, will retry: " + previousFallbackId);
|
|
1869
|
+
}
|
|
1870
|
+
} catch (final IOException e) {
|
|
1871
|
+
logger.error("Failed to delete previous bundle: " + previousFallbackId + " " + e.getMessage());
|
|
1872
|
+
}
|
|
1873
|
+
}, "CapgoUpdater-autoDeletePrevious").start();
|
|
1874
|
+
}
|
|
1645
1875
|
}
|
|
1646
1876
|
|
|
1647
1877
|
public void setError(final BundleInfo bundle) {
|
|
@@ -1964,7 +2194,6 @@ public class CapgoUpdater {
|
|
|
1964
2194
|
logger.info("Public channel requested, channel override removed");
|
|
1965
2195
|
callback.callback(res);
|
|
1966
2196
|
} else {
|
|
1967
|
-
// Success - persist defaultChannel
|
|
1968
2197
|
this.defaultChannel = channel;
|
|
1969
2198
|
editor.putString(defaultChannelKey, channel);
|
|
1970
2199
|
editor.apply();
|
|
@@ -2484,10 +2713,10 @@ public class CapgoUpdater {
|
|
|
2484
2713
|
this.saveBundleInfo(id, null);
|
|
2485
2714
|
}
|
|
2486
2715
|
|
|
2487
|
-
public
|
|
2716
|
+
public boolean saveBundleInfo(final String id, final BundleInfo info) {
|
|
2488
2717
|
if (id == null || (info != null && (info.isBuiltin() || info.isUnknown()))) {
|
|
2489
2718
|
logger.debug("Not saving info for bundle: [" + id + "] " + info);
|
|
2490
|
-
return;
|
|
2719
|
+
return false;
|
|
2491
2720
|
}
|
|
2492
2721
|
|
|
2493
2722
|
if (info == null) {
|
|
@@ -2499,7 +2728,7 @@ public class CapgoUpdater {
|
|
|
2499
2728
|
logger.debug("Storing info for bundle [" + id + "] " + update.getClass().getName() + " -> " + jsonString);
|
|
2500
2729
|
this.editor.putString(id + INFO_SUFFIX, jsonString);
|
|
2501
2730
|
}
|
|
2502
|
-
this.editor.commit();
|
|
2731
|
+
return this.editor.commit();
|
|
2503
2732
|
}
|
|
2504
2733
|
|
|
2505
2734
|
private void setBundleStatus(final String id, final BundleStatus status) {
|
|
@@ -40,7 +40,7 @@ public class DelayCondition {
|
|
|
40
40
|
public boolean equals(Object o) {
|
|
41
41
|
if (this == o) return true;
|
|
42
42
|
if (!(o instanceof DelayCondition that)) return false;
|
|
43
|
-
return
|
|
43
|
+
return getKind() == that.getKind() && Objects.equals(getValue(), that.getValue());
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
@Override
|
|
@@ -51,6 +51,6 @@ public class DelayCondition {
|
|
|
51
51
|
@NonNull
|
|
52
52
|
@Override
|
|
53
53
|
public String toString() {
|
|
54
|
-
return
|
|
54
|
+
return "DelayCondition{" + "kind=" + kind + ", value='" + value + '\'' + '}';
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -22,7 +22,7 @@ public class InternalUtils {
|
|
|
22
22
|
public static String getPackageName(PackageManager pm, String packageName) {
|
|
23
23
|
try {
|
|
24
24
|
PackageInfo pInfo = getPackageInfoInternal(pm, packageName);
|
|
25
|
-
return
|
|
25
|
+
return pInfo != null ? pInfo.packageName : null;
|
|
26
26
|
} catch (PackageManager.NameNotFoundException e) {
|
|
27
27
|
// Exception is handled internally, and null is returned to indicate the package name could not be retrieved
|
|
28
28
|
return null;
|
|
@@ -273,7 +273,7 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
273
273
|
String name = preview.optString("name", "");
|
|
274
274
|
JSObject bundle = preview.getJSObject("bundle");
|
|
275
275
|
String version = bundle == null ? "" : bundle.optString("version", "");
|
|
276
|
-
String label = !name.isEmpty() ? name :
|
|
276
|
+
String label = !name.isEmpty() ? name : !version.isEmpty() ? version : preview.optString("id", "Preview");
|
|
277
277
|
if (preview.optBoolean("isActive", false)) {
|
|
278
278
|
label += " (current)";
|
|
279
279
|
}
|
package/dist/docs.json
CHANGED
|
@@ -846,7 +846,7 @@
|
|
|
846
846
|
"text": "4.7.0"
|
|
847
847
|
}
|
|
848
848
|
],
|
|
849
|
-
"docs": "Assign this device to a specific update channel at runtime.\n\nChannels allow you to distribute different bundle versions to different groups of users\n(e.g., \"production\", \"beta\", \"staging\"). This method switches the device to a new channel.\n\n**Device Override UI:** `setChannel()` validates the channel with the backend, then stores the\nselected channel locally on the device. It does not create or update
|
|
849
|
+
"docs": "Assign this device to a specific update channel at runtime.\n\nChannels allow you to distribute different bundle versions to different groups of users\n(e.g., \"production\", \"beta\", \"staging\"). This method switches the device to a new channel.\n\n**Device Override UI:** `setChannel()` validates the channel with the backend, then stores the\nselected channel locally on the device for future app restarts. It does not create or update\na backend Device Override, so the device will not appear as overridden in the Capgo dashboard.\nOnly assignments created from the dashboard or the Public API are shown in the Device Override UI.\n\n**Requirements:**\n- The target channel must allow self-assignment (configured in your Capgo dashboard or backend)\n- The backend may accept or reject the request based on channel settings\n\n**When to use:**\n- After the app is ready and the user has interacted (e.g., opted into beta program)\n- To implement in-app channel switching (beta toggle, tester access, etc.)\n- For user-driven channel changes\n\n**When NOT to use:**\n- At app boot/initialization - use {@link PluginsConfig.CapacitorUpdater.defaultChannel} config instead\n- Before user interaction\n\n**Important: Listen for the `channelPrivate` event**\n\nWhen a user attempts to set a channel that doesn't allow device self-assignment, the method will\nthrow an error AND fire a {@link addListener}('channelPrivate') event. You should listen to this event\nto provide appropriate feedback to users:\n\n```typescript\nCapacitorUpdater.addListener('channelPrivate', (data) => {\n console.warn(`Cannot access channel \"${data.channel}\": ${data.message}`);\n // Show user-friendly message\n});\n```\n\nThis sends a request to the Capgo backend to validate the specified channel, then stores the\nchannel locally on the device for future app restarts.",
|
|
850
850
|
"complexTypes": [
|
|
851
851
|
"ChannelRes",
|
|
852
852
|
"SetChannelOptions"
|
|
@@ -911,7 +911,7 @@
|
|
|
911
911
|
"text": "4.8.0"
|
|
912
912
|
}
|
|
913
913
|
],
|
|
914
|
-
"docs": "Get the current channel assigned to this device.\n\nReturns information about:\n- `channel`: The currently assigned channel name (if any)\n- `allowSet`: Whether the channel allows self-assignment\n- `status`: Operation status\n- `error`/`message`: Additional information (if applicable)\n\nUse this to:\n- Display current channel to users (e.g., \"You're on the Beta channel\")\n- Check if a device is on a specific channel before showing features\n- Verify channel assignment after calling {@link setChannel}\n\nOn native platforms, a successful response also refreshes the
|
|
914
|
+
"docs": "Get the current channel assigned to this device.\n\nReturns information about:\n- `channel`: The currently assigned channel name (if any)\n- `allowSet`: Whether the channel allows self-assignment\n- `status`: Operation status\n- `error`/`message`: Additional information (if applicable)\n\nUse this to:\n- Display current channel to users (e.g., \"You're on the Beta channel\")\n- Check if a device is on a specific channel before showing features\n- Verify channel assignment after calling {@link setChannel}\n\nOn native platforms, a successful response also refreshes the default channel used by update checks.\nThis refresh is persisted across app restarts.",
|
|
915
915
|
"complexTypes": [
|
|
916
916
|
"GetChannelRes"
|
|
917
917
|
],
|
|
@@ -4468,6 +4468,14 @@
|
|
|
4468
4468
|
{
|
|
4469
4469
|
"text": "'downloading'",
|
|
4470
4470
|
"complexTypes": []
|
|
4471
|
+
},
|
|
4472
|
+
{
|
|
4473
|
+
"text": "'deleted'",
|
|
4474
|
+
"complexTypes": []
|
|
4475
|
+
},
|
|
4476
|
+
{
|
|
4477
|
+
"text": "'deleting'",
|
|
4478
|
+
"complexTypes": []
|
|
4471
4479
|
}
|
|
4472
4480
|
]
|
|
4473
4481
|
},
|
|
@@ -5035,6 +5043,22 @@
|
|
|
5035
5043
|
"complexTypes": [],
|
|
5036
5044
|
"type": "boolean | undefined"
|
|
5037
5045
|
},
|
|
5046
|
+
{
|
|
5047
|
+
"name": "persistDefaultChannelOnReinstall",
|
|
5048
|
+
"tags": [
|
|
5049
|
+
{
|
|
5050
|
+
"text": "true",
|
|
5051
|
+
"name": "default"
|
|
5052
|
+
},
|
|
5053
|
+
{
|
|
5054
|
+
"text": "8.51.0",
|
|
5055
|
+
"name": "since"
|
|
5056
|
+
}
|
|
5057
|
+
],
|
|
5058
|
+
"docs": "Keep the default channel stored by {@link CapacitorUpdaterPlugin.setChannel} or refreshed by\n{@link CapacitorUpdaterPlugin.getChannel} when app data is restored into a new app install.\n\n`setChannel()` and a successful `getChannel()` still persist the selected channel across app\nrestarts. When this option is `false`, native startup clears that persisted channel when it\ndetects app data restored into a new installation. Native build cleanup clears the persisted\nchannel only when `persistDefaultChannelOnReinstall` is `false`, `resetWhenUpdate` is `true`,\nand the native build version has changed.\n\nOnly available for Android and iOS.",
|
|
5059
|
+
"complexTypes": [],
|
|
5060
|
+
"type": "boolean | undefined"
|
|
5061
|
+
},
|
|
5038
5062
|
{
|
|
5039
5063
|
"name": "defaultChannel",
|
|
5040
5064
|
"tags": [
|
|
@@ -311,6 +311,22 @@ declare module '@capacitor/cli' {
|
|
|
311
311
|
* @since 7.34.0
|
|
312
312
|
*/
|
|
313
313
|
allowSetDefaultChannel?: boolean;
|
|
314
|
+
/**
|
|
315
|
+
* Keep the default channel stored by {@link CapacitorUpdaterPlugin.setChannel} or refreshed by
|
|
316
|
+
* {@link CapacitorUpdaterPlugin.getChannel} when app data is restored into a new app install.
|
|
317
|
+
*
|
|
318
|
+
* `setChannel()` and a successful `getChannel()` still persist the selected channel across app
|
|
319
|
+
* restarts. When this option is `false`, native startup clears that persisted channel when it
|
|
320
|
+
* detects app data restored into a new installation. Native build cleanup clears the persisted
|
|
321
|
+
* channel only when `persistDefaultChannelOnReinstall` is `false`, `resetWhenUpdate` is `true`,
|
|
322
|
+
* and the native build version has changed.
|
|
323
|
+
*
|
|
324
|
+
* Only available for Android and iOS.
|
|
325
|
+
*
|
|
326
|
+
* @default true
|
|
327
|
+
* @since 8.51.0
|
|
328
|
+
*/
|
|
329
|
+
persistDefaultChannelOnReinstall?: boolean;
|
|
314
330
|
/**
|
|
315
331
|
* Set the default channel for the app in the config. Case sensitive.
|
|
316
332
|
* This will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud.
|
|
@@ -947,9 +963,9 @@ export interface CapacitorUpdaterPlugin {
|
|
|
947
963
|
* (e.g., "production", "beta", "staging"). This method switches the device to a new channel.
|
|
948
964
|
*
|
|
949
965
|
* **Device Override UI:** `setChannel()` validates the channel with the backend, then stores the
|
|
950
|
-
* selected channel locally on the device. It does not create or update
|
|
951
|
-
* so the device will not appear as overridden in the Capgo dashboard.
|
|
952
|
-
* from the dashboard or the Public API are shown in the Device Override UI.
|
|
966
|
+
* selected channel locally on the device for future app restarts. It does not create or update
|
|
967
|
+
* a backend Device Override, so the device will not appear as overridden in the Capgo dashboard.
|
|
968
|
+
* Only assignments created from the dashboard or the Public API are shown in the Device Override UI.
|
|
953
969
|
*
|
|
954
970
|
* **Requirements:**
|
|
955
971
|
* - The target channel must allow self-assignment (configured in your Capgo dashboard or backend)
|
|
@@ -978,7 +994,7 @@ export interface CapacitorUpdaterPlugin {
|
|
|
978
994
|
* ```
|
|
979
995
|
*
|
|
980
996
|
* This sends a request to the Capgo backend to validate the specified channel, then stores the
|
|
981
|
-
* channel locally on the device.
|
|
997
|
+
* channel locally on the device for future app restarts.
|
|
982
998
|
*
|
|
983
999
|
* @param options The {@link SetChannelOptions} containing the channel name and optional auto-update trigger.
|
|
984
1000
|
* @returns {Promise<ChannelRes>} Channel operation result with status and optional error/message.
|
|
@@ -1019,8 +1035,8 @@ export interface CapacitorUpdaterPlugin {
|
|
|
1019
1035
|
* - Check if a device is on a specific channel before showing features
|
|
1020
1036
|
* - Verify channel assignment after calling {@link setChannel}
|
|
1021
1037
|
*
|
|
1022
|
-
* On native platforms, a successful response also refreshes the
|
|
1023
|
-
*
|
|
1038
|
+
* On native platforms, a successful response also refreshes the default channel used by update checks.
|
|
1039
|
+
* This refresh is persisted across app restarts.
|
|
1024
1040
|
*
|
|
1025
1041
|
* @returns {Promise<GetChannelRes>} The current channel information.
|
|
1026
1042
|
* @throws {Error} If the operation fails.
|
|
@@ -1604,7 +1620,7 @@ export interface CapacitorUpdaterPlugin {
|
|
|
1604
1620
|
* success: The bundle has been downloaded and is ready to be **SET** as the next bundle.
|
|
1605
1621
|
* error: The bundle has failed to download.
|
|
1606
1622
|
*/
|
|
1607
|
-
export type BundleStatus = 'success' | 'error' | 'pending' | 'downloading';
|
|
1623
|
+
export type BundleStatus = 'success' | 'error' | 'pending' | 'downloading' | 'deleted' | 'deleting';
|
|
1608
1624
|
export type DelayUntilNext = 'background' | 'kill' | 'nativeVersion' | 'date';
|
|
1609
1625
|
/**
|
|
1610
1626
|
* Classification for update-check responses that do not provide a downloadable bundle.
|