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,879 @@
1
+ import Foundation
2
+ import CoreLocation
3
+ import UIKit
4
+
5
+ @objc public class BGTrackingService: NSObject, CLLocationManagerDelegate {
6
+
7
+ private static var _sharedInstance: BGTrackingService?
8
+ private static let instanceLock = NSLock()
9
+
10
+ @objc public class func sharedInstance() -> BGTrackingService {
11
+ instanceLock.lock()
12
+ defer { instanceLock.unlock() }
13
+ if _sharedInstance == nil { _sharedInstance = BGTrackingService() }
14
+ return _sharedInstance!
15
+ }
16
+
17
+ // MARK: - State
18
+
19
+ @objc public var locationManager: CLLocationManager?
20
+ @objc public var locationFilter: BGLocationFilter?
21
+ @objc public var locationMetrics: BGLocationMetricsEngine?
22
+ @objc public var beforeInsertBlock: ((BGLocation) -> BGLocation?)?
23
+
24
+ @objc public var isEnabled: Bool = false
25
+ @objc public var isMoving: Bool = false
26
+ @objc public var isUpdatingLocation: Bool = false
27
+ @objc public var isMonitoringSignificantLocationChanges: Bool = false
28
+ @objc public var isMonitoringBackgroundFetch: Bool = false
29
+ @objc public var didReceiveFirstLocation: Bool = false
30
+ @objc public var isAwaitingAuthorizationForChangePace: Bool = false
31
+
32
+ @objc public var lastLocation: CLLocation?
33
+ @objc public var lastGoodLocation: CLLocation?
34
+ @objc public var lastOdometerLocation: CLLocation?
35
+ @objc public var bestLocation: CLLocation?
36
+ @objc public var stationaryLocation: CLLocation?
37
+ @objc public var prevStationaryLocation: CLLocation?
38
+ @objc public var stationaryRegion: CLCircularRegion?
39
+ @objc public var motionChangeRequest: BGCurrentPositionRequest?
40
+ @objc public var motionStateRequest: BGCurrentPositionRequest?
41
+ @objc public var currentMotionActivity: BGMotionActivity?
42
+ @objc public var locationError: Error?
43
+ @objc public var pendingIsMovingForAuthorization: NSNumber?
44
+ @objc public var preventSuspendTask: Any?
45
+
46
+ @objc public var distanceFilter: CLLocationDistance = 10
47
+ @objc public var medianLocationAccuracy: CLLocationAccuracy = 0
48
+ @objc public var lastLocationTimeInterval: TimeInterval = 0
49
+ @objc public var startedAcquiringLocationAt: Date?
50
+ @objc public var stopUpdatingLocationAt: Date?
51
+ @objc public var stoppedAt: Date?
52
+ @objc public var stopOnNextStationary: Bool = false
53
+
54
+ private var configChangeBufferTimer: Timer?
55
+ private var startDetectionTimer: Timer?
56
+ private var stopDetectionDelayTimer: Timer?
57
+ private var stopTimeoutTimer: Timer?
58
+ private var motionTriggerTimer: Timer?
59
+ private var motionObserver: NSObjectProtocol?
60
+ private var locationAccuracyQueue: [CLLocationAccuracy] = []
61
+ private let stateQueue = DispatchQueue(label: "BGTrackingService.state", attributes: .concurrent)
62
+ private let clMutationQueue = DispatchQueue(label: "BGTrackingService.cl")
63
+
64
+ @objc public override init() {
65
+ super.init()
66
+ locationFilter = BGLocationFilter()
67
+ locationMetrics = BGLocationMetricsEngine()
68
+ }
69
+
70
+ // MARK: - Start / Stop
71
+
72
+ @objc public func start(_ isMoving: Bool) {
73
+ guard !isEnabled else { return }
74
+ isEnabled = true
75
+ self.isMoving = isMoving
76
+ didReceiveFirstLocation = false
77
+ loadStationaryLocation()
78
+
79
+ registerConfigChangeHandlers()
80
+ registerEventBusHandlers()
81
+
82
+ _mutateCL { mgr in
83
+ // NOTE: the delegate is owned solely by BGCLRouter (see
84
+ // BGLocationManager.setupCoreLocation). Do NOT assign it here.
85
+ self.configureLocationManager(mgr)
86
+ self.startMonitoringSignificantLocationChanges()
87
+ if let stationary = self.stationaryLocation, !isMoving {
88
+ self.startMonitoringStationaryRegion(stationary, radius: BGConfig.sharedInstance().geolocation.stationaryRadius)
89
+ }
90
+ if BGConfig.sharedInstance().geolocation.useSignificantChangesOnly {
91
+ self.stopUpdatingLocation()
92
+ } else {
93
+ self.startUpdatingLocation()
94
+ }
95
+ if isMoving {
96
+ self.beginStartDetection(isMoving)
97
+ }
98
+ }
99
+ startMotionMonitoring()
100
+ evaluateHeartbeatTimer()
101
+ }
102
+
103
+ @objc public func stop() {
104
+ guard isEnabled else { return }
105
+ isEnabled = false
106
+ stopDetection()
107
+ stopMotionMonitoring()
108
+ _mutateCL { mgr in
109
+ self.stopUpdatingLocation()
110
+ self.stopMonitoringSignificantLocationChanges()
111
+ self.stopMonitoringStationaryRegion()
112
+ }
113
+ stopHeartbeat()
114
+ }
115
+
116
+ // MARK: - Motion activity monitoring
117
+ //
118
+ // Drives autonomous stationary<->moving transitions from CMMotionActivity
119
+ // (the motion coprocessor). Previously BGMotionDetector was never started
120
+ // and its updates were observed by nobody, so the only way to detect motion
121
+ // onset was a coarse stationary-region exit. Now the detector runs while
122
+ // tracking is enabled and feeds onMotionActivityChange -> changePace.
123
+
124
+ @objc public func startMotionMonitoring() {
125
+ let detector = BGMotionDetector.sharedInstance()
126
+ detector.start()
127
+ if motionObserver == nil {
128
+ motionObserver = NotificationCenter.default.addObserver(
129
+ forName: NSNotification.Name("BGMotionDetectorDidUpdateActivity"),
130
+ object: nil, queue: .main
131
+ ) { [weak self] note in
132
+ if let activity = note.object as? BGMotionActivity {
133
+ self?.onMotionActivityChange(activity)
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ @objc public func stopMotionMonitoring() {
140
+ BGMotionDetector.sharedInstance().stop()
141
+ if let obs = motionObserver {
142
+ NotificationCenter.default.removeObserver(obs)
143
+ motionObserver = nil
144
+ }
145
+ }
146
+
147
+ // MARK: - CL mutation
148
+
149
+ @objc public func _mutateCL(_ block: @escaping (CLLocationManager) -> Void) {
150
+ clMutationQueue.async {
151
+ guard let mgr = self.locationManager else { return }
152
+ DispatchQueue.main.async { block(mgr) }
153
+ }
154
+ }
155
+
156
+ @objc public func configureLocationManager(_ manager: CLLocationManager) {
157
+ let config = BGConfig.sharedInstance().geolocation
158
+ manager.desiredAccuracy = config.desiredAccuracy
159
+ manager.distanceFilter = config.distanceFilter
160
+ manager.activityType = config.activityType
161
+ manager.pausesLocationUpdatesAutomatically = config.pausesLocationUpdatesAutomatically
162
+ if #available(iOS 9.0, *) {
163
+ manager.allowsBackgroundLocationUpdates = BGAppState.sharedInstance().hasBackgroundLocationMode()
164
+ }
165
+ if #available(iOS 11.0, *) {
166
+ manager.showsBackgroundLocationIndicator = config.showsBackgroundLocationIndicator
167
+ }
168
+ distanceFilter = config.distanceFilter
169
+ }
170
+
171
+ // MARK: - Location updating
172
+
173
+ @objc public func startUpdatingLocation() {
174
+ guard !isUpdatingLocation else { return }
175
+ _mutateCL { mgr in
176
+ mgr.startUpdatingLocation()
177
+ self.isUpdatingLocation = true
178
+ self.startedAcquiringLocationAt = Date()
179
+ }
180
+ }
181
+
182
+ @objc public func stopUpdatingLocation() {
183
+ guard isUpdatingLocation else { return }
184
+ _mutateCL { mgr in
185
+ mgr.stopUpdatingLocation()
186
+ self.isUpdatingLocation = false
187
+ self.stopUpdatingLocationAt = Date()
188
+ }
189
+ }
190
+
191
+ /// Re-assert tracking's intended continuous-updates state on the shared
192
+ /// CLLocationManager. Called by BGLocationRequestService after a
193
+ /// getCurrentPosition fix completes, so a one-off fix powering up GPS does
194
+ /// not leave it running (or off) against tracking's wishes.
195
+ @objc public func restoreUpdatingState() {
196
+ guard isEnabled else { return }
197
+ let geo = BGConfig.sharedInstance().geolocation
198
+ _mutateCL { mgr in
199
+ if geo.disableStopDetection || (self.isMoving && !geo.useSignificantChangesOnly) {
200
+ mgr.startUpdatingLocation()
201
+ self.isUpdatingLocation = true
202
+ } else {
203
+ mgr.stopUpdatingLocation()
204
+ self.isUpdatingLocation = false
205
+ }
206
+ }
207
+ }
208
+
209
+ @objc public func startMonitoringSignificantLocationChanges() {
210
+ guard !isMonitoringSignificantLocationChanges else { return }
211
+ _mutateCL { mgr in
212
+ mgr.startMonitoringSignificantLocationChanges()
213
+ self.isMonitoringSignificantLocationChanges = true
214
+ }
215
+ }
216
+
217
+ @objc public func stopMonitoringSignificantLocationChanges() {
218
+ guard isMonitoringSignificantLocationChanges else { return }
219
+ _mutateCL { mgr in
220
+ mgr.stopMonitoringSignificantLocationChanges()
221
+ self.isMonitoringSignificantLocationChanges = false
222
+ }
223
+ }
224
+
225
+ @objc public func startMonitoringBackgroundFetch() {
226
+ isMonitoringBackgroundFetch = true
227
+ }
228
+
229
+ @objc public func stopMonitoringBackgroundFetch() {
230
+ isMonitoringBackgroundFetch = false
231
+ }
232
+
233
+ // MARK: - Stationary region
234
+
235
+ @objc public func startMonitoringStationaryRegion(_ location: CLLocation, radius: CLLocationDistance) {
236
+ // iOS CLCircularRegion monitoring is cell/wifi-coarse and unreliable
237
+ // below ~100-150m: a small radius either never fires the real exit or
238
+ // fires spurious exits from GPS jitter. The stationary region is the
239
+ // primary mechanism that wakes a suspended or system-terminated app when the user
240
+ // departs, so clamp it to a dependable minimum (decoupled from the
241
+ // motion stop/start `stationaryRadius` distance logic).
242
+ let minReliableRadius: CLLocationDistance = 200.0
243
+ let effectiveRadius = max(radius, minReliableRadius)
244
+ let region = CLCircularRegion(center: location.coordinate, radius: effectiveRadius, identifier: "BGStationary")
245
+ region.notifyOnExit = true
246
+ stationaryRegion = region
247
+ _mutateCL { mgr in
248
+ mgr.startMonitoring(for: region)
249
+ }
250
+ }
251
+
252
+ func stopMonitoringStationaryRegion() {
253
+ if let region = stationaryRegion {
254
+ _mutateCL { mgr in
255
+ mgr.stopMonitoring(for: region)
256
+ }
257
+ stationaryRegion = nil
258
+ }
259
+ }
260
+
261
+ @objc public func stationaryRegionContains(location: CLLocation) -> Bool {
262
+ guard let region = stationaryRegion else { return false }
263
+ return region.contains(location.coordinate)
264
+ }
265
+
266
+ @objc public func locationIsBeyondStationaryRegion(_ location: CLLocation) -> Bool {
267
+ guard let region = stationaryRegion else { return true }
268
+ return !region.contains(location.coordinate)
269
+ }
270
+
271
+ // MARK: - Pace change
272
+
273
+ @objc public func changePace(_ moving: Bool) {
274
+ if isEnabled {
275
+ setMoving(moving)
276
+ } else {
277
+ pendingIsMovingForAuthorization = NSNumber(value: moving)
278
+ }
279
+ }
280
+
281
+ private func setMoving(_ moving: Bool) {
282
+ // Continuous keep-alive ("ride app") mode: never relinquish the moving
283
+ // state, so GPS stays on and the app stays alive in the background with
284
+ // the location indicator. Ignore stationary transitions entirely.
285
+ if !moving && BGConfig.sharedInstance().geolocation.disableStopDetection {
286
+ return
287
+ }
288
+ let wasMoving = isMoving
289
+ isMoving = moving
290
+
291
+ if moving && !wasMoving {
292
+ beginStartDetection(moving)
293
+ } else if !moving && wasMoving {
294
+ beginStopDetection()
295
+ }
296
+ }
297
+
298
+ // MARK: - Start detection
299
+
300
+ @objc public func beginStartDetection() {
301
+ beginStartDetection(true)
302
+ }
303
+
304
+ @objc public func beginStartDetection(_ moving: Bool) {
305
+ endStopDetection()
306
+ stopMonitoringStationaryRegion()
307
+ startUpdatingLocation()
308
+ startMotionTriggerTimer()
309
+ }
310
+
311
+ @objc public func endStartDetection() {
312
+ startDetectionTimer?.invalidate()
313
+ startDetectionTimer = nil
314
+ }
315
+
316
+ @objc public func detectStartMotion(_ location: CLLocation) {
317
+ guard !isMoving else { return }
318
+ isMoving = true
319
+ onMotionChangeSuccess(true, location: location, didPersist: false)
320
+ }
321
+
322
+ // MARK: - Stop detection
323
+
324
+ @objc public func beginStopDetection() {
325
+ stopDetectionDelayTimer?.invalidate()
326
+ let config = BGConfig.sharedInstance()
327
+ let stopTimeout = config.geolocation.stopTimeout * 60
328
+
329
+ stopDetectionDelayTimer = Timer.scheduledTimer(withTimeInterval: stopTimeout, repeats: false) { [weak self] _ in
330
+ self?.detectStopMotion(nil)
331
+ }
332
+ }
333
+
334
+ @objc public func endStopDetection() {
335
+ stopDetectionDelayTimer?.invalidate()
336
+ stopDetectionDelayTimer = nil
337
+ stopTimeoutTimer?.invalidate()
338
+ stopTimeoutTimer = nil
339
+ }
340
+
341
+ func stopDetection() {
342
+ endStartDetection()
343
+ endStopDetection()
344
+ stopMotionTriggerTimer()
345
+ }
346
+
347
+ @objc public func beginStopDetectionDelayTimer() {
348
+ beginStopDetection()
349
+ }
350
+
351
+ @objc public func detectStopMotion(_ location: CLLocation?) {
352
+ guard isMoving else { return }
353
+ isMoving = false
354
+ let loc = location ?? lastGoodLocation
355
+ onMotionChangeSuccess(false, location: loc, didPersist: false)
356
+ }
357
+
358
+ @objc public func onStopTimeout() {
359
+ detectStopMotion(lastGoodLocation)
360
+ }
361
+
362
+ @objc public func resetStopDetectionDelayTimer() {
363
+ stopDetectionDelayTimer?.invalidate()
364
+ beginStopDetection()
365
+ }
366
+
367
+ @objc public func resetStopTimeoutTimer() {
368
+ stopTimeoutTimer?.invalidate()
369
+ }
370
+
371
+ // MARK: - Motion trigger timer
372
+
373
+ @objc public func startMotionTriggerTimer() {
374
+ motionTriggerTimer?.invalidate()
375
+ let interval: TimeInterval = 30.0
376
+ DispatchQueue.main.async {
377
+ self.motionTriggerTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
378
+ self?.onMotionTrigger()
379
+ }
380
+ }
381
+ }
382
+
383
+ @objc public func stopMotionTriggerTimer() {
384
+ motionTriggerTimer?.invalidate()
385
+ motionTriggerTimer = nil
386
+ }
387
+
388
+ @objc public func resetMotionTriggerTimer() {
389
+ stopMotionTriggerTimer()
390
+ startMotionTriggerTimer()
391
+ }
392
+
393
+ @objc public func onMotionTrigger() {
394
+ guard isEnabled else { return }
395
+ if let best = bestLocation {
396
+ detectStartMotion(best)
397
+ }
398
+ }
399
+
400
+ // MARK: - Heartbeat
401
+
402
+ @objc public func beginHeartbeat() {
403
+ let config = BGConfig.sharedInstance()
404
+ let interval = config.app.heartbeatInterval
405
+ BGHeartbeatService.sharedInstance().startWithInterval(interval) { [weak self] _ in
406
+ self?.onHeartbeat()
407
+ }
408
+ }
409
+
410
+ @objc public func stopHeartbeat() {
411
+ BGHeartbeatService.sharedInstance().stop()
412
+ }
413
+
414
+ @objc public func onHeartbeat() {
415
+ // Renew the keep-alive background task so the next interval can fire
416
+ // while briefly suspended (best-effort; true kill-state longevity comes
417
+ // from SLC/region wake, not from background tasks).
418
+ if BGConfig.sharedInstance().app.preventSuspend {
419
+ BGBackgroundTaskManager.sharedInstance().renewPreventSuspend()
420
+ }
421
+ // Emit the typed event the bridge/listener expects (a BGHeartbeatEvent,
422
+ // not a raw dictionary — the previous payload type never matched and so
423
+ // never reached JS onHeartbeat).
424
+ let event = BGHeartbeatEvent(location: lastLocation ?? lastGoodLocation)
425
+ BGEventBus.sharedInstance().emit(BGEventNames.heartbeat, payload: event)
426
+ }
427
+
428
+ @objc public func evaluateHeartbeatTimer() {
429
+ let config = BGConfig.sharedInstance()
430
+ if config.app.heartbeatInterval > 0 && isEnabled {
431
+ beginHeartbeat()
432
+ } else {
433
+ stopHeartbeat()
434
+ }
435
+ }
436
+
437
+ // MARK: - Location processing
438
+
439
+ @objc public func locationIsGoodEnough(_ location: CLLocation) -> Bool {
440
+ let config = BGConfig.sharedInstance()
441
+ let desiredAccuracy = config.geolocation.desiredAccuracy
442
+ return location.horizontalAccuracy <= desiredAccuracy || desiredAccuracy == 0
443
+ }
444
+
445
+ @objc public func calculateDistanceFilter(_ location: CLLocation) -> CLLocationDistance {
446
+ let config = BGConfig.sharedInstance()
447
+ let base = config.geolocation.distanceFilter
448
+ if config.geolocation.disableElasticity { return base }
449
+ let speed = max(0, location.speed)
450
+ let multiplier = config.geolocation.elasticityMultiplier
451
+ let elastic = base + speed * multiplier
452
+ return elastic
453
+ }
454
+
455
+ @objc public func calculateMedianLocationAccuracy(_ accuracies: [CLLocationAccuracy]) -> CLLocationAccuracy {
456
+ guard !accuracies.isEmpty else { return 0 }
457
+ let sorted = accuracies.sorted()
458
+ let mid = sorted.count / 2
459
+ return sorted.count % 2 == 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
460
+ }
461
+
462
+ @objc public func applyDistanceFilter(_ filter: CLLocationDistance) {
463
+ distanceFilter = filter
464
+ _mutateCL { mgr in mgr.distanceFilter = filter }
465
+ }
466
+
467
+ @objc public func persistLocation(_ location: BGLocation) {
468
+ var didPersist = false
469
+ if let block = beforeInsertBlock {
470
+ guard let modified = block(location) else { return }
471
+ didPersist = BGLocationDAO.sharedInstance().create(modified, error: nil)
472
+ } else {
473
+ didPersist = BGLocationDAO.sharedInstance().create(location, error: nil)
474
+ }
475
+
476
+ let httpConfig = BGConfig.sharedInstance().http
477
+ if didPersist && httpConfig.autoSync && httpConfig.hasValidUrl {
478
+ let http = BGHttpService.sharedInstance()
479
+ if isInBackground() || BGAppState.sharedInstance().didLaunchInBackground {
480
+ http.flushForBackgroundWake()
481
+ } else {
482
+ http.flush()
483
+ }
484
+ }
485
+ }
486
+
487
+ @objc public func persistStationaryLocation(_ location: CLLocation) {
488
+ let tsLocation = BGLocation(location: location, type: "stationary", extras: nil)
489
+ _ = BGLocationDAO.sharedInstance().create(tsLocation, error: nil)
490
+ stationaryLocation = location
491
+ UserDefaults.standard.set(try? JSONSerialization.data(withJSONObject: tsLocation.toDictionary()), forKey: "BGLocationManager_stationary")
492
+ if BGConfig.sharedInstance().http.autoSync && BGConfig.sharedInstance().http.hasValidUrl {
493
+ let http = BGHttpService.sharedInstance()
494
+ if isInBackground() || BGAppState.sharedInstance().didLaunchInBackground {
495
+ http.flushForBackgroundWake()
496
+ } else {
497
+ http.flush()
498
+ }
499
+ }
500
+ }
501
+
502
+ @objc public func loadStationaryLocation() {
503
+ if let data = UserDefaults.standard.data(forKey: "BGLocationManager_stationary"),
504
+ let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
505
+ stationaryLocation = BGLocationDAO.sharedInstance().inflate(dict).location
506
+ }
507
+ }
508
+
509
+ @objc public func destroyStationaryLocation() {
510
+ UserDefaults.standard.removeObject(forKey: "BGLocationManager_stationary")
511
+ stationaryLocation = nil
512
+ }
513
+
514
+ // MARK: - Motion change
515
+
516
+ @objc public func onMotionChangeSuccess(_ moving: Bool, location: CLLocation?, didPersist: Bool) {
517
+ let loc = location ?? lastGoodLocation
518
+ let tsLocation = loc.map { BGLocation(location: $0, type: "motionchange", extras: nil) }
519
+ tsLocation?.isMoving = moving
520
+
521
+ if let tsLoc = tsLocation, !didPersist {
522
+ persistLocation(tsLoc)
523
+ }
524
+
525
+ // Deliver BGLocation object to RN module listeners (onMotionChange: registered via BGEventManager)
526
+ if let tsLoc = tsLocation {
527
+ BGEventManager.sharedInstance().trigger(BGEventNames.motionChangeComplete, payload: tsLoc)
528
+ }
529
+ // Keep BGEventBus emission for internal engine consumers
530
+ BGEventBus.sharedInstance().emit(BGEventNames.motionChangeComplete, payload: [
531
+ "isMoving": moving,
532
+ "location": tsLocation?.toDictionary() ?? [:]
533
+ ])
534
+
535
+ if moving {
536
+ stopMonitoringStationaryRegion()
537
+ startMonitoringSignificantLocationChanges()
538
+ startUpdatingLocation()
539
+ } else {
540
+ // Going stationary: persist the stop, arm the wake mechanisms (SLC +
541
+ // stationary region), and CRITICALLY tear down the high-power
542
+ // continuous GPS stream — otherwise full-accuracy GPS runs forever
543
+ // while parked (the biggest iOS battery drain, and the exact thing
544
+ // the stationary state exists to prevent).
545
+ if let loc = loc {
546
+ let config = BGConfig.sharedInstance()
547
+ persistStationaryLocation(loc)
548
+ startMonitoringStationaryRegion(loc, radius: config.geolocation.stationaryRadius)
549
+ }
550
+ startMonitoringSignificantLocationChanges()
551
+ // Keep GPS running in continuous keep-alive mode; otherwise tear it
552
+ // down to save battery and rely on SLC/region wakeups.
553
+ if !BGConfig.sharedInstance().geolocation.disableStopDetection {
554
+ stopUpdatingLocation()
555
+ }
556
+ }
557
+ // Heartbeat lifecycle is owned by evaluateHeartbeatTimer() for the whole
558
+ // enabled session, so it keeps firing across pace changes (including
559
+ // while stationary, which is when it matters most).
560
+ }
561
+
562
+ @objc public func onMotionChangeError(_ error: Error) {
563
+ locationError = error
564
+ BGEventBus.sharedInstance().emit(BGEventNames.motionChangeError, payload: ["error": error.localizedDescription])
565
+ }
566
+
567
+ // MARK: - Current position
568
+
569
+ @objc public func getCurrentPosition(_ request: BGCurrentPositionRequest) {
570
+ BGLocationRequestService.sharedInstance().requestLocation(request)
571
+ }
572
+
573
+ // MARK: - Location error event
574
+
575
+ @objc public func fireLocationErrorEvent(_ error: Error) {
576
+ BGEventBus.sharedInstance().emit(BGEventNames.locationError, payload: ["error": error.localizedDescription])
577
+ }
578
+
579
+ // MARK: - Odometer
580
+
581
+ @objc public func setOdometer(_ value: Double, request: BGCurrentPositionRequest?) {
582
+ BGOdometer.sharedInstance().setOdometer(value, location: lastGoodLocation)
583
+ lastOdometerLocation = lastGoodLocation
584
+ }
585
+
586
+ // MARK: - Schedule
587
+
588
+ @objc public func startSchedule() {
589
+ _ = BGScheduler.sharedInstance().start(withSchedule: BGConfig.sharedInstance().app.schedule)
590
+ }
591
+
592
+ @objc public func startGeofences() {
593
+ BGGeofenceManager.sharedInstance().start()
594
+ }
595
+
596
+ // MARK: - Significant location monitoring
597
+
598
+ @objc public func onStopMonitoringSLC() {
599
+ stopMonitoringSignificantLocationChanges()
600
+ }
601
+
602
+ // MARK: - Config change handlers
603
+
604
+ @objc public func registerConfigChangeHandlers() {
605
+ }
606
+
607
+ @objc public func registerEventBusHandlers() {
608
+ }
609
+
610
+ @objc public func onChangeDesiredAccuracy(_ accuracy: CLLocationAccuracy) {
611
+ _mutateCL { mgr in mgr.desiredAccuracy = accuracy }
612
+ }
613
+
614
+ @objc public func onChangeDistanceFilter(_ filter: CLLocationDistance) {
615
+ applyDistanceFilter(filter)
616
+ }
617
+
618
+ @objc public func onChangeActivityType(_ type: String) {
619
+ let activityType = BGGeolocationConfig.activityType(fromString: type)
620
+ _mutateCL { mgr in mgr.activityType = activityType }
621
+ }
622
+
623
+ @objc public func onChangePausesLocationUpdatesAutomatically(_ pauses: Bool) {
624
+ _mutateCL { mgr in mgr.pausesLocationUpdatesAutomatically = pauses }
625
+ }
626
+
627
+ @objc public func onChangeShowsBackgroundLocationIndicator(_ shows: Bool) {
628
+ if #available(iOS 11.0, *) {
629
+ _mutateCL { mgr in mgr.showsBackgroundLocationIndicator = shows }
630
+ }
631
+ }
632
+
633
+ @objc public func onChangeUseSignificantChangesOnly(_ enabled: Bool) {
634
+ if enabled {
635
+ stopUpdatingLocation()
636
+ startMonitoringSignificantLocationChanges()
637
+ } else {
638
+ stopMonitoringSignificantLocationChanges()
639
+ startUpdatingLocation()
640
+ }
641
+ }
642
+
643
+ @objc public func onChangeLocationAuthorizationRequest(_ request: String) {
644
+ }
645
+
646
+ @objc public func onChangeHeartbeatInterval(_ interval: TimeInterval) {
647
+ evaluateHeartbeatTimer()
648
+ }
649
+
650
+ @objc public func onChangeActivityRecognitionInterval(_ interval: TimeInterval) {
651
+ }
652
+
653
+ @objc public func onChangeDisableMotionActivityUpdates(_ disabled: Bool) {
654
+ }
655
+
656
+ @objc public func onChangeElasticity(_ enabled: Bool) {
657
+ }
658
+
659
+ @objc public func onChangePreventSuspend(_ enabled: Bool) {
660
+ }
661
+
662
+ @objc public func onChangeScheduleEvent(_ event: BGScheduleEvent) {
663
+ }
664
+
665
+ // MARK: - Event handlers
666
+
667
+ @objc public func onScheduleEvent(_ event: BGScheduleEvent) {
668
+ }
669
+
670
+ @objc public func onMotionActivityChange(_ activity: BGMotionActivity) {
671
+ currentMotionActivity = activity
672
+ // Surface the activitychange event to JS (nothing emitted this before,
673
+ // so BackgroundGeolocation.onActivityChange never fired).
674
+ BGEventBus.sharedInstance().emit(BGEventNames.activityChange, payload: activity)
675
+ // Drive pace from the detected activity. "still"/"unknown" => stationary;
676
+ // anything else (walking/running/in_vehicle/on_bicycle) => moving. Route
677
+ // through changePace so the pending-authorization path is honored.
678
+ let moving = activity.type != "still" && activity.type != "unknown"
679
+ BGLiveActivityManager.shared.update(
680
+ location: lastGoodLocation,
681
+ isMoving: moving,
682
+ activity: activity.type,
683
+ force: true
684
+ )
685
+ if moving != isMoving {
686
+ changePace(moving)
687
+ }
688
+ }
689
+
690
+ @objc public func onLocationError(_ error: Error) {
691
+ locationError = error
692
+ fireLocationErrorEvent(error)
693
+ }
694
+
695
+ @objc public func onResume() {
696
+ if isEnabled {
697
+ startMonitoringSignificantLocationChanges()
698
+ if !BGConfig.sharedInstance().geolocation.useSignificantChangesOnly {
699
+ startUpdatingLocation()
700
+ }
701
+ }
702
+ }
703
+
704
+ @objc public func onSuspend() {
705
+ guard isEnabled else { return }
706
+ let geo = BGConfig.sharedInstance().geolocation
707
+ startMonitoringSignificantLocationChanges()
708
+ // Continuous keep-alive mode: keep GPS running while backgrounded so the
709
+ // app is never suspended (this is what shows the status-bar indicator and
710
+ // is how ride apps stay alive). Otherwise downgrade to SLC + stationary
711
+ // region to save battery.
712
+ if geo.disableStopDetection || (isMoving && !geo.useSignificantChangesOnly) {
713
+ startUpdatingLocation()
714
+ } else {
715
+ startMonitoringSignificantLocationChanges()
716
+ if let loc = stationaryLocation ?? lastGoodLocation {
717
+ startMonitoringStationaryRegion(loc, radius: geo.stationaryRadius)
718
+ }
719
+ stopUpdatingLocation()
720
+ }
721
+ }
722
+
723
+ @objc public func onAppTerminate() {
724
+ let config = BGConfig.sharedInstance()
725
+ guard config.app.stopOnTerminate else {
726
+ config.enabled = true
727
+ config.isMoving = isMoving
728
+ config.forcePersistNow()
729
+ // UIApplication.willTerminate handlers MUST work synchronously — iOS
730
+ // kills the process shortly after this returns, so the async
731
+ // _mutateCL (clMutationQueue -> main) hop used elsewhere would be
732
+ // dropped. Re-arm the wake mechanisms directly on the manager here.
733
+ // (We are already on the main thread for this notification.)
734
+ if let mgr = locationManager {
735
+ configureLocationManager(mgr)
736
+ mgr.startMonitoringSignificantLocationChanges()
737
+ isMonitoringSignificantLocationChanges = true
738
+ if let loc = stationaryLocation ?? lastGoodLocation {
739
+ let radius = max(config.geolocation.stationaryRadius, 200.0)
740
+ let region = CLCircularRegion(center: loc.coordinate, radius: radius, identifier: "BGStationary")
741
+ region.notifyOnExit = true
742
+ stationaryRegion = region
743
+ mgr.startMonitoring(for: region)
744
+ }
745
+ }
746
+ return
747
+ }
748
+ stop()
749
+ }
750
+
751
+ @objc public func updateCurrentState() {
752
+ }
753
+
754
+ @objc public func onUpdateState(_ state: [String: Any]) {
755
+ }
756
+
757
+ @objc public func shouldStopAfterElapsedMinutes() -> Bool {
758
+ return false
759
+ }
760
+
761
+ @objc public func stopAfterElapsedMinutes() -> TimeInterval {
762
+ return 0
763
+ }
764
+
765
+ @objc public func isInBackground() -> Bool {
766
+ return BGAppState.sharedInstance().isInBackground
767
+ }
768
+
769
+ // MARK: - CLLocationManagerDelegate
770
+
771
+ @objc public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
772
+ guard let location = locations.last else { return }
773
+
774
+ NSLog("[BGGEO] didUpdateLocations: lat=\(location.coordinate.latitude) lng=\(location.coordinate.longitude) acc=\(location.horizontalAccuracy) background=\(isInBackground()) moving=\(isMoving)")
775
+
776
+ let config = BGConfig.sharedInstance()
777
+
778
+ locationAccuracyQueue.append(location.horizontalAccuracy)
779
+ if locationAccuracyQueue.count > 10 { locationAccuracyQueue.removeFirst() }
780
+ medianLocationAccuracy = calculateMedianLocationAccuracy(locationAccuracyQueue)
781
+
782
+ lastLocation = location
783
+
784
+ if locationIsGoodEnough(location) {
785
+ lastGoodLocation = location
786
+ if !didReceiveFirstLocation {
787
+ didReceiveFirstLocation = true
788
+ if !isMoving {
789
+ persistStationaryLocation(location)
790
+ startMonitoringStationaryRegion(location, radius: config.geolocation.stationaryRadius)
791
+ }
792
+ }
793
+ bestLocation = location
794
+ }
795
+
796
+ let newDistanceFilter = calculateDistanceFilter(location)
797
+ if newDistanceFilter != distanceFilter {
798
+ applyDistanceFilter(newDistanceFilter)
799
+ }
800
+
801
+ BGGeofenceManager.sharedInstance().setLocation(location, isMoving: isMoving)
802
+ // Give the motion classifier speed/location context so its
803
+ // walking/running/vehicle classification can use speed alongside the
804
+ // accelerometer / CMMotionActivity signal.
805
+ BGMotionDetector.sharedInstance().setLocation(location, isMoving: isMoving)
806
+
807
+ // Speed-based motion onset. CMMotionActivity is unreliable for vehicles
808
+ // and bikes — when the phone is held steady the classifier reports
809
+ // `unknown`/`stationary` even at speed, which kept isMoving=false on real
810
+ // rides. GPS speed is decisive here, so flip to moving when we see real
811
+ // ground speed regardless of the activity classifier. (Going back to
812
+ // stationary is still handled by stop-detection / activity, except in
813
+ // keep-alive mode where we intentionally never stop.)
814
+ let speedMovingThreshold: CLLocationDistance = 1.5 // m/s ≈ 5.4 km/h
815
+ if location.speed >= 0, location.speed > speedMovingThreshold, !isMoving {
816
+ NSLog("[BGGEO] speed-based motion onset: speed=\(location.speed) m/s -> moving")
817
+ detectStartMotion(location)
818
+ }
819
+
820
+ let tsLocation = BGLocation(location: location, type: isMoving ? "tracking" : "stationary", extras: nil)
821
+ tsLocation.isMoving = isMoving
822
+ tsLocation.odometer = BGOdometer.sharedInstance().getOdometer()
823
+
824
+ if config.shouldPersist(tsLocation) {
825
+ persistLocation(tsLocation)
826
+ }
827
+
828
+ BGLiveActivityManager.shared.update(
829
+ location: location,
830
+ isMoving: isMoving,
831
+ activity: currentMotionActivity?.type ?? (isMoving ? "moving" : "still"),
832
+ force: false
833
+ )
834
+
835
+ // Deliver to RN module listeners (BgGeolocation.mm registered via onLocation:)
836
+ BGEventManager.sharedInstance().triggerLocationSuccess(tsLocation)
837
+ // Keep BGEventBus emission for any internal engine consumers
838
+ BGEventBus.sharedInstance().emit(BGEventNames.locationComplete, payload: tsLocation.toDictionary())
839
+ }
840
+
841
+ @objc public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
842
+ BGEventManager.sharedInstance().triggerLocationError(error)
843
+ onLocationError(error)
844
+ }
845
+
846
+ @objc public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
847
+ BGLocationAuthorization.sharedInstance().onAuthorizationStatusChanged(status)
848
+ }
849
+
850
+ @objc public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
851
+ guard region.identifier == "BGStationary" else { return }
852
+ if !isMoving {
853
+ startUpdatingLocation()
854
+ let fallback: CLLocation
855
+ if let circular = region as? CLCircularRegion {
856
+ fallback = CLLocation(latitude: circular.center.latitude, longitude: circular.center.longitude)
857
+ } else {
858
+ fallback = CLLocation()
859
+ }
860
+ detectStartMotion(lastLocation ?? lastGoodLocation ?? fallback)
861
+ }
862
+ }
863
+
864
+ @objc public func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
865
+ }
866
+
867
+ @objc public func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {
868
+ isUpdatingLocation = false
869
+ }
870
+
871
+ @objc public func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) {
872
+ isUpdatingLocation = true
873
+ }
874
+
875
+ // MARK: - KVO
876
+
877
+ @objc public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
878
+ }
879
+ }