@bricks-soft/capacitor-location-tracking 8.0.1 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/BricksSoftCapacitorLocationTracking.podspec +18 -0
  2. package/Package.swift +13 -0
  3. package/README.md +476 -147
  4. package/android/build.gradle +3 -1
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +4 -1
  7. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +14 -5
  8. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +10 -0
  9. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +70 -15
  10. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +6 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +8 -3
  12. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +10 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/config/SecretStore.kt +188 -0
  14. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +188 -17
  15. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +11 -0
  16. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +21 -5
  17. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +20 -9
  18. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +24 -2
  19. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +43 -1
  20. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +17 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceDetails.kt +167 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +191 -34
  23. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +6 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +3 -0
  25. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +52 -8
  26. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +22 -0
  27. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +78 -27
  28. package/android/src/main/java/com/brickssoft/locationtracking/http/ConfigChecks.kt +140 -0
  29. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +50 -12
  30. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +17 -0
  31. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +507 -92
  32. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +6 -6
  33. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +23 -1
  34. package/android/src/main/java/com/brickssoft/locationtracking/http/UploadJobs.kt +217 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/http/UploadWait.kt +184 -0
  36. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +66 -7
  37. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogRedaction.kt +31 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +44 -1
  39. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +79 -8
  40. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +9 -0
  41. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +12 -2
  42. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +10 -2
  43. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +24 -1
  44. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +31 -10
  45. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +48 -3
  46. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +60 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/provider/LocationSubscriptions.kt +180 -0
  48. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +200 -126
  49. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +20 -0
  50. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +39 -51
  51. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +8 -0
  52. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +47 -53
  53. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +34 -2
  54. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +19 -1
  55. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +7 -0
  56. package/android/src/main/res/xml/lt_file_paths.xml +1 -1
  57. package/dist/esm/definitions.d.ts +117 -8
  58. package/dist/esm/definitions.js.map +1 -1
  59. package/dist/esm/events.d.ts +8 -2
  60. package/dist/esm/events.js +7 -1
  61. package/dist/esm/events.js.map +1 -1
  62. package/dist/esm/index.d.ts +1 -1
  63. package/dist/esm/index.js +1 -1
  64. package/dist/esm/index.js.map +1 -1
  65. package/dist/esm/web/config.js +10 -0
  66. package/dist/esm/web/config.js.map +1 -1
  67. package/dist/esm/web/device.d.ts +1 -1
  68. package/dist/esm/web/device.js +8 -1
  69. package/dist/esm/web/device.js.map +1 -1
  70. package/dist/esm/web.d.ts +0 -8
  71. package/dist/esm/web.js +12 -3
  72. package/dist/esm/web.js.map +1 -1
  73. package/dist/plugin.cjs.js +39 -5
  74. package/dist/plugin.cjs.js.map +1 -1
  75. package/dist/plugin.js +39 -5
  76. package/dist/plugin.js.map +1 -1
  77. package/ios/Sources/LocationTrackingPlugin/Api/BackgroundUpload.swift +24 -0
  78. package/ios/Sources/LocationTrackingPlugin/Api/LocationTrackingNative.swift +183 -0
  79. package/ios/Sources/LocationTrackingPlugin/Config/ConfigDefaults.swift +96 -0
  80. package/ios/Sources/LocationTrackingPlugin/Config/ConfigSchema.swift +74 -0
  81. package/ios/Sources/LocationTrackingPlugin/Config/ConfigValidator.swift +40 -0
  82. package/ios/Sources/LocationTrackingPlugin/Config/SecretStore.swift +35 -0
  83. package/ios/Sources/LocationTrackingPlugin/Config/TrackingConfig.swift +104 -0
  84. package/ios/Sources/LocationTrackingPlugin/Core/JSON.swift +50 -0
  85. package/ios/Sources/LocationTrackingPlugin/Core/Options.swift +26 -0
  86. package/ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift +6 -0
  87. package/ios/Sources/LocationTrackingPlugin/Core/TrackingClock.swift +11 -0
  88. package/ios/Sources/LocationTrackingPlugin/Core/TrackingCore.swift +298 -0
  89. package/ios/Sources/LocationTrackingPlugin/Data/TrackingStore.swift +155 -0
  90. package/ios/Sources/LocationTrackingPlugin/Device/DeviceReadings.swift +43 -0
  91. package/ios/Sources/LocationTrackingPlugin/Engine/CheckInNotifier.swift +92 -0
  92. package/ios/Sources/LocationTrackingPlugin/Engine/MotionStateMachine.swift +122 -0
  93. package/ios/Sources/LocationTrackingPlugin/Engine/SessionDeadline.swift +29 -0
  94. package/ios/Sources/LocationTrackingPlugin/Engine/TrackingEngine.swift +344 -0
  95. package/ios/Sources/LocationTrackingPlugin/Engine/TrackingTimers.swift +35 -0
  96. package/ios/Sources/LocationTrackingPlugin/Geofence/DwellTracker.swift +35 -0
  97. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceEventAdapter.swift +88 -0
  98. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceManager.swift +453 -0
  99. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceMonitor.swift +207 -0
  100. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceSpec.swift +103 -0
  101. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceStore.swift +52 -0
  102. package/ios/Sources/LocationTrackingPlugin/Geofence/PolygonMath.swift +176 -0
  103. package/ios/Sources/LocationTrackingPlugin/Heartbeat/ExecutionAssertion.swift +33 -0
  104. package/ios/Sources/LocationTrackingPlugin/Heartbeat/HeartbeatWindow.swift +29 -0
  105. package/ios/Sources/LocationTrackingPlugin/Http/BodyBuilder.swift +52 -0
  106. package/ios/Sources/LocationTrackingPlugin/Http/HTTPTransport.swift +87 -0
  107. package/ios/Sources/LocationTrackingPlugin/Http/StrictJSON.swift +98 -0
  108. package/ios/Sources/LocationTrackingPlugin/Http/Syncer.swift +313 -0
  109. package/ios/Sources/LocationTrackingPlugin/Http/UploadBackoff.swift +26 -0
  110. package/ios/Sources/LocationTrackingPlugin/Location/RouteSource.swift +124 -0
  111. package/ios/Sources/LocationTrackingPlugin/LocationTrackingPlugin.swift +155 -0
  112. package/ios/Sources/LocationTrackingPlugin/Logging/LogExport.swift +73 -0
  113. package/ios/Sources/LocationTrackingPlugin/Logging/TrackingLog.swift +47 -0
  114. package/ios/Sources/LocationTrackingPlugin/Permission/LocationServices.swift +32 -0
  115. package/ios/Sources/LocationTrackingPlugin/Permission/Permissions.swift +102 -0
  116. package/ios/Sources/LocationTrackingPlugin/Position/PositionSource.swift +79 -0
  117. package/ios/Sources/LocationTrackingPlugin/Position/Positions.swift +33 -0
  118. package/ios/Sources/LocationTrackingPlugin/Position/SampleCollector.swift +31 -0
  119. package/ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy +10 -0
  120. package/ios/Sources/LocationTrackingPlugin/Processing/LocationProcessor.swift +83 -0
  121. package/ios/Sources/LocationTrackingPlugin/Processing/TrackedLocation.swift +64 -0
  122. package/ios/Sources/LocationTrackingPlugin/Record/ExternalRecord.swift +54 -0
  123. package/ios/Sources/LocationTrackingPlugin/Record/RecordFactory.swift +27 -0
  124. package/package.json +10 -4
@@ -0,0 +1,103 @@
1
+ import Foundation
2
+ import CoreFoundation
3
+
4
+ struct GeofenceSpec: Equatable {
5
+ var identifier: String
6
+ var latitude: Double
7
+ var longitude: Double
8
+ var radius: Double
9
+ var vertices: [LatLng]? = nil
10
+ var notifyOnEntry = true
11
+ var notifyOnExit = true
12
+ var notifyOnDwell = false
13
+ var loiteringDelay: Double = 30000
14
+ var extras: TrackingJSON? = nil
15
+
16
+ var isPolygon: Bool { vertices != nil }
17
+ var isActive: Bool { notifyOnEntry || notifyOnExit || notifyOnDwell }
18
+
19
+ static func == (a: GeofenceSpec, b: GeofenceSpec) -> Bool {
20
+ a.identifier == b.identifier && a.latitude == b.latitude && a.longitude == b.longitude && a.radius == b.radius && a.vertices == b.vertices && a.notifyOnEntry == b.notifyOnEntry && a.notifyOnExit == b.notifyOnExit && a.notifyOnDwell == b.notifyOnDwell && a.loiteringDelay == b.loiteringDelay && NSDictionary(dictionary: a.extras ?? [:]).isEqual(to: b.extras ?? [:]) && (a.extras == nil) == (b.extras == nil)
21
+ }
22
+
23
+ var json: TrackingJSON {
24
+ var value: TrackingJSON = ["identifier": identifier, "latitude": latitude, "longitude": longitude, "radius": radius, "notifyOnEntry": notifyOnEntry, "notifyOnExit": notifyOnExit, "notifyOnDwell": notifyOnDwell, "loiteringDelay": loiteringDelay]
25
+ if let vertices { value["vertices"] = vertices.map { [$0.latitude, $0.longitude] } }
26
+ if let extras { value["extras"] = extras }
27
+ return value
28
+ }
29
+
30
+ static func stored(_ json: TrackingJSON) -> GeofenceSpec? {
31
+ guard let identifier = json["identifier"] as? String else { return nil }
32
+ let vertices = (json["vertices"] as? [[Double]]).map { $0.map { LatLng(latitude: $0[0], longitude: $0[1]) } }
33
+ return GeofenceSpec(identifier: identifier, latitude: json.number("latitude"), longitude: json.number("longitude"), radius: json.number("radius"), vertices: vertices, notifyOnEntry: json.flag("notifyOnEntry", true), notifyOnExit: json.flag("notifyOnExit", true), notifyOnDwell: json.flag("notifyOnDwell"), loiteringDelay: json.number("loiteringDelay", 30000), extras: json["extras"] as? TrackingJSON)
34
+ }
35
+
36
+ static func parse(_ json: TrackingJSON) throws -> GeofenceSpec {
37
+ guard let identifier = json["identifier"] as? String, !identifier.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw invalid("geofence.identifier is required") }
38
+ for key in ["latitude", "longitude", "radius", "loiteringDelay"] where !isNull(json[key]) && number(json[key]) == nil { throw invalid("geofence '\(identifier)' \(key) must be a number") }
39
+ for key in ["notifyOnEntry", "notifyOnExit", "notifyOnDwell"] where !isNull(json[key]) && !isBoolean(json[key]) { throw invalid("geofence '\(identifier)' \(key) must be a boolean") }
40
+ if !isNull(json["extras"]) && !(json["extras"] is TrackingJSON) { throw invalid("geofence '\(identifier)' extras must be an object") }
41
+ var vertices: [LatLng]?
42
+ if !isNull(json["vertices"]) {
43
+ guard let array = json["vertices"] as? [Any] else { throw invalid("geofence '\(identifier)': vertices must be [[lat,lng],...]") }
44
+ vertices = try array.enumerated().map { index, item in
45
+ guard let pair = item as? [Any], pair.count == 2, let latitude = number(pair[0]), let longitude = number(pair[1]) else { throw invalid("geofence '\(identifier)': vertices[\(index)] must be [lat,lng]") }
46
+ return LatLng(latitude: latitude, longitude: longitude)
47
+ }
48
+ }
49
+ let latitude = number(json["latitude"]), longitude = number(json["longitude"]), radius = number(json["radius"])
50
+ if vertices == nil && (latitude == nil || longitude == nil || radius == nil) { throw invalid("geofence '\(identifier)' needs latitude, longitude and radius, or vertices") }
51
+ let delay = number(json["loiteringDelay"]) ?? 30000
52
+ if delay < 0 { throw invalid("geofence '\(identifier)' loiteringDelay must be >= 0") }
53
+ let spec = GeofenceSpec(identifier: identifier, latitude: latitude ?? 0, longitude: longitude ?? 0, radius: radius ?? 0, vertices: vertices, notifyOnEntry: (json["notifyOnEntry"] as? Bool) ?? true, notifyOnExit: (json["notifyOnExit"] as? Bool) ?? true, notifyOnDwell: (json["notifyOnDwell"] as? Bool) ?? false, loiteringDelay: delay.rounded(.towardZero), extras: json["extras"] as? TrackingJSON)
54
+ if let vertices {
55
+ if vertices.count < 3 { throw invalid("geofence '\(identifier)' needs at least 3 vertices") }
56
+ for (index, vertex) in vertices.enumerated() {
57
+ if !(-90...90).contains(vertex.latitude) || !vertex.latitude.isFinite { throw invalid("geofence '\(identifier)' vertices[\(index)] latitude is out of range") }
58
+ if !(-180...180).contains(vertex.longitude) || !vertex.longitude.isFinite { throw invalid("geofence '\(identifier)' vertices[\(index)] longitude is out of range") }
59
+ }
60
+ } else {
61
+ if !(-90...90).contains(spec.latitude) || !spec.latitude.isFinite { throw invalid("geofence '\(identifier)' latitude is out of range") }
62
+ if !(-180...180).contains(spec.longitude) || !spec.longitude.isFinite { throw invalid("geofence '\(identifier)' longitude is out of range") }
63
+ if !(spec.radius > 0) || spec.radius.isInfinite { throw invalid("geofence '\(identifier)' radius must be > 0") }
64
+ }
65
+ return spec
66
+ }
67
+
68
+ private static func isNull(_ value: Any?) -> Bool { value == nil || value is NSNull }
69
+ private static func isBoolean(_ value: Any?) -> Bool { (value as? NSNumber).map { CFGetTypeID($0) == CFBooleanGetTypeID() } ?? false }
70
+ private static func number(_ value: Any?) -> Double? {
71
+ guard let number = value as? NSNumber, CFGetTypeID(number) != CFBooleanGetTypeID(), number.doubleValue.isFinite else { return nil }
72
+ return number.doubleValue
73
+ }
74
+ private static func invalid(_ message: String) -> TrackingError { TrackingError("INVALID_ARGUMENT", message) }
75
+ }
76
+
77
+ struct GeofenceRuntime: Equatable {
78
+ var insideCircle = false
79
+ var insidePolygon = false
80
+ var enteredAt: Double? = nil
81
+ static let unknown = GeofenceRuntime()
82
+ var json: TrackingJSON { ["insideCircle": insideCircle, "insidePolygon": insidePolygon, "enteredAt": enteredAt as Any? ?? NSNull()] }
83
+ static func from(_ json: TrackingJSON) -> GeofenceRuntime { GeofenceRuntime(insideCircle: json.flag("insideCircle"), insidePolygon: json.flag("insidePolygon"), enteredAt: json["enteredAt"] as? Double) }
84
+ }
85
+
86
+ enum GeofenceAction: String { case enter = "ENTER", exit = "EXIT", dwell = "DWELL" }
87
+
88
+ struct OsGeofence: Equatable {
89
+ var id: String
90
+ var latitude: Double
91
+ var longitude: Double
92
+ var radius: Double
93
+ var onEntry: Bool
94
+ var onExit: Bool
95
+ var initialTriggerEntry: Bool
96
+ }
97
+
98
+ struct OsGeofenceTransition: Equatable {
99
+ var id: String
100
+ var action: GeofenceAction
101
+ var location: TrackedLocation?
102
+ var initial = false
103
+ }
@@ -0,0 +1,52 @@
1
+ import Foundation
2
+
3
+ protocol GeofenceStore: AnyObject {
4
+ func upsert(_ specs: [GeofenceSpec]) throws
5
+ func remove(_ ids: [String]) throws
6
+ func removeAll() throws
7
+ func all() throws -> [GeofenceSpec]
8
+ func get(_ id: String) throws -> GeofenceSpec?
9
+ func setRuntime(_ id: String, _ runtime: GeofenceRuntime) throws
10
+ func runtimes() throws -> [String: GeofenceRuntime]
11
+ }
12
+
13
+ final class SQLiteGeofenceStore: GeofenceStore {
14
+ private let store: TrackingStore
15
+ private let key = "geofences"
16
+ init(store: TrackingStore) { self.store = store }
17
+ private func load() throws -> TrackingJSON { try store.load(key) ?? [:] }
18
+ private func save(_ rows: TrackingJSON) throws { try store.save(key, rows) }
19
+ func upsert(_ specs: [GeofenceSpec]) throws {
20
+ guard !specs.isEmpty else { return }
21
+ var rows = try load()
22
+ for spec in specs {
23
+ var row = rows.object(spec.identifier)
24
+ row["spec"] = spec.json
25
+ rows[spec.identifier] = row
26
+ }
27
+ try save(rows)
28
+ }
29
+ func remove(_ ids: [String]) throws {
30
+ guard !ids.isEmpty else { return }
31
+ var rows = try load()
32
+ ids.forEach { rows.removeValue(forKey: $0) }
33
+ try save(rows)
34
+ }
35
+ func removeAll() throws { try save([:]) }
36
+ func all() throws -> [GeofenceSpec] {
37
+ try load().sorted { $0.key.utf8.lexicographicallyPrecedes($1.key.utf8) }.compactMap { GeofenceSpec.stored(($0.value as? TrackingJSON)?.object("spec") ?? [:]) }
38
+ }
39
+ func get(_ id: String) throws -> GeofenceSpec? { GeofenceSpec.stored(try load().object(id).object("spec")) }
40
+ func setRuntime(_ id: String, _ runtime: GeofenceRuntime) throws {
41
+ var rows = try load()
42
+ guard var row = rows[id] as? TrackingJSON else { return }
43
+ row["runtime"] = runtime.json
44
+ rows[id] = row
45
+ try save(rows)
46
+ }
47
+ func runtimes() throws -> [String: GeofenceRuntime] {
48
+ var result: [String: GeofenceRuntime] = [:]
49
+ for (id, value) in try load() { if let runtime = (value as? TrackingJSON)?["runtime"] as? TrackingJSON { result[id] = GeofenceRuntime.from(runtime) } }
50
+ return result
51
+ }
52
+ }
@@ -0,0 +1,176 @@
1
+ import Foundation
2
+
3
+ struct LatLng: Equatable, Hashable {
4
+ var latitude: Double
5
+ var longitude: Double
6
+ }
7
+
8
+ struct XorWowRandom {
9
+ private var x: Int32, y: Int32, z: Int32, w: Int32, v: Int32, addend: Int32
10
+ init(seed: Int32) {
11
+ x = seed; y = seed >> 31; z = 0; w = 0; v = ~seed
12
+ addend = (seed << 10) ^ Int32(bitPattern: UInt32(bitPattern: seed >> 31) >> 4)
13
+ for _ in 0..<64 { _ = nextInt() }
14
+ }
15
+ mutating func nextInt() -> Int32 {
16
+ var t = x
17
+ t = t ^ Int32(bitPattern: UInt32(bitPattern: t) >> 2)
18
+ x = y; y = z; z = w
19
+ let v0 = v
20
+ w = v0
21
+ t = (t ^ (t << 1)) ^ v0 ^ (v0 << 4)
22
+ v = t
23
+ addend = addend &+ 362437
24
+ return t &+ addend
25
+ }
26
+ mutating func nextBits(_ count: Int32) -> Int32 {
27
+ Int32(bitPattern: UInt32(bitPattern: nextInt()) >> UInt32(32 - count)) & ((-count) >> 31)
28
+ }
29
+ mutating func nextInt(_ until: Int32) -> Int32 {
30
+ if until & -until == until { return nextBits(Int32(31 - until.leadingZeroBitCount)) }
31
+ while true {
32
+ let bits = Int32(bitPattern: UInt32(bitPattern: nextInt()) >> 1)
33
+ let value = bits % until
34
+ if bits &- value &+ (until - 1) >= 0 { return value }
35
+ }
36
+ }
37
+ mutating func nextDouble() -> Double {
38
+ let high = Int64(nextBits(26)), low = Int64(nextBits(27))
39
+ return Double((high << 27) + low) / Double(Int64(1) << 53)
40
+ }
41
+ mutating func nextDouble(_ from: Double, _ until: Double) -> Double {
42
+ let value = from + nextDouble() * (until - from)
43
+ return value >= until ? until.nextDown : value
44
+ }
45
+ mutating func shuffled<T>(_ values: [T]) -> [T] {
46
+ var result = values
47
+ for index in stride(from: result.count - 1, through: 1, by: -1) { result.swapAt(index, Int(nextInt(Int32(index + 1)))) }
48
+ return result
49
+ }
50
+ }
51
+
52
+ enum PolygonMath {
53
+ static let earthRadius = 6371008.8
54
+ static let padding = 1.1
55
+ static let minimumEnclosingRadius = 50.0
56
+ private static let epsilon = 1e-6
57
+
58
+ struct Circle: Equatable {
59
+ var latitude: Double
60
+ var longitude: Double
61
+ var radius: Double
62
+ }
63
+
64
+ static func isValidLatLng(_ latitude: Double, _ longitude: Double) -> Bool {
65
+ latitude.isFinite && longitude.isFinite && (-90...90).contains(latitude) && (-180...180).contains(longitude)
66
+ }
67
+
68
+ static func distanceMeters(_ lat1: Double, _ lng1: Double, _ lat2: Double, _ lng2: Double) -> Double {
69
+ let phi1 = lat1 * .pi / 180, phi2 = lat2 * .pi / 180
70
+ let dPhi = phi2 - phi1, dLambda = (lng2 - lng1) * .pi / 180
71
+ let a = sin(dPhi / 2) * sin(dPhi / 2) + cos(phi1) * cos(phi2) * sin(dLambda / 2) * sin(dLambda / 2)
72
+ return 2 * earthRadius * asin(min(1, sqrt(a)))
73
+ }
74
+
75
+ static func contains(_ vertices: [LatLng], _ latitude: Double, _ longitude: Double) -> Bool {
76
+ var inside = false
77
+ var j = vertices.count - 1
78
+ for i in vertices.indices {
79
+ let yi = vertices[i].latitude, xi = vertices[i].longitude, yj = vertices[j].latitude, xj = vertices[j].longitude
80
+ if (yi > latitude) != (yj > latitude) && longitude < (xj - xi) * (latitude - yi) / (yj - yi) + xi { inside.toggle() }
81
+ j = i
82
+ }
83
+ return inside
84
+ }
85
+
86
+ static func distanceToBoundary(_ vertices: [LatLng], _ latitude: Double, _ longitude: Double) -> Double {
87
+ guard !vertices.isEmpty else { return .infinity }
88
+ let projection = Projection(latitude, longitude)
89
+ let points = vertices.map { projection.point($0.latitude, $0.longitude) }
90
+ if points.count == 1 { return hypot(points[0].x, points[0].y) }
91
+ var best = Double.infinity
92
+ var j = points.count - 1
93
+ for i in points.indices {
94
+ best = min(best, distanceToSegment(points[j], points[i]))
95
+ j = i
96
+ }
97
+ return best
98
+ }
99
+
100
+ static func areaSquareMeters(_ vertices: [LatLng]) -> Double {
101
+ guard vertices.count >= 3 else { return 0 }
102
+ let projection = Projection.around(vertices)
103
+ let points = vertices.map { projection.point($0.latitude, $0.longitude) }
104
+ var sum = 0.0
105
+ var j = points.count - 1
106
+ for i in points.indices {
107
+ sum += points[j].x * points[i].y - points[i].x * points[j].y
108
+ j = i
109
+ }
110
+ return abs(sum) / 2
111
+ }
112
+
113
+ static func minimalEnclosingCircle(_ vertices: [LatLng]) -> Circle {
114
+ precondition(!vertices.isEmpty)
115
+ let projection = Projection.around(vertices)
116
+ var random = XorWowRandom(seed: Int32(vertices.count))
117
+ let points = random.shuffled(vertices.map { projection.point($0.latitude, $0.longitude) })
118
+ var c = PlaneCircle(x: points[0].x, y: points[0].y, r: 0)
119
+ for i in points.indices where !c.contains(points[i]) {
120
+ c = PlaneCircle(x: points[i].x, y: points[i].y, r: 0)
121
+ for j in 0..<i where !c.contains(points[j]) {
122
+ c = PlaneCircle.diameter(points[i], points[j])
123
+ for k in 0..<j where !c.contains(points[k]) { c = PlaneCircle.through(points[i], points[j], points[k]) }
124
+ }
125
+ }
126
+ let center = projection.latLng(c.x, c.y)
127
+ let radius = vertices.map { distanceMeters(center.latitude, center.longitude, $0.latitude, $0.longitude) }.max() ?? 0
128
+ return Circle(latitude: center.latitude, longitude: center.longitude, radius: radius)
129
+ }
130
+
131
+ static func enclosingCircle(_ vertices: [LatLng]) -> Circle {
132
+ var circle = minimalEnclosingCircle(vertices)
133
+ circle.radius = max(circle.radius * padding, minimumEnclosingRadius)
134
+ return circle
135
+ }
136
+
137
+ private static func distanceToSegment(_ a: Point, _ b: Point) -> Double {
138
+ let dx = b.x - a.x, dy = b.y - a.y
139
+ let lengthSquared = dx * dx + dy * dy
140
+ if lengthSquared == 0 { return hypot(a.x, a.y) }
141
+ let t = min(1, max(0, (-a.x * dx - a.y * dy) / lengthSquared))
142
+ return hypot(a.x + t * dx, a.y + t * dy)
143
+ }
144
+
145
+ private struct Point { var x: Double; var y: Double }
146
+
147
+ private struct Projection {
148
+ let lat0: Double, lng0: Double, cosLat0: Double
149
+ init(_ lat0: Double, _ lng0: Double) { self.lat0 = lat0; self.lng0 = lng0; cosLat0 = max(cos(lat0 * .pi / 180), 1e-9) }
150
+ func point(_ latitude: Double, _ longitude: Double) -> Point {
151
+ Point(x: (longitude - lng0) * .pi / 180 * cosLat0 * PolygonMath.earthRadius, y: (latitude - lat0) * .pi / 180 * PolygonMath.earthRadius)
152
+ }
153
+ func latLng(_ x: Double, _ y: Double) -> LatLng {
154
+ LatLng(latitude: lat0 + y / PolygonMath.earthRadius * 180 / .pi, longitude: lng0 + x / (PolygonMath.earthRadius * cosLat0) * 180 / .pi)
155
+ }
156
+ static func around(_ vertices: [LatLng]) -> Projection {
157
+ let latitudes = vertices.map(\.latitude), longitudes = vertices.map(\.longitude)
158
+ return Projection((latitudes.min()! + latitudes.max()!) / 2, (longitudes.min()! + longitudes.max()!) / 2)
159
+ }
160
+ }
161
+
162
+ private struct PlaneCircle {
163
+ var x: Double, y: Double, r: Double
164
+ func contains(_ p: Point) -> Bool { hypot(p.x - x, p.y - y) <= r + PolygonMath.epsilon + r * 1e-12 }
165
+ static func diameter(_ a: Point, _ b: Point) -> PlaneCircle { PlaneCircle(x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, r: hypot(a.x - b.x, a.y - b.y) / 2) }
166
+ static func through(_ a: Point, _ b: Point, _ c: Point) -> PlaneCircle {
167
+ let bx = b.x - a.x, by = b.y - a.y, cx = c.x - a.x, cy = c.y - a.y
168
+ let d = 2 * (bx * cy - by * cx)
169
+ let scale = max(bx * bx + by * by, cx * cx + cy * cy)
170
+ if abs(d) <= 1e-12 * max(scale, 1e-12) { return [diameter(a, b), diameter(a, c), diameter(b, c)].max { $0.r < $1.r }! }
171
+ let b2 = bx * bx + by * by, c2 = cx * cx + cy * cy
172
+ let ux = (cy * b2 - by * c2) / d, uy = (bx * c2 - cx * b2) / d
173
+ return PlaneCircle(x: a.x + ux, y: a.y + uy, r: hypot(ux, uy))
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,33 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ final class ExecutionAssertion {
5
+ private let lock = NSLock()
6
+ private let begin: (@escaping () -> Void) -> Int
7
+ private let end: (Int) -> Void
8
+ private var identifier: Int?
9
+ private var ended = false
10
+ private var timeout: DispatchWorkItem?
11
+ init(begin: @escaping (@escaping () -> Void) -> Int = { expiration in UIApplication.shared.beginBackgroundTask(withName: "Location tracking heartbeat", expirationHandler: expiration).rawValue }, end: @escaping (Int) -> Void = { UIApplication.shared.endBackgroundTask(UIBackgroundTaskIdentifier(rawValue: $0)) }) { self.begin = begin; self.end = end }
12
+ func start(seconds: Double, queue: DispatchQueue) {
13
+ guard seconds > 0 else { finish(); return }
14
+ let id = begin { [weak self] in self?.finish() }
15
+ lock.lock()
16
+ if ended { lock.unlock(); if id != UIBackgroundTaskIdentifier.invalid.rawValue { end(id) }; return }
17
+ identifier = id
18
+ let item = DispatchWorkItem { [weak self] in self?.finish() }
19
+ timeout = item
20
+ lock.unlock()
21
+ queue.asyncAfter(deadline: .now() + seconds, execute: item)
22
+ }
23
+ func finish() {
24
+ lock.lock()
25
+ guard !ended else { lock.unlock(); return }
26
+ ended = true
27
+ timeout?.cancel(); timeout = nil
28
+ let id = identifier; identifier = nil
29
+ lock.unlock()
30
+ if let id, id != UIBackgroundTaskIdentifier.invalid.rawValue { end(id) }
31
+ }
32
+ deinit { finish() }
33
+ }
@@ -0,0 +1,29 @@
1
+ import Foundation
2
+
3
+ struct HeartbeatWindow: Equatable {
4
+ var baseElapsed: Double
5
+ var dueElapsed: Double
6
+ var deadlineElapsed: Double
7
+ var nextHeartbeatAt: Double?
8
+ var minInterval: Double
9
+ var enabled: Bool
10
+ func shouldFire(_ elapsed: Double) -> Bool { elapsed >= dueElapsed - 1000 }
11
+ func needsRearm(_ armed: HeartbeatWindow?) -> Bool {
12
+ guard let armed, armed.enabled == enabled else { return true }
13
+ let move = dueElapsed - armed.dueElapsed
14
+ return move < 0 || move >= 30000
15
+ }
16
+ static func compute(lastRecordAt: Double?, lastRecordElapsed: Double?, sameBoot: Bool?, now: Double, nowElapsed: Double, minInterval: Double = 180, maxInterval: Double = 300, enabled: Bool = true, noRecordBaseElapsed: Double? = nil, lastAttemptElapsed: Double? = nil, trackingStartedAt: Double? = nil) -> HeartbeatWindow {
17
+ let minimum = max(60, minInterval) * 1000
18
+ let maximum = max(minimum, maxInterval * 1000)
19
+ var base: Double
20
+ if let at = lastRecordAt {
21
+ if let elapsed = lastRecordElapsed, sameBoot ?? (elapsed <= nowElapsed) { base = elapsed }
22
+ else { base = nowElapsed - (now - at) }
23
+ if let started = trackingStartedAt, at < started, started <= now { base = max(base, nowElapsed - (now - started)) }
24
+ } else { base = noRecordBaseElapsed ?? nowElapsed }
25
+ base = min(base, nowElapsed)
26
+ let due = max(base + minimum, lastAttemptElapsed.map { $0 + minimum } ?? -.infinity)
27
+ return HeartbeatWindow(baseElapsed: base, dueElapsed: due, deadlineElapsed: base + maximum, nextHeartbeatAt: enabled ? now + max(0, due - nowElapsed) : nil, minInterval: minimum, enabled: enabled)
28
+ }
29
+ }
@@ -0,0 +1,52 @@
1
+ import Foundation
2
+
3
+ struct BodyBuilder {
4
+ private static let placeholder = try! NSRegularExpression(pattern: "(\"?)<%=\\s*(.*?)\\s*%>(\"?)", options: .dotMatchesLineSeparators)
5
+ private static let coordinateKeys: Set<String> = ["latitude", "longitude", "accuracy", "altitude", "altitude_accuracy", "speed", "speed_accuracy", "heading", "heading_accuracy"]
6
+ private static let supported: Set<String> = ["uuid", "event", "timestamp", "recorded_at", "sent_at", "latitude", "longitude", "accuracy", "altitude", "altitude_accuracy", "speed", "speed_accuracy", "heading", "heading_accuracy", "is_moving", "odometer", "mock", "activity.type", "activity.confidence", "battery.level", "battery.is_charging", "battery.temperature", "battery.health", "battery.plugged", "network", "device.device_id", "device.background_restricted", "device.standby_bucket", "device.disk_free_bytes", "device.thermal_status", "device.manufacturer", "device.model", "device.os_version", "device.sdk_int", "device.low_ram_device", "device.ram_total_bytes", "elapsed_realtime_ms", "boot_count", "backend", "reason", "stopped_at", "geofence.identifier", "geofence.action", "provider.enabled", "provider.gps", "provider.network", "provider.permission", "extras", "record", "provider", "device"]
7
+ static func build(_ records: [TrackingJSON], http: TrackingJSON, sentAt: String = iso()) throws -> Data {
8
+ let rendered = records.map { record -> Any in
9
+ var wire = record
10
+ wire["sent_at"] = sentAt
11
+ let template = record.text("event") == "geofence" ? http.text("geofenceTemplate", http.text("locationTemplate")) : http.text("locationTemplate")
12
+ return render(template, record: wire) ?? wire
13
+ }
14
+ let root = http.text("rootProperty", "location")
15
+ let merge = root == "." || root.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
16
+ let batch = http.flag("batchSync")
17
+ if merge && batch { return try jsonData(rendered) }
18
+ let value: Any = batch ? rendered : rendered[0]
19
+ var body: TrackingJSON
20
+ if merge {
21
+ guard let object = value as? TrackingJSON else { return try jsonData(value) }
22
+ body = object
23
+ } else { body = [root: value] }
24
+ for (key, value) in http.object("params") where body[key] == nil { body[key] = value }
25
+ return try jsonData(body)
26
+ }
27
+ static func render(_ template: String, record: TrackingJSON) -> Any? {
28
+ guard !template.isEmpty else { return nil }
29
+ let source = template as NSString
30
+ var output = template
31
+ for match in Self.placeholder.matches(in: template, range: NSRange(location: 0, length: source.length)).reversed() {
32
+ let key = source.substring(with: match.range(at: 2))
33
+ let open = source.substring(with: match.range(at: 1))
34
+ let close = source.substring(with: match.range(at: 3))
35
+ let path = Self.coordinateKeys.contains(key) ? "coords." + key : key
36
+ var value: Any? = record
37
+ if key != "record" {
38
+ for component in path.split(separator: ".") { value = (value as? TrackingJSON)?[String(component)] }
39
+ }
40
+ if key == "extras", value == nil { value = TrackingJSON() }
41
+ let replacement: String
42
+ if !Self.supported.contains(key) { replacement = open + close }
43
+ else if value == nil || value is NSNull { replacement = open.isEmpty || close.isEmpty ? open + "null" + close : "null" }
44
+ else if let data = try? jsonData(value!), let literal = String(data: data, encoding: .utf8) {
45
+ replacement = open + (value is String ? String(literal.dropFirst().dropLast()) : literal) + close
46
+ } else { replacement = "null" }
47
+ if let range = Range(match.range, in: output) { output.replaceSubrange(range, with: replacement) }
48
+ }
49
+ guard StrictJSON.isValid(output), let data = output.data(using: .utf8), let value = try? JSONSerialization.jsonObject(with: data), value is TrackingJSON || value is [Any] else { return nil }
50
+ return value
51
+ }
52
+ }
@@ -0,0 +1,87 @@
1
+ import Foundation
2
+
3
+ struct HTTPResponse {
4
+ let status: Int
5
+ let body: Data
6
+ let retryAfter: String?
7
+ var success: Bool { (200..<300).contains(status) }
8
+ }
9
+
10
+ final class HTTPTransport: NSObject, URLSessionDataDelegate {
11
+ private struct Pending {
12
+ var data = Data()
13
+ var response: HTTPURLResponse?
14
+ var timeout: DispatchWorkItem?
15
+ let responseLimit: Int?
16
+ let completion: (Result<HTTPResponse, Error>) -> Void
17
+ }
18
+ private var pending: [Int: Pending] = [:]
19
+ private let lock = NSLock()
20
+ private var closed = false
21
+ private lazy var session: URLSession = {
22
+ let config = URLSessionConfiguration.ephemeral
23
+ config.urlCache = nil
24
+ config.httpCookieStorage = nil
25
+ config.urlCredentialStorage = nil
26
+ config.requestCachePolicy = .reloadIgnoringLocalCacheData
27
+ config.waitsForConnectivity = false
28
+ return URLSession(configuration: config, delegate: self, delegateQueue: nil)
29
+ }()
30
+ func send(url: URL, method: String, headers: [String: String], body: Data, timeout: Double, responseLimit: Int? = 16384, completion: @escaping (Result<HTTPResponse, Error>) -> Void) {
31
+ var request = URLRequest(url: url, timeoutInterval: timeout / 1000)
32
+ request.httpMethod = method
33
+ request.httpBody = body
34
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
35
+ for (name, value) in headers { request.setValue(value, forHTTPHeaderField: name) }
36
+ lock.lock()
37
+ guard !closed else { lock.unlock(); completion(.failure(URLError(.cancelled))); return }
38
+ let task = session.dataTask(with: request)
39
+ let deadline = DispatchWorkItem { [weak self, weak task] in
40
+ guard let self, let task else { return }
41
+ self.lock.lock(); let item = self.pending.removeValue(forKey: task.taskIdentifier); self.lock.unlock()
42
+ guard let item else { return }
43
+ task.cancel()
44
+ item.completion(.failure(URLError(.timedOut)))
45
+ }
46
+ pending[task.taskIdentifier] = Pending(timeout: deadline, responseLimit: responseLimit, completion: completion); lock.unlock()
47
+ DispatchQueue.global().asyncAfter(deadline: .now() + timeout / 1000, execute: deadline)
48
+ task.resume()
49
+ }
50
+ func cancel() {
51
+ lock.lock()
52
+ guard !closed else { lock.unlock(); return }
53
+ let current = session
54
+ lock.unlock()
55
+ current.getAllTasks { $0.forEach { $0.cancel() } }
56
+ }
57
+ func close() {
58
+ lock.lock()
59
+ guard !closed else { lock.unlock(); return }
60
+ closed = true
61
+ let current = session
62
+ lock.unlock()
63
+ current.invalidateAndCancel()
64
+ }
65
+ func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) }
66
+ func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
67
+ lock.lock(); pending[dataTask.taskIdentifier]?.response = response as? HTTPURLResponse; lock.unlock()
68
+ completionHandler(.allow)
69
+ }
70
+ func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
71
+ lock.lock()
72
+ if var item = pending[dataTask.taskIdentifier] {
73
+ if let limit = item.responseLimit { item.data.append(data.prefix(max(0, limit - item.data.count))) }
74
+ else { item.data.append(data) }
75
+ pending[dataTask.taskIdentifier] = item
76
+ }
77
+ lock.unlock()
78
+ }
79
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
80
+ lock.lock(); let item = pending.removeValue(forKey: task.taskIdentifier); lock.unlock()
81
+ guard let item else { return }
82
+ item.timeout?.cancel()
83
+ if let error { item.completion(.failure(error)) }
84
+ else if let response = item.response { item.completion(.success(HTTPResponse(status: response.statusCode, body: item.data, retryAfter: response.value(forHTTPHeaderField: "Retry-After")))) }
85
+ else { item.completion(.failure(TrackingError("NETWORK_ERROR", "No HTTP response"))) }
86
+ }
87
+ }
@@ -0,0 +1,98 @@
1
+ import Foundation
2
+
3
+ struct StrictJSON {
4
+ private let bytes: [UInt8]
5
+ private var index = 0
6
+ init(_ text: String) { bytes = Array(text.utf8) }
7
+ static func isValid(_ text: String) -> Bool {
8
+ var parser = StrictJSON(text)
9
+ parser.whitespace()
10
+ guard parser.value(0) else { return false }
11
+ parser.whitespace()
12
+ return parser.index == parser.bytes.count
13
+ }
14
+ private mutating func whitespace() { while index < bytes.count && [9, 10, 13, 32].contains(bytes[index]) { index += 1 } }
15
+ private mutating func consume(_ byte: UInt8) -> Bool {
16
+ guard index < bytes.count && bytes[index] == byte else { return false }
17
+ index += 1; return true
18
+ }
19
+ private mutating func value(_ depth: Int) -> Bool {
20
+ guard depth <= 256 && index < bytes.count else { return false }
21
+ switch bytes[index] {
22
+ case 123: return object(depth + 1)
23
+ case 91: return array(depth + 1)
24
+ case 34: return string()
25
+ case 116: return literal("true")
26
+ case 102: return literal("false")
27
+ case 110: return literal("null")
28
+ default: return number()
29
+ }
30
+ }
31
+ private mutating func object(_ depth: Int) -> Bool {
32
+ index += 1; whitespace()
33
+ if consume(125) { return true }
34
+ while true {
35
+ whitespace()
36
+ guard index < bytes.count && bytes[index] == 34 && string() else { return false }
37
+ whitespace()
38
+ guard consume(58) else { return false }
39
+ whitespace()
40
+ guard value(depth) else { return false }
41
+ whitespace()
42
+ if consume(125) { return true }
43
+ guard consume(44) else { return false }
44
+ }
45
+ }
46
+ private mutating func array(_ depth: Int) -> Bool {
47
+ index += 1; whitespace()
48
+ if consume(93) { return true }
49
+ while true {
50
+ whitespace()
51
+ guard value(depth) else { return false }
52
+ whitespace()
53
+ if consume(93) { return true }
54
+ guard consume(44) else { return false }
55
+ }
56
+ }
57
+ private mutating func string() -> Bool {
58
+ index += 1
59
+ while index < bytes.count {
60
+ let byte = bytes[index]; index += 1
61
+ if byte == 34 { return true }
62
+ if byte < 32 { return false }
63
+ if byte == 92 {
64
+ guard index < bytes.count else { return false }
65
+ let escape = bytes[index]; index += 1
66
+ if escape == 117 {
67
+ guard index + 4 <= bytes.count else { return false }
68
+ guard bytes[index..<index + 4].allSatisfy({ (48...57).contains($0) || (65...70).contains($0) || (97...102).contains($0) }) else { return false }
69
+ index += 4
70
+ } else if ![34, 92, 47, 98, 102, 110, 114, 116].contains(escape) { return false }
71
+ }
72
+ }
73
+ return false
74
+ }
75
+ private mutating func literal(_ text: String) -> Bool {
76
+ let expected = Array(text.utf8)
77
+ guard index + expected.count <= bytes.count && Array(bytes[index..<index + expected.count]) == expected else { return false }
78
+ index += expected.count; return true
79
+ }
80
+ private mutating func number() -> Bool {
81
+ _ = consume(45)
82
+ guard index < bytes.count else { return false }
83
+ if bytes[index] == 48 { index += 1 }
84
+ else if (49...57).contains(bytes[index]) { _ = digits() }
85
+ else { return false }
86
+ if consume(46) && !digits() { return false }
87
+ if consume(101) || consume(69) {
88
+ if !consume(43) { _ = consume(45) }
89
+ if !digits() { return false }
90
+ }
91
+ return true
92
+ }
93
+ private mutating func digits() -> Bool {
94
+ let start = index
95
+ while index < bytes.count && (48...57).contains(bytes[index]) { index += 1 }
96
+ return index > start
97
+ }
98
+ }