@capgo/capacitor-updater 8.41.13 → 8.42.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.
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +84 -5
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +74 -11
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +22 -0
- package/package.json +1 -1
|
@@ -84,7 +84,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
84
84
|
private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
|
|
85
85
|
private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
|
|
86
86
|
|
|
87
|
-
private final String pluginVersion = "8.
|
|
87
|
+
private final String pluginVersion = "8.42.0";
|
|
88
88
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
89
89
|
|
|
90
90
|
private SharedPreferences.Editor editor;
|
|
@@ -35,8 +35,11 @@ import java.util.Objects;
|
|
|
35
35
|
import java.util.Set;
|
|
36
36
|
import java.util.concurrent.CompletableFuture;
|
|
37
37
|
import java.util.concurrent.ConcurrentHashMap;
|
|
38
|
+
import java.util.concurrent.CopyOnWriteArrayList;
|
|
38
39
|
import java.util.concurrent.ExecutorService;
|
|
39
40
|
import java.util.concurrent.Executors;
|
|
41
|
+
import java.util.concurrent.ScheduledExecutorService;
|
|
42
|
+
import java.util.concurrent.ScheduledFuture;
|
|
40
43
|
import java.util.concurrent.TimeUnit;
|
|
41
44
|
import java.util.zip.ZipEntry;
|
|
42
45
|
import java.util.zip.ZipInputStream;
|
|
@@ -91,6 +94,12 @@ public class CapgoUpdater {
|
|
|
91
94
|
// Flag to track if we've already sent the rate limit statistic - prevents infinite loop
|
|
92
95
|
private static volatile boolean rateLimitStatisticSent = false;
|
|
93
96
|
|
|
97
|
+
// Stats batching - queue events and send max once per second
|
|
98
|
+
private final List<JSONObject> statsQueue = new CopyOnWriteArrayList<>();
|
|
99
|
+
private final ScheduledExecutorService statsScheduler = Executors.newSingleThreadScheduledExecutor();
|
|
100
|
+
private ScheduledFuture<?> statsFlushTask = null;
|
|
101
|
+
private static final long STATS_FLUSH_INTERVAL_MS = 1000;
|
|
102
|
+
|
|
94
103
|
private final Map<String, CompletableFuture<BundleInfo>> downloadFutures = new ConcurrentHashMap<>();
|
|
95
104
|
private final ExecutorService io = Executors.newSingleThreadExecutor();
|
|
96
105
|
|
|
@@ -1450,30 +1459,74 @@ public class CapgoUpdater {
|
|
|
1450
1459
|
if (statsUrl == null || statsUrl.isEmpty()) {
|
|
1451
1460
|
return;
|
|
1452
1461
|
}
|
|
1462
|
+
|
|
1453
1463
|
JSONObject json;
|
|
1454
1464
|
try {
|
|
1455
1465
|
json = this.createInfoObject();
|
|
1456
1466
|
json.put("version_name", versionName);
|
|
1457
1467
|
json.put("old_version_name", oldVersionName);
|
|
1458
1468
|
json.put("action", action);
|
|
1469
|
+
json.put("timestamp", System.currentTimeMillis());
|
|
1459
1470
|
} catch (JSONException e) {
|
|
1460
1471
|
logger.error("Error preparing stats");
|
|
1461
1472
|
logger.debug("JSONException: " + e.getMessage());
|
|
1462
1473
|
return;
|
|
1463
1474
|
}
|
|
1464
1475
|
|
|
1476
|
+
statsQueue.add(json);
|
|
1477
|
+
ensureStatsTimerStarted();
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private synchronized void ensureStatsTimerStarted() {
|
|
1481
|
+
if (statsFlushTask == null || statsFlushTask.isCancelled() || statsFlushTask.isDone()) {
|
|
1482
|
+
statsFlushTask = statsScheduler.scheduleAtFixedRate(
|
|
1483
|
+
this::flushStatsQueue,
|
|
1484
|
+
STATS_FLUSH_INTERVAL_MS,
|
|
1485
|
+
STATS_FLUSH_INTERVAL_MS,
|
|
1486
|
+
TimeUnit.MILLISECONDS
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
private void flushStatsQueue() {
|
|
1492
|
+
if (statsQueue.isEmpty()) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
String statsUrl = this.statsUrl;
|
|
1497
|
+
if (statsUrl == null || statsUrl.isEmpty()) {
|
|
1498
|
+
statsQueue.clear();
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// Copy and clear the queue atomically using synchronized block
|
|
1503
|
+
List<JSONObject> eventsToSend;
|
|
1504
|
+
synchronized (statsQueue) {
|
|
1505
|
+
if (statsQueue.isEmpty()) {
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
eventsToSend = new ArrayList<>(statsQueue);
|
|
1509
|
+
statsQueue.clear();
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
JSONArray jsonArray = new JSONArray();
|
|
1513
|
+
for (JSONObject event : eventsToSend) {
|
|
1514
|
+
jsonArray.put(event);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1465
1517
|
Request request = new Request.Builder()
|
|
1466
1518
|
.url(statsUrl)
|
|
1467
|
-
.post(RequestBody.create(
|
|
1519
|
+
.post(RequestBody.create(jsonArray.toString(), MediaType.get("application/json")))
|
|
1468
1520
|
.build();
|
|
1469
1521
|
|
|
1522
|
+
final int eventCount = eventsToSend.size();
|
|
1470
1523
|
DownloadService.sharedClient
|
|
1471
1524
|
.newCall(request)
|
|
1472
1525
|
.enqueue(
|
|
1473
1526
|
new okhttp3.Callback() {
|
|
1474
1527
|
@Override
|
|
1475
1528
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
1476
|
-
logger.error("Failed to send stats");
|
|
1529
|
+
logger.error("Failed to send stats batch");
|
|
1477
1530
|
logger.debug("Error: " + e.getMessage());
|
|
1478
1531
|
}
|
|
1479
1532
|
|
|
@@ -1486,10 +1539,10 @@ public class CapgoUpdater {
|
|
|
1486
1539
|
}
|
|
1487
1540
|
|
|
1488
1541
|
if (response.isSuccessful()) {
|
|
1489
|
-
logger.info("Stats sent successfully");
|
|
1490
|
-
logger.debug("
|
|
1542
|
+
logger.info("Stats batch sent successfully");
|
|
1543
|
+
logger.debug("Sent " + eventCount + " events");
|
|
1491
1544
|
} else {
|
|
1492
|
-
logger.error("Error sending stats");
|
|
1545
|
+
logger.error("Error sending stats batch");
|
|
1493
1546
|
logger.debug("Response code: " + response.code());
|
|
1494
1547
|
}
|
|
1495
1548
|
}
|
|
@@ -1623,4 +1676,30 @@ public class CapgoUpdater {
|
|
|
1623
1676
|
this.editor.commit();
|
|
1624
1677
|
return true;
|
|
1625
1678
|
}
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* Shuts down the stats scheduler and flushes any pending stats.
|
|
1682
|
+
* Should be called when the plugin is destroyed to prevent resource leaks.
|
|
1683
|
+
*/
|
|
1684
|
+
public void shutdown() {
|
|
1685
|
+
// Cancel the scheduled task
|
|
1686
|
+
if (statsFlushTask != null) {
|
|
1687
|
+
statsFlushTask.cancel(false);
|
|
1688
|
+
statsFlushTask = null;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// Flush any remaining stats before shutdown
|
|
1692
|
+
flushStatsQueue();
|
|
1693
|
+
|
|
1694
|
+
// Shutdown the scheduler
|
|
1695
|
+
statsScheduler.shutdown();
|
|
1696
|
+
try {
|
|
1697
|
+
if (!statsScheduler.awaitTermination(2, TimeUnit.SECONDS)) {
|
|
1698
|
+
statsScheduler.shutdownNow();
|
|
1699
|
+
}
|
|
1700
|
+
} catch (InterruptedException e) {
|
|
1701
|
+
statsScheduler.shutdownNow();
|
|
1702
|
+
Thread.currentThread().interrupt();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1626
1705
|
}
|
|
@@ -71,7 +71,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
71
71
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
72
72
|
]
|
|
73
73
|
public var implementation = CapgoUpdater()
|
|
74
|
-
private let pluginVersion: String = "8.
|
|
74
|
+
private let pluginVersion: String = "8.42.0"
|
|
75
75
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
76
76
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
77
77
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -48,6 +48,12 @@ import UIKit
|
|
|
48
48
|
// Flag to track if we've already sent the rate limit statistic - prevents infinite loop
|
|
49
49
|
private static var rateLimitStatisticSent = false
|
|
50
50
|
|
|
51
|
+
// Stats batching - queue events and send max once per second
|
|
52
|
+
private var statsQueue: [StatsEvent] = []
|
|
53
|
+
private let statsQueueLock = NSLock()
|
|
54
|
+
private var statsFlushTimer: Timer?
|
|
55
|
+
private static let statsFlushInterval: TimeInterval = 1.0
|
|
56
|
+
|
|
51
57
|
private var userAgent: String {
|
|
52
58
|
let safePluginVersion = pluginVersion.isEmpty ? "unknown" : pluginVersion
|
|
53
59
|
let safeAppId = appId.isEmpty ? "unknown" : appId
|
|
@@ -70,6 +76,15 @@ import UIKit
|
|
|
70
76
|
self.logger = logger
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
deinit {
|
|
80
|
+
// Invalidate the stats timer to prevent memory leaks
|
|
81
|
+
statsFlushTimer?.invalidate()
|
|
82
|
+
statsFlushTimer = nil
|
|
83
|
+
|
|
84
|
+
// Flush any remaining stats before deallocation
|
|
85
|
+
flushStatsQueue()
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
private func calcTotalPercent(percent: Int, min: Int, max: Int) -> Int {
|
|
74
89
|
return (percent * (max - min)) / 100 + min
|
|
75
90
|
}
|
|
@@ -1675,21 +1690,70 @@ import UIKit
|
|
|
1675
1690
|
guard !statsUrl.isEmpty else {
|
|
1676
1691
|
return
|
|
1677
1692
|
}
|
|
1678
|
-
operationQueue.maxConcurrentOperationCount = 1
|
|
1679
1693
|
|
|
1680
|
-
let
|
|
1694
|
+
let resolvedVersionName = versionName ?? getCurrentBundle().getVersionName()
|
|
1695
|
+
let info = createInfoObject()
|
|
1696
|
+
|
|
1697
|
+
let event = StatsEvent(
|
|
1698
|
+
platform: info.platform,
|
|
1699
|
+
device_id: info.device_id,
|
|
1700
|
+
app_id: info.app_id,
|
|
1701
|
+
custom_id: info.custom_id,
|
|
1702
|
+
version_build: info.version_build,
|
|
1703
|
+
version_code: info.version_code,
|
|
1704
|
+
version_os: info.version_os,
|
|
1705
|
+
version_name: resolvedVersionName,
|
|
1706
|
+
old_version_name: oldVersionName ?? "",
|
|
1707
|
+
plugin_version: info.plugin_version,
|
|
1708
|
+
is_emulator: info.is_emulator,
|
|
1709
|
+
is_prod: info.is_prod,
|
|
1710
|
+
action: action,
|
|
1711
|
+
channel: info.channel,
|
|
1712
|
+
defaultChannel: info.defaultChannel,
|
|
1713
|
+
key_id: info.key_id,
|
|
1714
|
+
timestamp: Int64(Date().timeIntervalSince1970 * 1000)
|
|
1715
|
+
)
|
|
1681
1716
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1717
|
+
statsQueueLock.lock()
|
|
1718
|
+
statsQueue.append(event)
|
|
1719
|
+
statsQueueLock.unlock()
|
|
1720
|
+
|
|
1721
|
+
ensureStatsTimerStarted()
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
private func ensureStatsTimerStarted() {
|
|
1725
|
+
DispatchQueue.main.async { [weak self] in
|
|
1726
|
+
guard let self = self else { return }
|
|
1727
|
+
if self.statsFlushTimer == nil || !self.statsFlushTimer!.isValid {
|
|
1728
|
+
// Use closure-based timer to avoid strong reference cycle
|
|
1729
|
+
self.statsFlushTimer = Timer.scheduledTimer(
|
|
1730
|
+
withTimeInterval: CapgoUpdater.statsFlushInterval,
|
|
1731
|
+
repeats: true
|
|
1732
|
+
) { [weak self] _ in
|
|
1733
|
+
self?.flushStatsQueue()
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
private func flushStatsQueue() {
|
|
1740
|
+
statsQueueLock.lock()
|
|
1741
|
+
guard !statsQueue.isEmpty else {
|
|
1742
|
+
statsQueueLock.unlock()
|
|
1743
|
+
return
|
|
1744
|
+
}
|
|
1745
|
+
let eventsToSend = statsQueue
|
|
1746
|
+
statsQueue.removeAll()
|
|
1747
|
+
statsQueueLock.unlock()
|
|
1748
|
+
|
|
1749
|
+
operationQueue.maxConcurrentOperationCount = 1
|
|
1686
1750
|
|
|
1687
1751
|
let operation = BlockOperation {
|
|
1688
1752
|
let semaphore = DispatchSemaphore(value: 0)
|
|
1689
1753
|
self.alamofireSession.request(
|
|
1690
1754
|
self.statsUrl,
|
|
1691
1755
|
method: .post,
|
|
1692
|
-
parameters:
|
|
1756
|
+
parameters: eventsToSend,
|
|
1693
1757
|
encoder: JSONParameterEncoder.default,
|
|
1694
1758
|
requestModifier: { $0.timeoutInterval = self.timeout }
|
|
1695
1759
|
).responseData { response in
|
|
@@ -1701,10 +1765,10 @@ import UIKit
|
|
|
1701
1765
|
|
|
1702
1766
|
switch response.result {
|
|
1703
1767
|
case .success:
|
|
1704
|
-
self.logger.info("Stats sent successfully")
|
|
1705
|
-
self.logger.debug("
|
|
1768
|
+
self.logger.info("Stats batch sent successfully")
|
|
1769
|
+
self.logger.debug("Sent \(eventsToSend.count) events")
|
|
1706
1770
|
case let .failure(error):
|
|
1707
|
-
self.logger.error("Error sending stats")
|
|
1771
|
+
self.logger.error("Error sending stats batch")
|
|
1708
1772
|
self.logger.debug("Response: \(response.value?.debugDescription ?? "nil"), Error: \(error.localizedDescription)")
|
|
1709
1773
|
}
|
|
1710
1774
|
semaphore.signal()
|
|
@@ -1712,7 +1776,6 @@ import UIKit
|
|
|
1712
1776
|
semaphore.wait()
|
|
1713
1777
|
}
|
|
1714
1778
|
operationQueue.addOperation(operation)
|
|
1715
|
-
|
|
1716
1779
|
}
|
|
1717
1780
|
|
|
1718
1781
|
public func getBundleInfo(id: String?) -> BundleInfo {
|
|
@@ -135,6 +135,28 @@ struct InfoObject: Codable {
|
|
|
135
135
|
}
|
|
136
136
|
// swiftlint:enable identifier_name
|
|
137
137
|
|
|
138
|
+
// swiftlint:disable identifier_name
|
|
139
|
+
struct StatsEvent: Codable {
|
|
140
|
+
let platform: String?
|
|
141
|
+
let device_id: String?
|
|
142
|
+
let app_id: String?
|
|
143
|
+
let custom_id: String?
|
|
144
|
+
let version_build: String?
|
|
145
|
+
let version_code: String?
|
|
146
|
+
let version_os: String?
|
|
147
|
+
let version_name: String?
|
|
148
|
+
let old_version_name: String?
|
|
149
|
+
let plugin_version: String?
|
|
150
|
+
let is_emulator: Bool?
|
|
151
|
+
let is_prod: Bool?
|
|
152
|
+
let action: String?
|
|
153
|
+
let channel: String?
|
|
154
|
+
let defaultChannel: String?
|
|
155
|
+
let key_id: String?
|
|
156
|
+
let timestamp: Int64
|
|
157
|
+
}
|
|
158
|
+
// swiftlint:enable identifier_name
|
|
159
|
+
|
|
138
160
|
// swiftlint:disable identifier_name
|
|
139
161
|
public struct ManifestEntry: Codable {
|
|
140
162
|
let file_name: String?
|