@capgo/cordova-updater 8.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 (76) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +82 -0
  3. package/dist/esm/definitions.d.ts +2684 -0
  4. package/dist/esm/definitions.js +109 -0
  5. package/dist/esm/definitions.js.map +1 -0
  6. package/dist/esm/exec.d.ts +4 -0
  7. package/dist/esm/exec.js +25 -0
  8. package/dist/esm/exec.js.map +1 -0
  9. package/dist/esm/history.d.ts +1 -0
  10. package/dist/esm/history.js +283 -0
  11. package/dist/esm/history.js.map +1 -0
  12. package/dist/esm/index.d.ts +57 -0
  13. package/dist/esm/index.js +68 -0
  14. package/dist/esm/index.js.map +1 -0
  15. package/dist/esm/plugin.d.ts +57 -0
  16. package/dist/esm/plugin.js +15 -0
  17. package/dist/esm/plugin.js.map +1 -0
  18. package/dist/plugin.cjs.js +217 -0
  19. package/dist/plugin.cjs.js.map +1 -0
  20. package/dist/plugin.js +222 -0
  21. package/dist/plugin.js.map +1 -0
  22. package/package.json +96 -0
  23. package/plugin.xml +207 -0
  24. package/src/android/CordovaUpdaterPlugin.java +5662 -0
  25. package/src/android/UpdaterPathHandler.java +99 -0
  26. package/src/android/app/capgo/cordova/updater/AndroidAppExitReporter.java +92 -0
  27. package/src/android/app/capgo/cordova/updater/AppLifecycleObserver.java +115 -0
  28. package/src/android/app/capgo/cordova/updater/BundleInfo.java +246 -0
  29. package/src/android/app/capgo/cordova/updater/BundleStatus.java +45 -0
  30. package/src/android/app/capgo/cordova/updater/Callback.java +13 -0
  31. package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +3404 -0
  32. package/src/android/app/capgo/cordova/updater/CordovaUpdaterConfig.java +56 -0
  33. package/src/android/app/capgo/cordova/updater/CryptoCipher.java +462 -0
  34. package/src/android/app/capgo/cordova/updater/DataManager.java +44 -0
  35. package/src/android/app/capgo/cordova/updater/DelayCondition.java +56 -0
  36. package/src/android/app/capgo/cordova/updater/DelayUntilNext.java +14 -0
  37. package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +307 -0
  38. package/src/android/app/capgo/cordova/updater/DeviceIdHelper.java +230 -0
  39. package/src/android/app/capgo/cordova/updater/DownloadService.java +1219 -0
  40. package/src/android/app/capgo/cordova/updater/DownloadWorkerManager.java +260 -0
  41. package/src/android/app/capgo/cordova/updater/InternalUtils.java +54 -0
  42. package/src/android/app/capgo/cordova/updater/Logger.java +345 -0
  43. package/src/android/app/capgo/cordova/updater/ShakeDetector.java +72 -0
  44. package/src/android/app/capgo/cordova/updater/ShakeMenu.java +39 -0
  45. package/src/android/app/capgo/cordova/updater/ThreeFingerPinchDetector.java +323 -0
  46. package/src/android/app/capgo/cordova/updater/compat/JSArray.java +9 -0
  47. package/src/android/app/capgo/cordova/updater/compat/JSObject.java +83 -0
  48. package/src/android/app/capgo/cordova/updater/compat/PluginCall.java +165 -0
  49. package/src/android/capgo-cordova-updater.gradle +19 -0
  50. package/src/android/capgo-updater.xml +3 -0
  51. package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +3039 -0
  52. package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +177 -0
  53. package/src/android/test/app/capgo/cordova/updater/NativeContractTest.java +105 -0
  54. package/src/ios/AES.swift +216 -0
  55. package/src/ios/AppHealthTracker.swift +82 -0
  56. package/src/ios/BundleInfo.swift +185 -0
  57. package/src/ios/BundleStatus.swift +127 -0
  58. package/src/ios/CapgoRawRsa.swift +36 -0
  59. package/src/ios/CapgoUpdater.swift +3544 -0
  60. package/src/ios/CordovaCompat.swift +11 -0
  61. package/src/ios/CordovaPluginCall.swift +190 -0
  62. package/src/ios/CordovaUpdaterCommands.swift +270 -0
  63. package/src/ios/CordovaUpdaterPlugin.swift +4706 -0
  64. package/src/ios/CryptoCipher.swift +326 -0
  65. package/src/ios/DelayCondition.swift +74 -0
  66. package/src/ios/DelayUntilNext.swift +30 -0
  67. package/src/ios/DelayUpdateUtils.swift +257 -0
  68. package/src/ios/DeviceIdHelper.swift +120 -0
  69. package/src/ios/Info.plist +28 -0
  70. package/src/ios/InternalUtils.swift +471 -0
  71. package/src/ios/Logger.swift +283 -0
  72. package/src/ios/RSA.swift +96 -0
  73. package/src/ios/ShakeMenu.swift +52 -0
  74. package/src/ios/UpdaterSchemeHandler.swift +81 -0
  75. package/src/ios/UserDefaultsExtension.swift +46 -0
  76. package/src/ios/WebViewStatsReporter.swift +303 -0
@@ -0,0 +1,177 @@
1
+ package app.capgo.cordova.updater;
2
+
3
+ import static org.junit.Assert.*;
4
+ import static org.mockito.ArgumentMatchers.anyLong;
5
+ import static org.mockito.ArgumentMatchers.anyString;
6
+ import static org.mockito.ArgumentMatchers.eq;
7
+ import static org.mockito.Mockito.*;
8
+
9
+ import android.content.SharedPreferences;
10
+ import io.github.g00fy2.versioncompare.Version;
11
+ import java.util.ArrayList;
12
+ import org.json.JSONArray;
13
+ import org.json.JSONObject;
14
+ import org.junit.Before;
15
+ import org.junit.Test;
16
+ import org.mockito.ArgumentCaptor;
17
+ import org.mockito.Mock;
18
+ import org.mockito.MockitoAnnotations;
19
+
20
+ public class DelayUpdateUtilsTest {
21
+
22
+ @Mock
23
+ private SharedPreferences prefs;
24
+
25
+ @Mock
26
+ private SharedPreferences.Editor editor;
27
+
28
+ @Mock
29
+ private Logger logger;
30
+
31
+ private DelayUpdateUtils utils;
32
+
33
+ @Before
34
+ public void setUp() {
35
+ MockitoAnnotations.openMocks(this);
36
+
37
+ when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn("[]");
38
+ when(prefs.getLong(eq(DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY), anyLong())).thenReturn(0L);
39
+ when(editor.putString(anyString(), anyString())).thenReturn(editor);
40
+ when(editor.remove(anyString())).thenReturn(editor);
41
+ when(editor.commit()).thenReturn(true);
42
+
43
+ utils = new DelayUpdateUtils(prefs, editor, new Version("1.0.0"), logger);
44
+ }
45
+
46
+ @Test
47
+ public void parseDelayConditions_returnsPopulatedListForValidJson() throws Exception {
48
+ JSONArray jsonArray = new JSONArray();
49
+ jsonArray.put(new JSONObject().put("kind", "background").put("value", "5000"));
50
+ jsonArray.put(new JSONObject().put("kind", "kill").put("value", ""));
51
+ jsonArray.put(new JSONObject().put("kind", "nativeVersion").put("value", "2.0.0"));
52
+ jsonArray.put(new JSONObject().put("kind", "date").put("value", "2023-01-01T00:00:00.000"));
53
+
54
+ ArrayList<DelayCondition> result = utils.parseDelayConditions(jsonArray.toString());
55
+
56
+ assertEquals(4, result.size());
57
+ assertEquals(DelayUntilNext.background, result.get(0).getKind());
58
+ assertEquals("5000", result.get(0).getValue());
59
+ assertEquals(DelayUntilNext.kill, result.get(1).getKind());
60
+ assertEquals(DelayUntilNext.nativeVersion, result.get(2).getKind());
61
+ assertEquals(DelayUntilNext.date, result.get(3).getKind());
62
+ }
63
+
64
+ @Test
65
+ public void parseDelayConditions_skipsInvalidEntries() throws Exception {
66
+ JSONArray jsonArray = new JSONArray();
67
+ jsonArray.put(new JSONObject().put("kind", "unknown").put("value", "123"));
68
+ jsonArray.put(new JSONObject().put("value", "missingKind"));
69
+ jsonArray.put(new JSONObject().put("kind", "kill").put("value", ""));
70
+
71
+ ArrayList<DelayCondition> result = utils.parseDelayConditions(jsonArray.toString());
72
+
73
+ assertEquals(1, result.size());
74
+ assertEquals(DelayUntilNext.kill, result.get(0).getKind());
75
+ }
76
+
77
+ @Test
78
+ public void parseDelayConditions_handlesInvalidJsonSafely() {
79
+ ArrayList<DelayCondition> result = utils.parseDelayConditions("not-json");
80
+
81
+ assertTrue(result.isEmpty());
82
+ verify(logger).error(contains("Failed to parse delay conditions"));
83
+ }
84
+
85
+ @Test
86
+ public void checkCancelDelay_foregroundRemovesExpiredBackgroundCondition() throws Exception {
87
+ JSONArray stored = new JSONArray();
88
+ stored.put(new JSONObject().put("kind", "background").put("value", "1"));
89
+ stored.put(new JSONObject().put("kind", "kill").put("value", ""));
90
+ when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
91
+
92
+ utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
93
+
94
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
95
+ verify(editor).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), captor.capture());
96
+ verify(editor).commit();
97
+
98
+ JSONArray updated = new JSONArray(captor.getValue());
99
+ assertEquals(1, updated.length());
100
+ JSONObject remaining = updated.getJSONObject(0);
101
+ assertEquals("kill", remaining.getString("kind"));
102
+ }
103
+
104
+ @Test
105
+ public void checkCancelDelay_killedClearsDelaysWithoutInstalling() throws Exception {
106
+ JSONArray stored = new JSONArray();
107
+ stored.put(new JSONObject().put("kind", "kill").put("value", ""));
108
+ when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
109
+
110
+ utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
111
+
112
+ verify(editor).remove(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES));
113
+ verify(editor).commit();
114
+ verify(editor, never()).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString());
115
+ }
116
+
117
+ @Test
118
+ public void checkCancelDelay_killedKeepsOtherConditions() throws Exception {
119
+ JSONArray stored = new JSONArray();
120
+ stored.put(new JSONObject().put("kind", "kill").put("value", ""));
121
+ stored.put(new JSONObject().put("kind", "background").put("value", "5000"));
122
+ when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
123
+
124
+ utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
125
+
126
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
127
+ verify(editor).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), captor.capture());
128
+ verify(editor).commit();
129
+ verify(editor, never()).remove(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES));
130
+
131
+ JSONArray updated = new JSONArray(captor.getValue());
132
+ assertEquals(1, updated.length());
133
+ JSONObject remaining = updated.getJSONObject(0);
134
+ assertEquals("background", remaining.getString("kind"));
135
+ assertEquals("5000", remaining.getString("value"));
136
+ }
137
+
138
+ @Test
139
+ public void checkCancelDelay_foregroundKeepsIsoDateWithoutMilliseconds() throws Exception {
140
+ JSONArray stored = new JSONArray();
141
+ stored.put(new JSONObject().put("kind", "date").put("value", "2099-12-31T23:59:59Z"));
142
+ when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
143
+
144
+ utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
145
+
146
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
147
+ verify(editor).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), captor.capture());
148
+ verify(editor).commit();
149
+ verify(editor, never()).remove(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES));
150
+
151
+ JSONArray updated = new JSONArray(captor.getValue());
152
+ assertEquals(1, updated.length());
153
+ JSONObject remaining = updated.getJSONObject(0);
154
+ assertEquals("date", remaining.getString("kind"));
155
+ assertEquals("2099-12-31T23:59:59Z", remaining.getString("value"));
156
+ }
157
+
158
+ @Test
159
+ public void checkCancelDelay_foregroundKeepsIsoDateWithCompactOffset() throws Exception {
160
+ JSONArray stored = new JSONArray();
161
+ stored.put(new JSONObject().put("kind", "date").put("value", "2099-12-31T23:59:59+0000"));
162
+ when(prefs.getString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), anyString())).thenReturn(stored.toString());
163
+
164
+ utils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
165
+
166
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
167
+ verify(editor).putString(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES), captor.capture());
168
+ verify(editor).commit();
169
+ verify(editor, never()).remove(eq(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES));
170
+
171
+ JSONArray updated = new JSONArray(captor.getValue());
172
+ assertEquals(1, updated.length());
173
+ JSONObject remaining = updated.getJSONObject(0);
174
+ assertEquals("date", remaining.getString("kind"));
175
+ assertEquals("2099-12-31T23:59:59+0000", remaining.getString("value"));
176
+ }
177
+ }
@@ -0,0 +1,105 @@
1
+ package app.capgo.cordova.updater;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+
5
+ import java.io.IOException;
6
+ import java.nio.charset.StandardCharsets;
7
+ import java.nio.file.Files;
8
+ import java.nio.file.Path;
9
+ import org.json.JSONArray;
10
+ import org.json.JSONObject;
11
+ import org.junit.Test;
12
+
13
+ public class NativeContractTest {
14
+
15
+ private static JSONObject contract() {
16
+ try {
17
+ return new JSONObject(new String(Files.readAllBytes(contractFile()), StandardCharsets.UTF_8));
18
+ } catch (Exception error) {
19
+ throw new AssertionError("Unable to load native contract fixture", error);
20
+ }
21
+ }
22
+
23
+ private static Path contractFile() throws IOException {
24
+ Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath();
25
+ while (current != null) {
26
+ Path candidate = current.resolve("native-contract-tests/core.json");
27
+ if (Files.exists(candidate)) {
28
+ return candidate;
29
+ }
30
+ current = current.getParent();
31
+ }
32
+ throw new IOException("native-contract-tests/core.json not found");
33
+ }
34
+
35
+ private static String nullableString(final JSONObject object, final String key) throws Exception {
36
+ if (!object.has(key) || object.isNull(key)) {
37
+ return null;
38
+ }
39
+ return object.getString(key);
40
+ }
41
+
42
+ @Test
43
+ public void periodCheckDelayMatchesNativeContract() throws Exception {
44
+ JSONArray cases = contract().getJSONArray("periodCheckDelay");
45
+ for (int index = 0; index < cases.length(); index++) {
46
+ JSONObject testCase = cases.getJSONObject(index);
47
+ String id = testCase.getString("id");
48
+ int seconds = testCase.getJSONObject("input").getInt("seconds");
49
+ int expected = testCase.getJSONObject("expect").getInt("normalizedSeconds");
50
+
51
+ assertEquals(id, expected, CordovaUpdaterPlugin.normalizedPeriodCheckDelaySeconds(seconds));
52
+ }
53
+ }
54
+
55
+ @Test
56
+ public void autoUpdateModeMatchesNativeContract() throws Exception {
57
+ JSONArray cases = contract().getJSONArray("autoUpdateMode");
58
+ for (int index = 0; index < cases.length(); index++) {
59
+ JSONObject testCase = cases.getJSONObject(index);
60
+ String id = testCase.getString("id");
61
+ String mode = testCase.getJSONObject("input").getString("mode");
62
+ JSONObject expect = testCase.getJSONObject("expect");
63
+ String normalizedMode = CordovaUpdaterPlugin.normalizedAutoUpdateMode(mode);
64
+
65
+ assertEquals(id, expect.getString("mode"), normalizedMode);
66
+ assertEquals(id, expect.getBoolean("enabled"), CordovaUpdaterPlugin.isAutoUpdateModeEnabled(normalizedMode));
67
+ assertEquals(
68
+ id,
69
+ expect.getString("directUpdateMode"),
70
+ CordovaUpdaterPlugin.directUpdateModeForAutoUpdateMode(normalizedMode)
71
+ );
72
+ assertEquals(id, expect.getBoolean("setNextBundle"), CordovaUpdaterPlugin.shouldAutoUpdateModeSetNextBundle(normalizedMode));
73
+ }
74
+ }
75
+
76
+ @Test
77
+ public void onLaunchDirectUpdateConsumptionMatchesNativeContract() throws Exception {
78
+ JSONArray cases = contract().getJSONArray("onLaunchDirectUpdateConsumption");
79
+ for (int index = 0; index < cases.length(); index++) {
80
+ JSONObject testCase = cases.getJSONObject(index);
81
+ String id = testCase.getString("id");
82
+ JSONObject input = testCase.getJSONObject("input");
83
+ boolean expected = testCase.getJSONObject("expect").getBoolean("consume");
84
+
85
+ assertEquals(
86
+ id,
87
+ expected,
88
+ CordovaUpdaterPlugin.shouldConsumeOnLaunchDirectUpdate(input.getString("mode"), input.getBoolean("plannedDirectUpdate"))
89
+ );
90
+ }
91
+ }
92
+
93
+ @Test
94
+ public void updateResponseKindMatchesNativeContract() throws Exception {
95
+ JSONArray cases = contract().getJSONArray("updateResponseKind");
96
+ for (int index = 0; index < cases.length(); index++) {
97
+ JSONObject testCase = cases.getJSONObject(index);
98
+ String id = testCase.getString("id");
99
+ String kind = nullableString(testCase.getJSONObject("input"), "kind");
100
+ String expected = testCase.getJSONObject("expect").getString("kind");
101
+
102
+ assertEquals(id, expected, CordovaUpdaterPlugin.normalizedUpdateResponseKind(kind));
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,216 @@
1
+ import Foundation
2
+ import CommonCrypto
3
+ import CryptoKit
4
+ import Darwin
5
+
6
+ ///
7
+ /// Constants
8
+ ///
9
+ private enum AESConstants {
10
+ static let aesAlgorithm: CCAlgorithm = CCAlgorithm(kCCAlgorithmAES)
11
+ static let aesOptions: CCOptions = CCOptions(kCCOptionPKCS7Padding)
12
+ }
13
+
14
+ // We do all this stuff because ios is shit and open source libraries allow to do decryption with public key
15
+ // So we have to do it manually, while in nodejs or Java it's ok and done at language level.
16
+
17
+ ///
18
+ /// The AES key. Contains both the initialization vector and secret key.
19
+ ///
20
+ public struct AES128Key {
21
+ /// Initialization vector
22
+ private let initVector: Data
23
+ private let logger: Logger
24
+ private let aes128Key: Data
25
+ #if DEBUG
26
+ // swiftlint:disable:next identifier_name
27
+ public var __debug_iv: Data { initVector }
28
+ // swiftlint:disable:next identifier_name
29
+ public var __debug_aes128Key: Data { aes128Key }
30
+ #endif
31
+ // swiftlint:disable:next identifier_name
32
+ init(iv: Data, aes128Key: Data, logger: Logger) {
33
+ self.initVector = iv
34
+ self.aes128Key = aes128Key
35
+ self.logger = logger
36
+ }
37
+ ///
38
+ /// Takes the data and uses the private key to decrypt it. Will call `CCCrypt` in CommonCrypto
39
+ /// and provide it `ivData` for the initialization vector. Will use cipher block chaining (CBC) as
40
+ /// the mode of operation.
41
+ ///
42
+ /// Returns the decrypted data.
43
+ ///
44
+ public func decrypt(data: Data) -> Data? {
45
+ let encryptedData: UnsafePointer<UInt8> = (data as NSData).bytes.bindMemory(
46
+ to: UInt8.self, capacity: data.count)
47
+ let encryptedDataLength: Int = data.count
48
+
49
+ if let result: NSMutableData = NSMutableData(length: encryptedDataLength) {
50
+ let keyData: UnsafePointer<UInt8> = (self.aes128Key as NSData).bytes.bindMemory(
51
+ to: UInt8.self, capacity: self.aes128Key.count)
52
+ let keyLength: size_t = size_t(self.aes128Key.count)
53
+ let ivData: UnsafePointer<UInt8> = (initVector as NSData).bytes.bindMemory(
54
+ to: UInt8.self, capacity: self.initVector.count)
55
+
56
+ let decryptedData: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>(
57
+ result.mutableBytes.assumingMemoryBound(to: UInt8.self))
58
+ let decryptedDataLength: size_t = size_t(result.length)
59
+
60
+ var decryptedLength: size_t = 0
61
+
62
+ let status: CCCryptorStatus = CCCrypt(
63
+ CCOperation(kCCDecrypt),
64
+ AESConstants.aesAlgorithm,
65
+ AESConstants.aesOptions,
66
+ keyData,
67
+ keyLength,
68
+ ivData,
69
+ encryptedData,
70
+ encryptedDataLength,
71
+ decryptedData,
72
+ decryptedDataLength,
73
+ &decryptedLength)
74
+
75
+ if Int32(status) == Int32(kCCSuccess) {
76
+ result.length = Int(decryptedLength)
77
+ return result as Data
78
+ } else {
79
+ logger.error("AES decryption failed with status: \(status)")
80
+ return nil
81
+ }
82
+ } else {
83
+ logger.error("Failed to allocate memory for AES decryption")
84
+ return nil
85
+ }
86
+ }
87
+
88
+ /// AES-CBC file-to-file. Never holds the whole ciphertext in RAM.
89
+ func decrypt(from source: URL, to destination: URL) throws {
90
+ var cryptor: CCCryptorRef?
91
+ let createStatus: CCCryptorStatus = aes128Key.withUnsafeBytes { keyBytes in
92
+ initVector.withUnsafeBytes { ivBytes in
93
+ guard let keyPtr = keyBytes.baseAddress, let ivPtr = ivBytes.baseAddress else {
94
+ return CCCryptorStatus(kCCParamError)
95
+ }
96
+ return CCCryptorCreate(
97
+ CCOperation(kCCDecrypt),
98
+ AESConstants.aesAlgorithm,
99
+ AESConstants.aesOptions,
100
+ keyPtr,
101
+ keyBytes.count,
102
+ ivPtr,
103
+ &cryptor
104
+ )
105
+ }
106
+ }
107
+ guard createStatus == kCCSuccess, let cryptor else {
108
+ logger.error("Failed to create AES cryptor")
109
+ throw NSError(domain: "AESDecryptError", code: Int(createStatus), userInfo: nil)
110
+ }
111
+ defer {
112
+ CCCryptorRelease(cryptor)
113
+ }
114
+
115
+ guard let input = InputStream(url: source) else {
116
+ throw NSError(domain: "AESDecryptError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to open AES source"])
117
+ }
118
+ input.open()
119
+ defer {
120
+ input.close()
121
+ }
122
+
123
+ let tempURL = destination.deletingLastPathComponent().appendingPathComponent("capgo-aes-\(UUID().uuidString).tmp")
124
+ let fileManager = FileManager.default
125
+ fileManager.createFile(atPath: tempURL.path, contents: nil)
126
+ let output = try FileHandle(forWritingTo: tempURL)
127
+ defer {
128
+ try? output.close()
129
+ try? fileManager.removeItem(at: tempURL)
130
+ }
131
+
132
+ let bufferSize = CryptoCipher.ioBufferBytes()
133
+ let outBufSize = bufferSize + kCCBlockSizeAES128
134
+ var inBuf = [UInt8](repeating: 0, count: bufferSize)
135
+ var outBuf = [UInt8](repeating: 0, count: outBufSize)
136
+ let outFd = output.fileDescriptor
137
+
138
+ while true {
139
+ let n = inBuf.withUnsafeMutableBufferPointer { ptr in
140
+ input.read(ptr.baseAddress!, maxLength: ptr.count)
141
+ }
142
+ if n == 0 {
143
+ break
144
+ }
145
+ if n < 0 {
146
+ throw input.streamError ?? NSError(domain: "AESDecryptError", code: 2, userInfo: [NSLocalizedDescriptionKey: "AES stream read failed"])
147
+ }
148
+ var moved: size_t = 0
149
+ let status: CCCryptorStatus = inBuf.withUnsafeBufferPointer { inRaw in
150
+ outBuf.withUnsafeMutableBytes { outRaw in
151
+ CCCryptorUpdate(
152
+ cryptor,
153
+ inRaw.baseAddress,
154
+ n,
155
+ outRaw.baseAddress,
156
+ outBufSize,
157
+ &moved
158
+ )
159
+ }
160
+ }
161
+ guard status == kCCSuccess else {
162
+ logger.error("AES stream update failed")
163
+ throw NSError(domain: "AESDecryptError", code: Int(status), userInfo: nil)
164
+ }
165
+ if moved > 0 {
166
+ try Self.writeAll(fd: outFd, buffer: &outBuf, count: moved)
167
+ }
168
+ }
169
+
170
+ var moved: size_t = 0
171
+ let finalStatus: CCCryptorStatus = outBuf.withUnsafeMutableBytes { outRaw in
172
+ CCCryptorFinal(cryptor, outRaw.baseAddress, outBufSize, &moved)
173
+ }
174
+ guard finalStatus == kCCSuccess else {
175
+ logger.error("AES stream finalize failed")
176
+ throw NSError(domain: "AESDecryptError", code: Int(finalStatus), userInfo: nil)
177
+ }
178
+ if moved > 0 {
179
+ try Self.writeAll(fd: outFd, buffer: &outBuf, count: moved)
180
+ }
181
+ try output.close()
182
+
183
+ let decryptedSize = (try fileManager.attributesOfItem(atPath: tempURL.path)[.size] as? NSNumber)?.uint64Value ?? 0
184
+ if decryptedSize == 0 {
185
+ throw NSError(domain: "Empty decrypted data", code: 7, userInfo: nil)
186
+ }
187
+
188
+ do {
189
+ _ = try fileManager.replaceItemAt(destination, withItemAt: tempURL)
190
+ } catch {
191
+ if fileManager.fileExists(atPath: destination.path) {
192
+ throw error
193
+ }
194
+ try fileManager.moveItem(at: tempURL, to: destination)
195
+ }
196
+ }
197
+
198
+ private static func writeAll(fd: Int32, buffer: inout [UInt8], count: Int) throws {
199
+ var offset = 0
200
+ while offset < count {
201
+ let written = buffer.withUnsafeBufferPointer { ptr -> Int in
202
+ Darwin.write(fd, ptr.baseAddress!.advanced(by: offset), count - offset)
203
+ }
204
+ if written < 0 {
205
+ if errno == EINTR {
206
+ continue
207
+ }
208
+ throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSLocalizedDescriptionKey: "AES stream write failed"])
209
+ }
210
+ if written == 0 {
211
+ throw NSError(domain: NSPOSIXErrorDomain, code: Int(EIO), userInfo: [NSLocalizedDescriptionKey: "AES stream write failed"])
212
+ }
213
+ offset += written
214
+ }
215
+ }
216
+ }
@@ -0,0 +1,82 @@
1
+ import Foundation
2
+
3
+ final class AppHealthTracker {
4
+ private let appSessionIdDefaultsKey = "CapacitorUpdater.appSessionId"
5
+ private let appSessionForegroundDefaultsKey = "CapacitorUpdater.appSessionForeground"
6
+ private let appSessionStartedAtDefaultsKey = "CapacitorUpdater.appSessionStartedAt"
7
+ private let lastReportedUncleanSessionDefaultsKey = "CapacitorUpdater.lastReportedUncleanSessionId"
8
+
9
+ private let implementation: CapgoUpdater
10
+
11
+ init(implementation: CapgoUpdater) {
12
+ self.implementation = implementation
13
+ }
14
+
15
+ static func shouldReportUncleanForegroundExit(
16
+ previousSessionId: String?,
17
+ lastReportedSessionId: String?,
18
+ wasForeground: Bool
19
+ ) -> Bool {
20
+ guard wasForeground, let previousSessionId, !previousSessionId.isEmpty else {
21
+ return false
22
+ }
23
+ return previousSessionId != lastReportedSessionId
24
+ }
25
+
26
+ func reportPreviousUncleanForegroundExit() {
27
+ let defaults = UserDefaults.standard
28
+ let previousSessionId = defaults.string(forKey: appSessionIdDefaultsKey)
29
+ let lastReportedSessionId = defaults.string(forKey: lastReportedUncleanSessionDefaultsKey)
30
+ let wasForeground = defaults.bool(forKey: appSessionForegroundDefaultsKey)
31
+
32
+ guard Self.shouldReportUncleanForegroundExit(
33
+ previousSessionId: previousSessionId,
34
+ lastReportedSessionId: lastReportedSessionId,
35
+ wasForeground: wasForeground
36
+ ), let previousSessionId else {
37
+ return
38
+ }
39
+
40
+ var metadata = [
41
+ "exit_reason": "unclean_foreground_exit",
42
+ "exit_source": "ios_session_marker",
43
+ "previous_session_id": previousSessionId
44
+ ]
45
+ if let sessionStartedAt = defaults.string(forKey: appSessionStartedAtDefaultsKey), !sessionStartedAt.isEmpty {
46
+ metadata["session_started_at"] = sessionStartedAt
47
+ }
48
+
49
+ let current = implementation.getCurrentBundle()
50
+ implementation.sendStats(
51
+ action: "app_crash",
52
+ versionName: current.getVersionName(),
53
+ oldVersionName: "",
54
+ metadata: metadata
55
+ )
56
+ defaults.set(previousSessionId, forKey: lastReportedUncleanSessionDefaultsKey)
57
+ defaults.synchronize()
58
+ }
59
+
60
+ func startSession() {
61
+ let defaults = UserDefaults.standard
62
+ defaults.set(UUID().uuidString, forKey: appSessionIdDefaultsKey)
63
+ defaults.set(true, forKey: appSessionForegroundDefaultsKey)
64
+ defaults.set(String(Int64(Date().timeIntervalSince1970 * 1000)), forKey: appSessionStartedAtDefaultsKey)
65
+ defaults.synchronize()
66
+ }
67
+
68
+ func markForeground(_ isForeground: Bool) {
69
+ UserDefaults.standard.set(isForeground, forKey: appSessionForegroundDefaultsKey)
70
+ UserDefaults.standard.synchronize()
71
+ }
72
+
73
+ func reportMemoryWarning() {
74
+ let current = implementation.getCurrentBundle()
75
+ implementation.sendStats(
76
+ action: "app_memory_warning",
77
+ versionName: current.getVersionName(),
78
+ oldVersionName: "",
79
+ metadata: ["source": "ios_memory_warning"]
80
+ )
81
+ }
82
+ }