react-native-bg-geolocation 0.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 (142) hide show
  1. package/BgGeolocation.podspec +39 -0
  2. package/LICENSE +20 -0
  3. package/README.md +366 -0
  4. package/android/build.gradle +69 -0
  5. package/android/src/main/AndroidManifest.xml +53 -0
  6. package/android/src/main/java/com/bggeolocation/BgGeolocationActivityRecognitionReceiver.kt +116 -0
  7. package/android/src/main/java/com/bggeolocation/BgGeolocationBootReceiver.kt +44 -0
  8. package/android/src/main/java/com/bggeolocation/BgGeolocationForegroundService.kt +373 -0
  9. package/android/src/main/java/com/bggeolocation/BgGeolocationGeofenceReceiver.kt +55 -0
  10. package/android/src/main/java/com/bggeolocation/BgGeolocationHeadlessTask.kt +138 -0
  11. package/android/src/main/java/com/bggeolocation/BgGeolocationModule.kt +1030 -0
  12. package/android/src/main/java/com/bggeolocation/BgGeolocationMotionStateMachine.kt +159 -0
  13. package/android/src/main/java/com/bggeolocation/BgGeolocationPackage.kt +31 -0
  14. package/android/src/main/res/drawable/bg_geo_notification.xml +9 -0
  15. package/ios/BgGeolocation.h +14 -0
  16. package/ios/BgGeolocation.mm +709 -0
  17. package/ios/engine/AtomicBoolean.swift +48 -0
  18. package/ios/engine/BGActivityChangeEvent.swift +20 -0
  19. package/ios/engine/BGActivityConfig.swift +71 -0
  20. package/ios/engine/BGAppConfig.swift +92 -0
  21. package/ios/engine/BGAppState.swift +147 -0
  22. package/ios/engine/BGAuthorization.swift +85 -0
  23. package/ios/engine/BGAuthorizationAlertPresenter.swift +39 -0
  24. package/ios/engine/BGAuthorizationConfig.swift +50 -0
  25. package/ios/engine/BGAuthorizationEvent.swift +40 -0
  26. package/ios/engine/BGBackgroundTaskManager.swift +143 -0
  27. package/ios/engine/BGCLRouter.swift +101 -0
  28. package/ios/engine/BGCallback.swift +19 -0
  29. package/ios/engine/BGConfig.swift +440 -0
  30. package/ios/engine/BGConfigModuleBase.swift +180 -0
  31. package/ios/engine/BGConfigOLD.swift +582 -0
  32. package/ios/engine/BGConnectivityChangeEvent.swift +15 -0
  33. package/ios/engine/BGCrashDetector.swift +122 -0
  34. package/ios/engine/BGCurrentPositionRequest.swift +87 -0
  35. package/ios/engine/BGDataStore.swift +75 -0
  36. package/ios/engine/BGDatabase.swift +677 -0
  37. package/ios/engine/BGDatabasePool.swift +220 -0
  38. package/ios/engine/BGDatabaseQueue.swift +215 -0
  39. package/ios/engine/BGDateUtils.swift +26 -0
  40. package/ios/engine/BGDeviceInfo.swift +54 -0
  41. package/ios/engine/BGDeviceManager.swift +65 -0
  42. package/ios/engine/BGEnabledChangeEvent.swift +11 -0
  43. package/ios/engine/BGEnv.swift +17 -0
  44. package/ios/engine/BGEventBus.swift +83 -0
  45. package/ios/engine/BGEventManager.swift +169 -0
  46. package/ios/engine/BGEventNames.swift +51 -0
  47. package/ios/engine/BGGeofence.swift +233 -0
  48. package/ios/engine/BGGeofenceDAO.swift +152 -0
  49. package/ios/engine/BGGeofenceEvent.swift +42 -0
  50. package/ios/engine/BGGeofenceLocationRequest.swift +94 -0
  51. package/ios/engine/BGGeofenceManager.swift +315 -0
  52. package/ios/engine/BGGeofenceTransition.swift +97 -0
  53. package/ios/engine/BGGeofencesChangeEvent.swift +26 -0
  54. package/ios/engine/BGGeolocationConfig.swift +136 -0
  55. package/ios/engine/BGHeartbeatEvent.swift +31 -0
  56. package/ios/engine/BGHeartbeatService.swift +51 -0
  57. package/ios/engine/BGHttpConfig.swift +105 -0
  58. package/ios/engine/BGHttpErrorCodes.swift +63 -0
  59. package/ios/engine/BGHttpEvent.swift +34 -0
  60. package/ios/engine/BGHttpRequest.swift +126 -0
  61. package/ios/engine/BGHttpResponse.swift +93 -0
  62. package/ios/engine/BGHttpService.swift +428 -0
  63. package/ios/engine/BGKalmanFilter.swift +105 -0
  64. package/ios/engine/BGLMActionNames.swift +55 -0
  65. package/ios/engine/BGLicenseManager.swift +26 -0
  66. package/ios/engine/BGLiveActivityManager.swift +327 -0
  67. package/ios/engine/BGLocation.swift +311 -0
  68. package/ios/engine/BGLocationAuthorization.swift +427 -0
  69. package/ios/engine/BGLocationDAO.swift +252 -0
  70. package/ios/engine/BGLocationErrors.swift +28 -0
  71. package/ios/engine/BGLocationEvent.swift +43 -0
  72. package/ios/engine/BGLocationFilter.swift +82 -0
  73. package/ios/engine/BGLocationFilterConfig.swift +57 -0
  74. package/ios/engine/BGLocationHelper.swift +54 -0
  75. package/ios/engine/BGLocationManager.swift +662 -0
  76. package/ios/engine/BGLocationMetricsEngine.swift +116 -0
  77. package/ios/engine/BGLocationRequestService.swift +459 -0
  78. package/ios/engine/BGLocationSatisfier.swift +14 -0
  79. package/ios/engine/BGLocationStreamEvent.swift +27 -0
  80. package/ios/engine/BGLog.swift +337 -0
  81. package/ios/engine/BGLogLevel.swift +26 -0
  82. package/ios/engine/BGLoggerConfig.swift +60 -0
  83. package/ios/engine/BGMotionActivity.swift +31 -0
  84. package/ios/engine/BGMotionActivityClassifier.swift +108 -0
  85. package/ios/engine/BGMotionActivityManagerAdapter.swift +40 -0
  86. package/ios/engine/BGMotionActivitySource.swift +46 -0
  87. package/ios/engine/BGMotionDetector.swift +377 -0
  88. package/ios/engine/BGMotionPermissionManager.swift +50 -0
  89. package/ios/engine/BGNativeLogger.swift +48 -0
  90. package/ios/engine/BGNotificaitons.swift +37 -0
  91. package/ios/engine/BGOdometer.swift +66 -0
  92. package/ios/engine/BGPersistenceConfig.swift +29 -0
  93. package/ios/engine/BGPolygonStreamRequest.swift +48 -0
  94. package/ios/engine/BGPowerSaveChangeEvent.swift +12 -0
  95. package/ios/engine/BGPropertySpec.swift +29 -0
  96. package/ios/engine/BGProviderChangeEvent.swift +31 -0
  97. package/ios/engine/BGQueue.swift +50 -0
  98. package/ios/engine/BGRPC.swift +194 -0
  99. package/ios/engine/BGReachability.swift +58 -0
  100. package/ios/engine/BGResultSet.swift +157 -0
  101. package/ios/engine/BGSchedule.swift +228 -0
  102. package/ios/engine/BGScheduleEvent.swift +13 -0
  103. package/ios/engine/BGScheduler.swift +116 -0
  104. package/ios/engine/BGSingleLocationRequest.swift +49 -0
  105. package/ios/engine/BGStreamLocationRequest.swift +42 -0
  106. package/ios/engine/BGTemplate.swift +54 -0
  107. package/ios/engine/BGTimerService.swift +46 -0
  108. package/ios/engine/BGTrackingAudioManager.swift +286 -0
  109. package/ios/engine/BGTrackingService.swift +879 -0
  110. package/ios/engine/BGWatchPositionRequest.swift +63 -0
  111. package/ios/engine/DatabaseQueue.swift +47 -0
  112. package/ios/engine/LogQuery.swift +10 -0
  113. package/ios/engine/SQLQuery.swift +65 -0
  114. package/ios/engine/TransistorAuthorizationToken.swift +182 -0
  115. package/ios/liveactivity/BGLiveTrackingAttributes.swift +52 -0
  116. package/ios/locationpush/BGLocationPushDeliverer.swift +260 -0
  117. package/ios/locationpush/BGLocationPushService.swift +161 -0
  118. package/ios/locationpush/BGLocationPushShared.swift +98 -0
  119. package/ios/locationpush/BGLocationPushSocketClient.swift +198 -0
  120. package/lib/module/NativeBgGeolocation.js +5 -0
  121. package/lib/module/NativeBgGeolocation.js.map +1 -0
  122. package/lib/module/events.js +20 -0
  123. package/lib/module/events.js.map +1 -0
  124. package/lib/module/index.js +706 -0
  125. package/lib/module/index.js.map +1 -0
  126. package/lib/module/package.json +1 -0
  127. package/lib/module/types.js +2 -0
  128. package/lib/module/types.js.map +1 -0
  129. package/lib/typescript/package.json +1 -0
  130. package/lib/typescript/src/NativeBgGeolocation.d.ts +57 -0
  131. package/lib/typescript/src/NativeBgGeolocation.d.ts.map +1 -0
  132. package/lib/typescript/src/events.d.ts +18 -0
  133. package/lib/typescript/src/events.d.ts.map +1 -0
  134. package/lib/typescript/src/index.d.ts +238 -0
  135. package/lib/typescript/src/index.d.ts.map +1 -0
  136. package/lib/typescript/src/types.d.ts +229 -0
  137. package/lib/typescript/src/types.d.ts.map +1 -0
  138. package/package.json +141 -0
  139. package/src/NativeBgGeolocation.ts +236 -0
  140. package/src/events.ts +17 -0
  141. package/src/index.tsx +935 -0
  142. package/src/types.ts +254 -0
@@ -0,0 +1,327 @@
1
+ import Foundation
2
+ import CoreLocation
3
+ import UIKit
4
+
5
+ #if canImport(ActivityKit)
6
+ import ActivityKit
7
+ #endif
8
+
9
+ @objc public final class BGLiveActivityManager: NSObject {
10
+ private static let tokenDefaultsKey = "BGLocationManager_liveActivityPushToken"
11
+ private static let idDefaultsKey = "BGLocationManager_liveActivityId"
12
+ private static let trackingIdDefaultsKey = "BGLocationManager_liveActivityTrackingId"
13
+
14
+ @objc public static let shared = BGLiveActivityManager()
15
+
16
+ private let stateQueue = DispatchQueue(label: "BGLiveActivityManager.state")
17
+ private var lastUpdateAt: Date = .distantPast
18
+ private var locationCount = 0
19
+ private var pushTokenTask: Task<Void, Never>?
20
+ private var pendingStartIsMoving: Bool?
21
+ private var lastError: String?
22
+
23
+ private override init() {
24
+ super.init()
25
+ NotificationCenter.default.addObserver(
26
+ self,
27
+ selector: #selector(applicationDidBecomeActive),
28
+ name: UIApplication.didBecomeActiveNotification,
29
+ object: nil
30
+ )
31
+ }
32
+
33
+ @objc public var pushToken: String? {
34
+ UserDefaults.standard.string(forKey: Self.tokenDefaultsKey)
35
+ }
36
+
37
+ @objc public var activityId: String? {
38
+ UserDefaults.standard.string(forKey: Self.idDefaultsKey)
39
+ }
40
+
41
+ @objc public func startIfNeeded(isMoving: Bool) {
42
+ guard BGConfig.sharedInstance().app.liveActivityEnabled else { return }
43
+ guard UIApplication.shared.applicationState == .active else {
44
+ pendingStartIsMoving = isMoving
45
+ log("Live Activity waiting for the app to become active")
46
+ return
47
+ }
48
+
49
+ if #available(iOS 16.2, *) {
50
+ Task { @MainActor in
51
+ await self.startOrRecover(isMoving: isMoving)
52
+ }
53
+ } else {
54
+ lastError = "Live Activities require iOS 16.2 or later"
55
+ log(lastError!)
56
+ }
57
+ }
58
+
59
+ @objc private func applicationDidBecomeActive() {
60
+ guard let isMoving = pendingStartIsMoving else { return }
61
+ pendingStartIsMoving = nil
62
+ startIfNeeded(isMoving: isMoving)
63
+ }
64
+
65
+ @objc public func update(
66
+ location: CLLocation?,
67
+ isMoving: Bool,
68
+ activity: String,
69
+ force: Bool
70
+ ) {
71
+ guard BGConfig.sharedInstance().app.liveActivityEnabled else { return }
72
+
73
+ stateQueue.async {
74
+ let minimumInterval = max(5, BGConfig.sharedInstance().app.liveActivityUpdateInterval)
75
+ guard force || Date().timeIntervalSince(self.lastUpdateAt) >= minimumInterval else { return }
76
+ self.lastUpdateAt = Date()
77
+ if location != nil {
78
+ self.locationCount += 1
79
+ }
80
+ let count = self.locationCount
81
+
82
+ if #available(iOS 16.2, *) {
83
+ Task { @MainActor in
84
+ await self.updateActivities(
85
+ location: location,
86
+ isMoving: isMoving,
87
+ activity: activity,
88
+ locationCount: count
89
+ )
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ @objc public func end() {
96
+ pushTokenTask?.cancel()
97
+ pushTokenTask = nil
98
+
99
+ if #available(iOS 16.2, *) {
100
+ Task { @MainActor in
101
+ let final = self.makeState(
102
+ location: BGTrackingService.sharedInstance().lastGoodLocation,
103
+ isMoving: false,
104
+ activity: "stopped",
105
+ locationCount: self.locationCount,
106
+ status: "Tracking stopped"
107
+ )
108
+ let content = ActivityContent(
109
+ state: final,
110
+ staleDate: nil,
111
+ relevanceScore: 0
112
+ )
113
+ for activity in Activity<BGLiveTrackingAttributes>.activities {
114
+ await activity.end(content, dismissalPolicy: .immediate)
115
+ }
116
+ self.clearStoredActivity()
117
+ }
118
+ } else {
119
+ clearStoredActivity()
120
+ }
121
+ }
122
+
123
+ @objc public func stateDictionary() -> [String: Any] {
124
+ let activitiesEnabled: Bool = {
125
+ if #available(iOS 16.2, *) {
126
+ return ActivityAuthorizationInfo().areActivitiesEnabled
127
+ }
128
+ return false
129
+ }()
130
+ let active: Bool = {
131
+ if #available(iOS 16.2, *) {
132
+ return !Activity<BGLiveTrackingAttributes>.activities.isEmpty
133
+ }
134
+ return false
135
+ }()
136
+ var state: [String: Any] = [
137
+ "supported": {
138
+ if #available(iOS 16.2, *) { return true }
139
+ return false
140
+ }(),
141
+ "enabled": BGConfig.sharedInstance().app.liveActivityEnabled,
142
+ "activitiesEnabled": activitiesEnabled,
143
+ "active": active,
144
+ "pushUpdates": BGConfig.sharedInstance().app.liveActivityPushUpdates
145
+ ]
146
+ if let activityId {
147
+ state["activityId"] = activityId
148
+ }
149
+ if let pushToken {
150
+ state["pushToken"] = pushToken
151
+ }
152
+ if let lastError {
153
+ state["error"] = lastError
154
+ }
155
+ return state
156
+ }
157
+
158
+ @available(iOS 16.2, *)
159
+ @MainActor
160
+ private func startOrRecover(isMoving: Bool) async {
161
+ guard ActivityAuthorizationInfo().areActivitiesEnabled else {
162
+ lastError = "Live Activities are disabled in iOS Settings"
163
+ log(lastError!)
164
+ return
165
+ }
166
+
167
+ if let activity = Activity<BGLiveTrackingAttributes>.activities.first {
168
+ lastError = nil
169
+ persist(activity: activity)
170
+ observePushToken(for: activity)
171
+ log("Recovered Live Activity \(activity.id)")
172
+ await updateActivities(
173
+ location: BGTrackingService.sharedInstance().lastGoodLocation,
174
+ isMoving: isMoving,
175
+ activity: BGTrackingService.sharedInstance().currentMotionActivity?.type ?? "unknown",
176
+ locationCount: locationCount
177
+ )
178
+ return
179
+ }
180
+
181
+ let config = BGConfig.sharedInstance().app
182
+ let trackingId = UserDefaults.standard.string(forKey: Self.trackingIdDefaultsKey)
183
+ ?? UUID().uuidString
184
+ let attributes = BGLiveTrackingAttributes(
185
+ trackingId: trackingId,
186
+ title: config.liveActivityTitle,
187
+ subtitle: config.liveActivitySubtitle
188
+ )
189
+ let initial = makeState(
190
+ location: BGTrackingService.sharedInstance().lastGoodLocation,
191
+ isMoving: isMoving,
192
+ activity: BGTrackingService.sharedInstance().currentMotionActivity?.type ?? "unknown",
193
+ locationCount: locationCount
194
+ )
195
+ let staleDate = Date().addingTimeInterval(max(60, config.liveActivityStaleSeconds))
196
+ let content = ActivityContent(state: initial, staleDate: staleDate, relevanceScore: 100)
197
+
198
+ do {
199
+ let pushType: PushType? = config.liveActivityPushUpdates ? .token : nil
200
+ let activity = try Activity.request(
201
+ attributes: attributes,
202
+ content: content,
203
+ pushType: pushType
204
+ )
205
+ lastError = nil
206
+ UserDefaults.standard.set(trackingId, forKey: Self.trackingIdDefaultsKey)
207
+ persist(activity: activity)
208
+ observePushToken(for: activity)
209
+ log("Started Live Activity \(activity.id), pushUpdates=\(config.liveActivityPushUpdates)")
210
+ } catch {
211
+ let firstError = error.localizedDescription
212
+ guard config.liveActivityPushUpdates else {
213
+ lastError = firstError
214
+ log("Unable to start Live Activity: \(firstError)")
215
+ return
216
+ }
217
+
218
+ // A push-enabled request requires the Push Notifications capability.
219
+ // Keep local tracking useful when the host app has not configured it.
220
+ do {
221
+ let activity = try Activity.request(
222
+ attributes: attributes,
223
+ content: content,
224
+ pushType: nil
225
+ )
226
+ lastError = nil
227
+ UserDefaults.standard.set(trackingId, forKey: Self.trackingIdDefaultsKey)
228
+ persist(activity: activity)
229
+ log("Started local Live Activity after push request failed: \(firstError)")
230
+ } catch {
231
+ lastError = error.localizedDescription
232
+ log("Unable to start local Live Activity: \(error.localizedDescription)")
233
+ }
234
+ }
235
+ }
236
+
237
+ @available(iOS 16.2, *)
238
+ @MainActor
239
+ private func updateActivities(
240
+ location: CLLocation?,
241
+ isMoving: Bool,
242
+ activity: String,
243
+ locationCount: Int
244
+ ) async {
245
+ let activities = Activity<BGLiveTrackingAttributes>.activities
246
+ if activities.isEmpty {
247
+ if UIApplication.shared.applicationState == .active {
248
+ await startOrRecover(isMoving: isMoving)
249
+ }
250
+ return
251
+ }
252
+
253
+ let state = makeState(
254
+ location: location,
255
+ isMoving: isMoving,
256
+ activity: activity,
257
+ locationCount: locationCount
258
+ )
259
+ let config = BGConfig.sharedInstance().app
260
+ let content = ActivityContent(
261
+ state: state,
262
+ staleDate: Date().addingTimeInterval(max(60, config.liveActivityStaleSeconds)),
263
+ relevanceScore: isMoving ? 100 : 70
264
+ )
265
+
266
+ for liveActivity in activities {
267
+ persist(activity: liveActivity)
268
+ await liveActivity.update(content)
269
+ }
270
+ }
271
+
272
+ @available(iOS 16.2, *)
273
+ private func makeState(
274
+ location: CLLocation?,
275
+ isMoving: Bool,
276
+ activity: String,
277
+ locationCount: Int,
278
+ status: String? = nil
279
+ ) -> BGLiveTrackingAttributes.ContentState {
280
+ BGLiveTrackingAttributes.ContentState(
281
+ status: status ?? (isMoving ? "Live tracking" : "Stationary"),
282
+ isMoving: isMoving,
283
+ activity: activity.replacingOccurrences(of: "_", with: " "),
284
+ latitude: location?.coordinate.latitude ?? 0,
285
+ longitude: location?.coordinate.longitude ?? 0,
286
+ accuracy: max(0, location?.horizontalAccuracy ?? 0),
287
+ speed: max(0, location?.speed ?? 0),
288
+ distance: BGOdometer.sharedInstance().getOdometer(),
289
+ locationCount: locationCount,
290
+ updatedAt: location?.timestamp ?? Date()
291
+ )
292
+ }
293
+
294
+ @available(iOS 16.2, *)
295
+ private func persist(activity: Activity<BGLiveTrackingAttributes>) {
296
+ UserDefaults.standard.set(activity.id, forKey: Self.idDefaultsKey)
297
+ }
298
+
299
+ @available(iOS 16.2, *)
300
+ private func observePushToken(for activity: Activity<BGLiveTrackingAttributes>) {
301
+ guard BGConfig.sharedInstance().app.liveActivityPushUpdates else { return }
302
+ pushTokenTask?.cancel()
303
+ pushTokenTask = Task {
304
+ for await tokenData in activity.pushTokenUpdates {
305
+ guard !Task.isCancelled else { return }
306
+ let token = tokenData.map { String(format: "%02x", $0) }.joined()
307
+ UserDefaults.standard.set(token, forKey: Self.tokenDefaultsKey)
308
+ BGLog.sharedInstance().notify("Live Activity push token: \(token)", debug: true)
309
+ }
310
+ }
311
+ }
312
+
313
+ private func clearStoredActivity() {
314
+ UserDefaults.standard.removeObject(forKey: Self.idDefaultsKey)
315
+ UserDefaults.standard.removeObject(forKey: Self.trackingIdDefaultsKey)
316
+ UserDefaults.standard.removeObject(forKey: Self.tokenDefaultsKey)
317
+ stateQueue.async {
318
+ self.locationCount = 0
319
+ self.lastUpdateAt = .distantPast
320
+ }
321
+ }
322
+
323
+ private func log(_ message: String) {
324
+ NSLog("[BGGEO][LiveActivity] \(message)")
325
+ BGLog.sharedInstance().notify(message, debug: true)
326
+ }
327
+ }
@@ -0,0 +1,311 @@
1
+ import Foundation
2
+ import CoreLocation
3
+ import UIKit
4
+
5
+ @objc public class BGLocation: NSObject {
6
+
7
+ @objc public var location: CLLocation?
8
+ @objc public var uuid: String = UUID().uuidString
9
+ @objc public var event: String = ""
10
+ @objc public var type: String = ""
11
+ @objc public var isMoving: Bool = false
12
+ @objc public var isSample: Bool = false
13
+ @objc public var isHeartbeat: Bool = false
14
+ @objc public var odometer: Double = 0
15
+ @objc public var odometerError: Double = 0
16
+ @objc public var mock: Bool = false
17
+ @objc public var extras: [String: Any]?
18
+ @objc public var config: AnyObject?
19
+ @objc public var geofence: BGGeofence?
20
+ @objc public var geofenceEvent: BGGeofenceEvent?
21
+ @objc public var rawTimestampMs: Int64 = 0
22
+ @objc public var rawRecordedAtMs: Int64 = 0
23
+
24
+ private var batteryLevel: Float = 0
25
+ private var batteryIsCharging: Bool = false
26
+ private var activityType: String = "unknown"
27
+ private var activityConfidence: Int = 0
28
+ private var cachedDictionary: [String: Any]?
29
+ private var _uptime: TimeInterval = 0
30
+
31
+ @objc public class func isValidCoordinate(_ coordinate: CLLocationCoordinate2D) -> Bool {
32
+ return CLLocationCoordinate2DIsValid(coordinate) &&
33
+ coordinate.latitude != 0 &&
34
+ coordinate.longitude != 0
35
+ }
36
+
37
+ @objc public class func uptime() -> TimeInterval {
38
+ return ProcessInfo.processInfo.systemUptime
39
+ }
40
+
41
+ @objc public override init() {
42
+ super.init()
43
+ setupDefaultValues()
44
+ }
45
+
46
+ @objc public init(location: CLLocation) {
47
+ self.location = location
48
+ super.init()
49
+ setupDefaultValues()
50
+ extractLocationData(location)
51
+ }
52
+
53
+ @objc public init(location: CLLocation, geofence: BGGeofence) {
54
+ self.location = location
55
+ self.geofence = geofence
56
+ super.init()
57
+ setupDefaultValues()
58
+ extractLocationData(location)
59
+ event = "geofence"
60
+ }
61
+
62
+ @objc public init(location: CLLocation, geofenceEvent: BGGeofenceEvent) {
63
+ self.location = location
64
+ self.geofenceEvent = geofenceEvent
65
+ super.init()
66
+ setupDefaultValues()
67
+ extractLocationData(location)
68
+ event = "geofence"
69
+ }
70
+
71
+ @objc public init(location: CLLocation, type: String, extras: [String: Any]?) {
72
+ self.location = location
73
+ self.type = type
74
+ self.extras = extras
75
+ super.init()
76
+ setupDefaultValues()
77
+ extractLocationData(location)
78
+ }
79
+
80
+ @objc public func setupDefaultValues() {
81
+ _uptime = ProcessInfo.processInfo.systemUptime
82
+ updateDeviceStatus()
83
+ }
84
+
85
+ @objc public func configureWithType(_ type: String, extras: [String: Any]?) {
86
+ self.type = type
87
+ self.extras = extras
88
+ invalidateCache()
89
+ }
90
+
91
+ @objc public func extractLocationData(_ loc: CLLocation) {
92
+ rawTimestampMs = Int64(loc.timestamp.timeIntervalSince1970 * 1000)
93
+ rawRecordedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
94
+ }
95
+
96
+ @objc public func updateDeviceStatus() {
97
+ let device = UIDevice.current
98
+ device.isBatteryMonitoringEnabled = true
99
+ batteryLevel = device.batteryLevel
100
+ batteryIsCharging = device.batteryState == .charging || device.batteryState == .full
101
+ }
102
+
103
+ @objc public func updateActivityData() {
104
+ invalidateCache()
105
+ }
106
+
107
+ @objc public func invalidateCache() {
108
+ cachedDictionary = nil
109
+ }
110
+
111
+ @objc public func age() -> TimeInterval {
112
+ guard let loc = location else { return 0 }
113
+ return Date().timeIntervalSince(loc.timestamp)
114
+ }
115
+
116
+ @objc public func timestamp() -> Date {
117
+ return location?.timestamp ?? Date()
118
+ }
119
+
120
+ @objc public func recordedAt() -> Date {
121
+ if rawRecordedAtMs > 0 {
122
+ return Date(timeIntervalSince1970: Double(rawRecordedAtMs) / 1000.0)
123
+ }
124
+ return Date()
125
+ }
126
+
127
+ @objc public func isValidCoordinate(_ coord: CLLocationCoordinate2D) -> Bool {
128
+ return BGLocation.isValidCoordinate(coord)
129
+ }
130
+
131
+ @objc public func toDictionary() -> [String: Any] {
132
+ if let cached = cachedDictionary { return cached }
133
+ let dict = buildDictionary()
134
+ cachedDictionary = dict
135
+ return dict
136
+ }
137
+
138
+ @objc public func buildDictionary() -> [String: Any] {
139
+ var dict: [String: Any] = [:]
140
+ dict["uuid"] = uuid
141
+ dict["event"] = event.isEmpty ? NSNull() : event
142
+ dict["is_moving"] = isMoving
143
+ dict["odometer"] = odometer
144
+
145
+ if let loc = location {
146
+ dict["coords"] = buildCoordinatesDictionary()
147
+ dict["timestamp"] = ISO8601DateFormatter().string(from: loc.timestamp)
148
+ }
149
+
150
+ dict["activity"] = activityDictionary()
151
+ dict["battery"] = batteryDictionary()
152
+ dict["timestamp_meta"] = timestampMetaDictionary()
153
+
154
+ if mock { dict["mock"] = true }
155
+ if isSample { dict["sample"] = true }
156
+
157
+ if let geofenceEvent = geofenceEvent {
158
+ dict["geofence"] = geofenceTemplateData()
159
+ }
160
+
161
+ if let extras = extras {
162
+ dict["extras"] = extras
163
+ }
164
+
165
+ return dict
166
+ }
167
+
168
+ @objc public func buildCoordinatesDictionary() -> [String: Any] {
169
+ guard let loc = location else { return [:] }
170
+ var coords: [String: Any] = [:]
171
+ coords["latitude"] = loc.coordinate.latitude
172
+ coords["longitude"] = loc.coordinate.longitude
173
+ coords["accuracy"] = loc.horizontalAccuracy
174
+ coords["altitude"] = loc.altitude
175
+ coords["altitude_accuracy"] = loc.verticalAccuracy
176
+ coords["heading"] = loc.course
177
+ coords["speed"] = loc.speed
178
+ if let heading = speedAccuracyNumber(loc) { coords["speed_accuracy"] = heading }
179
+ if let headingAccuracy = headingAccuracyNumber(loc) { coords["heading_accuracy"] = headingAccuracy }
180
+ if let ellipsoidal = ellipsoidalAltitudeNumber(loc) { coords["ellipsoidal_altitude"] = ellipsoidal }
181
+ return coords
182
+ }
183
+
184
+ @objc public func activityDictionary() -> [String: Any] {
185
+ return ["type": activityType, "confidence": activityConfidence]
186
+ }
187
+
188
+ @objc public func batteryDictionary() -> [String: Any] {
189
+ return ["level": batteryLevel, "is_charging": batteryIsCharging]
190
+ }
191
+
192
+ @objc public func timestampMetaDictionary() -> [String: Any] {
193
+ var meta: [String: Any] = [:]
194
+ meta["system_time"] = Date().timeIntervalSince1970 * 1000
195
+ meta["system_clock_elapsed"] = ProcessInfo.processInfo.systemUptime * 1000
196
+ if rawTimestampMs > 0 { meta["event_elapsed"] = Double(rawTimestampMs) }
197
+ if rawRecordedAtMs > 0 { meta["recorded_at"] = Double(rawRecordedAtMs) }
198
+ return meta
199
+ }
200
+
201
+ @objc public func timestampMetaJSONString() -> String {
202
+ let dict = timestampMetaDictionary()
203
+ guard let data = try? JSONSerialization.data(withJSONObject: dict) else { return "{}" }
204
+ return String(data: data, encoding: .utf8) ?? "{}"
205
+ }
206
+
207
+ @objc public func geofenceTemplateData() -> [String: Any] {
208
+ guard let event = geofenceEvent else { return [:] }
209
+ return event.toDictionary()
210
+ }
211
+
212
+ @objc public func locationTemplateData() -> [String: Any] {
213
+ return toDictionary()
214
+ }
215
+
216
+ @objc public func combinedGeofenceExtras() -> [String: Any]? {
217
+ var combined: [String: Any] = [:]
218
+ if let e = extras { combined.merge(e) { $1 } }
219
+ if let ge = geofenceEvent?.extras as? [String: Any] { combined.merge(ge) { $1 } }
220
+ return combined.isEmpty ? nil : combined
221
+ }
222
+
223
+ @objc public func toJson(_ error: UnsafeMutablePointer<NSError?>?) -> String? {
224
+ let dict = toDictionary()
225
+ guard let data = try? JSONSerialization.data(withJSONObject: dict) else { return nil }
226
+ return String(data: data, encoding: .utf8)
227
+ }
228
+
229
+ @objc public func serializeClassicFormat(_ error: UnsafeMutablePointer<NSError?>?) -> Data? {
230
+ let dict = toDictionary()
231
+ return try? JSONSerialization.data(withJSONObject: dict)
232
+ }
233
+
234
+ @objc public func serializeJSONObject(_ error: UnsafeMutablePointer<NSError?>?) -> Data? {
235
+ let dict = toDictionary()
236
+ return try? JSONSerialization.data(withJSONObject: dict)
237
+ }
238
+
239
+ @objc public func renderTemplate(_ template: BGTemplate, error: UnsafeMutablePointer<NSError?>?) -> String? {
240
+ let data = toDictionary()
241
+ let rendered = template.render(with: data)
242
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: rendered) else { return nil }
243
+ return String(data: jsonData, encoding: .utf8)
244
+ }
245
+
246
+ @objc public func finalizeTemplateJSON(_ json: String, error: UnsafeMutablePointer<NSError?>?) -> String? {
247
+ return json
248
+ }
249
+
250
+ @objc public func handleTemplateError(_ error: Error, template: BGTemplate) {
251
+ }
252
+
253
+ @objc public func selectTemplate() -> BGTemplate? {
254
+ return nil
255
+ }
256
+
257
+ @objc public func templateName(forTemplate template: BGTemplate) -> String {
258
+ return template.name
259
+ }
260
+
261
+ @objc public func processLocation(_ location: CLLocation, error: UnsafeMutablePointer<NSError?>?) -> Bool {
262
+ self.location = location
263
+ extractLocationData(location)
264
+ invalidateCache()
265
+ return true
266
+ }
267
+
268
+ @objc public func mergeExtras(_ newExtras: [String: Any]) {
269
+ if extras == nil { extras = [:] }
270
+ extras?.merge(newExtras) { $1 }
271
+ invalidateCache()
272
+ }
273
+
274
+ @objc public func applyExtras(toDictionary dict: NSMutableDictionary) {
275
+ guard let extras = extras else { return }
276
+ dict["extras"] = extras
277
+ }
278
+
279
+ @objc public func applyExtras(toArray array: NSMutableArray) {
280
+ }
281
+
282
+ @objc public func applyExtras(toJSONObject obj: NSMutableDictionary) {
283
+ applyExtras(toDictionary: obj)
284
+ }
285
+
286
+ @objc public func errorWithCode(_ code: Int, description: String) -> NSError {
287
+ return NSError(domain: "BGLocation", code: code, userInfo: [NSLocalizedDescriptionKey: description])
288
+ }
289
+
290
+ @objc public func roundValue(_ value: Double) -> Double {
291
+ return (value * 10000).rounded() / 10000
292
+ }
293
+
294
+ func speedAccuracyNumber(_ loc: CLLocation) -> Double? {
295
+ if #available(iOS 13.4, *) {
296
+ return loc.speedAccuracy >= 0 ? loc.speedAccuracy : nil
297
+ }
298
+ return nil
299
+ }
300
+
301
+ func headingAccuracyNumber(_ loc: CLLocation) -> Double? {
302
+ return loc.courseAccuracy >= 0 ? loc.courseAccuracy : nil
303
+ }
304
+
305
+ func ellipsoidalAltitudeNumber(_ loc: CLLocation) -> Double? {
306
+ if #available(iOS 15.0, *) {
307
+ return loc.ellipsoidalAltitude
308
+ }
309
+ return nil
310
+ }
311
+ }