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,427 @@
1
+ import Foundation
2
+ import CoreLocation
3
+ import UIKit
4
+
5
+ @objc public protocol BGLocationAuthorizationDelegate: NSObjectProtocol {
6
+ @objc optional func locationAuthorization(_ authorization: BGLocationAuthorization, didChangeAuthorizationStatus status: CLAuthorizationStatus)
7
+ @objc optional func locationAuthorization(_ authorization: BGLocationAuthorization, didCompleteWith status: CLAuthorizationStatus, error: Error?)
8
+ }
9
+
10
+ @objc public class BGLocationAuthorization: NSObject, CLLocationManagerDelegate {
11
+
12
+ private static var _sharedInstance: BGLocationAuthorization?
13
+ private static let instanceLock = NSLock()
14
+
15
+ @objc public class func sharedInstance() -> BGLocationAuthorization {
16
+ instanceLock.lock()
17
+ defer { instanceLock.unlock() }
18
+ if _sharedInstance == nil { _sharedInstance = BGLocationAuthorization() }
19
+ return _sharedInstance!
20
+ }
21
+
22
+ @objc public class func configureShared(withLocationManager manager: CLLocationManager) {
23
+ sharedInstance().locationManager = manager
24
+ // The CLLocationManager delegate is owned by BGCLRouter, which forwards
25
+ // authorization callbacks here. Do NOT assign manager.delegate.
26
+ }
27
+
28
+ // MARK: - State
29
+
30
+ @objc public var locationManager: CLLocationManager?
31
+ @objc public weak var delegate: BGLocationAuthorizationDelegate?
32
+ @objc public var enabled: Bool = true
33
+ @objc public var automaticPromptEnabled: Bool = true
34
+ @objc public var authorizationStatus: CLAuthorizationStatus = .notDetermined
35
+ @objc public var accuracyAuthorization: Int = 0
36
+ @objc public var desiredPolicy: String = "Always"
37
+ @objc public var pendingPolicy: String = ""
38
+ @objc public var hasPendingPolicy: Bool = false
39
+ @objc public var state: String = "idle"
40
+ @objc public var authorizationState: String = "not_determined"
41
+ @objc public var authorizationTimeoutInterval: TimeInterval = 20.0
42
+ @objc public var currentAlert: UIAlertController?
43
+ public var pendingCompletion: ((CLAuthorizationStatus, Error?) -> Void)?
44
+ public var pendingCompletions: [((CLAuthorizationStatus, Error?) -> Void)] = []
45
+ @objc public var timeoutTimer: Timer?
46
+
47
+ @objc public override init() {
48
+ super.init()
49
+ authorizationStatus = CLLocationManager.authorizationStatus()
50
+ authorizationState = stateForAuthorizationStatus(authorizationStatus)
51
+ registerForApplicationNotifications()
52
+ }
53
+
54
+ @objc public init(locationManager: CLLocationManager) {
55
+ self.locationManager = locationManager
56
+ super.init()
57
+ authorizationStatus = CLLocationManager.authorizationStatus()
58
+ authorizationState = stateForAuthorizationStatus(authorizationStatus)
59
+ // Do NOT assign locationManager.delegate here — the single delegate is
60
+ // owned by BGCLRouter. Assigning it would silently steal callbacks and
61
+ // re-introduce the multi-delegate bug. (This initializer is unused; the
62
+ // delegate line is removed to keep the invariant un-violatable.)
63
+ registerForApplicationNotifications()
64
+ }
65
+
66
+ @objc public func registerForApplicationNotifications() {
67
+ NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
68
+ }
69
+
70
+ // MARK: - Authorization checks
71
+
72
+ @objc public func isAuthorized() -> Bool {
73
+ return authorizationStatus == .authorizedAlways || authorizationStatus == .authorizedWhenInUse
74
+ }
75
+
76
+ @objc public func isAuthorizedAlways() -> Bool {
77
+ return authorizationStatus == .authorizedAlways
78
+ }
79
+
80
+ @objc public func isAuthorizedForPolicy(_ policy: String) -> Bool {
81
+ if policy == "Always" {
82
+ return authorizationStatus == .authorizedAlways
83
+ }
84
+ return isAuthorized()
85
+ }
86
+
87
+ @objc public func isAuthorizedForDesiredLocationRequest() -> Bool {
88
+ return isAuthorizedForPolicy(desiredPolicy)
89
+ }
90
+
91
+ @objc public func isRequestInProgress() -> Bool {
92
+ return state != "idle"
93
+ }
94
+
95
+ @objc public func canRequestAlwaysUpgrade() -> Bool {
96
+ return authorizationStatus == .authorizedWhenInUse
97
+ }
98
+
99
+ @objc public func shouldAttemptAlwaysUpgrade() -> Bool {
100
+ return canRequestAlwaysUpgrade() && desiredPolicy == "Always"
101
+ }
102
+
103
+ @objc public func effectivePolicy() -> String {
104
+ return desiredPolicy
105
+ }
106
+
107
+ @objc public func updateDesiredPolicyFromConfig() {
108
+ let config = BGConfig.sharedInstance()
109
+ desiredPolicy = config.geolocation.locationAuthorizationRequest
110
+ }
111
+
112
+ // MARK: - Authorization flow
113
+
114
+ @objc public func requestAuthorization(_ completion: ((CLAuthorizationStatus, Error?) -> Void)?) {
115
+ updateDesiredPolicyFromConfig()
116
+ requestAuthorizationForPolicy(desiredPolicy, completion: completion)
117
+ }
118
+
119
+ @objc public func requestAuthorizationForPolicy(_ policy: String, completion: ((CLAuthorizationStatus, Error?) -> Void)?) {
120
+ desiredPolicy = policy
121
+ authorizationStatus = locationManager?.authorizationStatus ?? CLLocationManager.authorizationStatus()
122
+
123
+ guard enabled else {
124
+ completion?(authorizationStatus, nil)
125
+ return
126
+ }
127
+
128
+ guard locationManager != nil else {
129
+ completion?(
130
+ authorizationStatus,
131
+ NSError(
132
+ domain: "BGLocationAuthorization",
133
+ code: -2,
134
+ userInfo: [NSLocalizedDescriptionKey: "CLLocationManager is not ready"]
135
+ )
136
+ )
137
+ return
138
+ }
139
+
140
+ if isAuthorizedForPolicy(policy) {
141
+ completion?(authorizationStatus, nil)
142
+ return
143
+ }
144
+
145
+ if let c = completion { pendingCompletions.append(c) }
146
+ pendingPolicy = policy
147
+
148
+ beginAuthorizationFlow()
149
+ }
150
+
151
+ @objc public func requestLocationAuthorization(_ policy: String) {
152
+ requestLocationAuthorization(policy, onCancel: nil)
153
+ }
154
+
155
+ @objc public func requestLocationAuthorization(_ policy: String, onCancel: (() -> Void)?) {
156
+ requestAuthorizationForPolicy(policy) { status, error in
157
+ if status == .denied {
158
+ onCancel?()
159
+ }
160
+ }
161
+ }
162
+
163
+ @objc public func beginAuthorizationFlow() {
164
+ guard state == "idle" else { return }
165
+ transitionToState("requesting")
166
+ startTimeoutTimer()
167
+
168
+ if authorizationStatus == .notDetermined {
169
+ requestInitialAuthorization()
170
+ } else if shouldAttemptAlwaysUpgrade() {
171
+ requestAlwaysUpgrade()
172
+ } else if authorizationStatus == .denied || authorizationStatus == .restricted {
173
+ completeAuthorizationRequest(authorizationStatus, error: nil)
174
+ } else {
175
+ completeAuthorizationRequest(authorizationStatus, error: nil)
176
+ }
177
+ }
178
+
179
+ @objc public func requestInitialAuthorization() {
180
+ guard let mgr = locationManager else {
181
+ completeAuthorizationRequest(
182
+ authorizationStatus,
183
+ error: NSError(
184
+ domain: "BGLocationAuthorization",
185
+ code: -2,
186
+ userInfo: [NSLocalizedDescriptionKey: "CLLocationManager is not ready"]
187
+ )
188
+ )
189
+ return
190
+ }
191
+ DispatchQueue.main.async {
192
+ // iOS uses a two-stage flow for background location. The first
193
+ // system sheet grants foreground access; a later request upgrades
194
+ // that authorization to Always.
195
+ mgr.requestWhenInUseAuthorization()
196
+ }
197
+ }
198
+
199
+ @objc public func requestAlwaysUpgrade() {
200
+ guard let mgr = locationManager else {
201
+ completeAuthorizationRequest(
202
+ authorizationStatus,
203
+ error: NSError(
204
+ domain: "BGLocationAuthorization",
205
+ code: -2,
206
+ userInfo: [NSLocalizedDescriptionKey: "CLLocationManager is not ready"]
207
+ )
208
+ )
209
+ return
210
+ }
211
+ DispatchQueue.main.async {
212
+ mgr.requestAlwaysAuthorization()
213
+ }
214
+ }
215
+
216
+ @objc public func requestTemporaryFullAccuracy(_ purposeKey: String, completion: ((Error?) -> Void)?) {
217
+ if #available(iOS 14.0, *) {
218
+ locationManager?.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purposeKey) { error in
219
+ completion?(error)
220
+ }
221
+ } else {
222
+ completion?(nil)
223
+ }
224
+ }
225
+
226
+ @objc public func requestTemporaryFullAccuracy(_ purposeKey: String, success: (() -> Void)?, failure: ((Error) -> Void)?) {
227
+ requestTemporaryFullAccuracy(purposeKey) { error in
228
+ if let error = error { failure?(error) } else { success?() }
229
+ }
230
+ }
231
+
232
+ // MARK: - Alert presentation
233
+
234
+ @objc public func showSettingsAlert() {
235
+ dismissCurrentAlert()
236
+ let strings = BGConfig.sharedInstance().getLocationAuthorizationAlertStrings()
237
+ let alert = UIAlertController(
238
+ title: strings["title"] as? String ?? "Background Location Access",
239
+ message: strings["message"] as? String ?? "This app requires location access to operate correctly.",
240
+ preferredStyle: .alert
241
+ )
242
+ alert.addAction(UIAlertAction(title: strings["cancel"] as? String ?? "Cancel", style: .cancel) { [weak self] _ in
243
+ self?.handleAlertCancelled()
244
+ })
245
+ alert.addAction(UIAlertAction(title: strings["settings"] as? String ?? "Settings", style: .default) { [weak self] _ in
246
+ self?.openSettings()
247
+ })
248
+ currentAlert = alert
249
+ presentSettingsAlert()
250
+ }
251
+
252
+ @objc public func presentSettingsAlert() {
253
+ guard let alert = currentAlert else { return }
254
+ DispatchQueue.main.async {
255
+ guard let topVC = BGAppState.sharedInstance().topPresenter() else { return }
256
+ topVC.present(alert, animated: true)
257
+ }
258
+ }
259
+
260
+ @objc public func dismissCurrentAlert() {
261
+ DispatchQueue.main.async {
262
+ self.currentAlert?.dismiss(animated: false)
263
+ self.currentAlert = nil
264
+ }
265
+ }
266
+
267
+ @objc public func openSettings() {
268
+ guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
269
+ UIApplication.shared.open(url)
270
+ }
271
+
272
+ @objc public func hide() {
273
+ dismissCurrentAlert()
274
+ }
275
+
276
+ // MARK: - Completion
277
+
278
+ @objc public func completeAuthorizationRequest(_ status: CLAuthorizationStatus, error: Error?) {
279
+ cancelTimeoutTimer()
280
+ transitionToState("idle")
281
+ let completions = pendingCompletions
282
+ pendingCompletions.removeAll()
283
+ pendingCompletion = nil
284
+ for c in completions { c(status, error) }
285
+ delegate?.locationAuthorization?(self, didCompleteWith: status, error: error)
286
+ }
287
+
288
+ @objc public func cancelAuthorizationRequest() {
289
+ completeAuthorizationRequest(authorizationStatus, error: NSError(domain: "BGLocationAuthorization", code: -1, userInfo: [NSLocalizedDescriptionKey: "Cancelled"]))
290
+ }
291
+
292
+ // MARK: - State machine
293
+
294
+ @objc public func transitionToState(_ newState: String) {
295
+ state = newState
296
+ }
297
+
298
+ @objc public func stateForAuthorizationStatus(_ status: CLAuthorizationStatus) -> String {
299
+ switch status {
300
+ case .authorizedAlways: return "authorized_always"
301
+ case .authorizedWhenInUse: return "authorized_when_in_use"
302
+ case .denied: return "denied"
303
+ case .restricted: return "restricted"
304
+ case .notDetermined: return "not_determined"
305
+ @unknown default: return "unknown"
306
+ }
307
+ }
308
+
309
+ @objc public func stringForPolicy(_ policy: String) -> String {
310
+ return policy
311
+ }
312
+
313
+ @objc public func stringForState(_ state: String) -> String {
314
+ return state
315
+ }
316
+
317
+ // MARK: - Timeout
318
+
319
+ @objc public func startTimeoutTimer() {
320
+ cancelTimeoutTimer()
321
+ DispatchQueue.main.async {
322
+ self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: self.authorizationTimeoutInterval, repeats: false) { [weak self] _ in
323
+ self?.handleTimeout()
324
+ }
325
+ }
326
+ }
327
+
328
+ @objc public func cancelTimeoutTimer() {
329
+ timeoutTimer?.invalidate()
330
+ timeoutTimer = nil
331
+ }
332
+
333
+ @objc public func handleTimeout() {
334
+ // Complete with the CURRENT status and NO error. A slow user (or the
335
+ // deferred iOS Always prompt) must not produce a spurious rejection in
336
+ // JS — requestPermission inspects the returned status, not an error.
337
+ completeAuthorizationRequest(authorizationStatus, error: nil)
338
+ }
339
+
340
+ // MARK: - Handlers
341
+
342
+ @objc public func handleAlertCancelled() {
343
+ completeAuthorizationRequest(authorizationStatus, error: nil)
344
+ }
345
+
346
+ @objc public func handleAlwaysGranted() {
347
+ completeAuthorizationRequest(.authorizedAlways, error: nil)
348
+ }
349
+
350
+ @objc public func handleWhenInUseGranted() {
351
+ completeAuthorizationRequest(.authorizedWhenInUse, error: nil)
352
+ }
353
+
354
+ @objc public func handleAuthorizationDenied() {
355
+ dismissCurrentAlert()
356
+ completeAuthorizationRequest(.denied, error: nil)
357
+ }
358
+
359
+ @objc public func onAuthorizationStatusChanged(_ status: CLAuthorizationStatus) {
360
+ authorizationStatus = status
361
+ updateAuthorizationState()
362
+ checkAuthorizationStatusForPolicyChange()
363
+ delegate?.locationAuthorization?(self, didChangeAuthorizationStatus: status)
364
+ }
365
+
366
+ @objc public func updateAuthorizationState() {
367
+ // Keep permission status separate from the request lifecycle in `state`.
368
+ // Mixing the two caused the initial `.notDetermined` callback to replace
369
+ // `idle`, so beginAuthorizationFlow() silently returned without showing
370
+ // the system permission sheet.
371
+ authorizationState = stateForAuthorizationStatus(authorizationStatus)
372
+ }
373
+
374
+ @objc public func checkAuthorizationStatusForPolicyChange() {
375
+ guard isRequestInProgress() || state == "requesting" else { return }
376
+ switch authorizationStatus {
377
+ case .authorizedAlways:
378
+ handleAlwaysGranted()
379
+ case .authorizedWhenInUse:
380
+ // iOS grants When-In-Use first even when Always was requested; the
381
+ // actual "Always" upgrade prompt is deferred to a later background
382
+ // transition, so NO further callback arrives this session. Resolve
383
+ // the request now with the real (provisional) status instead of
384
+ // hanging until the 20s timeout — which used to surface to JS as a
385
+ // rejection even though the user just granted access. The caller can
386
+ // re-request to trigger the Always upgrade.
387
+ handleWhenInUseGranted()
388
+ case .denied, .restricted:
389
+ handleAuthorizationDenied()
390
+ default:
391
+ break
392
+ }
393
+ }
394
+
395
+ @objc public func onDidBecomeActive() {
396
+ if state == "requesting" && authorizationStatus != .notDetermined {
397
+ checkAuthorizationStatusForPolicyChange()
398
+ }
399
+ }
400
+
401
+ @objc public func cleanup() {
402
+ cancelTimeoutTimer()
403
+ dismissCurrentAlert()
404
+ }
405
+
406
+ // MARK: - Notifications
407
+
408
+ @objc func applicationDidBecomeActive() {
409
+ onDidBecomeActive()
410
+ }
411
+
412
+ // MARK: - CLLocationManagerDelegate
413
+
414
+ @objc public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
415
+ onAuthorizationStatusChanged(status)
416
+ }
417
+
418
+ @available(iOS 14.0, *)
419
+ @objc public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
420
+ let status = manager.authorizationStatus
421
+ onAuthorizationStatusChanged(status)
422
+ if #available(iOS 14.0, *) {
423
+ let acc = manager.accuracyAuthorization
424
+ accuracyAuthorization = acc == .fullAccuracy ? 1 : 0
425
+ }
426
+ }
427
+ }
@@ -0,0 +1,252 @@
1
+ import Foundation
2
+ import CoreLocation
3
+
4
+ @objc public class BGLocationDAO: NSObject {
5
+
6
+ private static var _sharedInstance: BGLocationDAO?
7
+ private static let lock = NSLock()
8
+
9
+ private var dbQueue: BGDatabaseQueue?
10
+ private let accessQueue = DispatchQueue(label: "BGLocationDAO.access")
11
+
12
+ @objc public class func sharedInstance() -> BGLocationDAO {
13
+ lock.lock()
14
+ defer { lock.unlock() }
15
+ if _sharedInstance == nil {
16
+ _sharedInstance = BGLocationDAO()
17
+ }
18
+ return _sharedInstance!
19
+ }
20
+
21
+ @objc public override init() {
22
+ super.init()
23
+ setupDatabase()
24
+ registerConfigChangeHandlers()
25
+ }
26
+
27
+ private func setupDatabase() {
28
+ let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
29
+ guard let docPath = paths.first else { return }
30
+ let dbPath = docPath.appendingPathComponent("BGLocationManager.db")
31
+ dbQueue = BGDatabaseQueue(path: dbPath.path)
32
+ dbQueue?.inDatabase { db in
33
+ _ = db.executeUpdate("""
34
+ CREATE TABLE IF NOT EXISTS locations (
35
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ uuid TEXT UNIQUE NOT NULL,
37
+ timestamp REAL NOT NULL,
38
+ latitude REAL NOT NULL,
39
+ longitude REAL NOT NULL,
40
+ accuracy REAL,
41
+ altitude REAL,
42
+ speed REAL,
43
+ heading REAL,
44
+ odometer REAL,
45
+ is_moving INTEGER,
46
+ event TEXT,
47
+ activity_type TEXT,
48
+ activity_confidence INTEGER,
49
+ battery_level REAL,
50
+ battery_is_charging INTEGER,
51
+ extras TEXT,
52
+ json TEXT NOT NULL,
53
+ locked INTEGER DEFAULT 0
54
+ )
55
+ """)
56
+ _ = db.executeUpdate("CREATE INDEX IF NOT EXISTS locations_timestamp ON locations (timestamp)")
57
+ _ = db.executeUpdate("CREATE INDEX IF NOT EXISTS locations_uuid ON locations (uuid)")
58
+ }
59
+ }
60
+
61
+ @objc public func registerConfigChangeHandlers() {
62
+ }
63
+
64
+ @objc public func create(_ location: BGLocation, error: UnsafeMutablePointer<NSError?>?) -> Bool {
65
+ guard let json = location.toJson(nil) else { return false }
66
+ let dict = location.toDictionary()
67
+ let coords = dict["coords"] as? [String: Any] ?? [:]
68
+ var result = false
69
+
70
+ dbQueue?.inDatabase { db in
71
+ let extras = location.extras.flatMap { try? JSONSerialization.data(withJSONObject: $0) }.flatMap { String(data: $0, encoding: .utf8) }
72
+ result = db.executeUpdate(
73
+ """
74
+ INSERT OR IGNORE INTO locations
75
+ (uuid, timestamp, latitude, longitude, accuracy, altitude, speed, heading,
76
+ odometer, is_moving, event, activity_type, battery_level, battery_is_charging, extras, json)
77
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
78
+ """,
79
+ withArgumentsInArray: [
80
+ location.uuid,
81
+ location.timestamp().timeIntervalSince1970,
82
+ coords["latitude"] ?? 0,
83
+ coords["longitude"] ?? 0,
84
+ coords["accuracy"] ?? 0,
85
+ coords["altitude"] ?? 0,
86
+ coords["speed"] ?? 0,
87
+ coords["heading"] ?? 0,
88
+ location.odometer,
89
+ location.isMoving ? 1 : 0,
90
+ location.event,
91
+ (dict["activity"] as? [String: Any])?["type"] ?? "",
92
+ (dict["battery"] as? [String: Any])?["level"] ?? 0,
93
+ (dict["battery"] as? [String: Any])?["is_charging"] ?? false,
94
+ extras as Any,
95
+ json
96
+ ]
97
+ )
98
+ }
99
+ return result
100
+ }
101
+
102
+ @objc public func all() -> [[String: Any]] {
103
+ return allWithLocking(false)
104
+ }
105
+
106
+ @objc public func allWithLocking(_ locking: Bool) -> [[String: Any]] {
107
+ var results: [[String: Any]] = []
108
+ dbQueue?.inDatabase { db in
109
+ let sql = locking ? "SELECT * FROM locations ORDER BY timestamp ASC" : "SELECT * FROM locations WHERE locked = 0 ORDER BY timestamp ASC"
110
+ guard let rs = db.executeQuery(sql) else { return }
111
+ defer { rs.close() }
112
+ while rs.next() {
113
+ if let json = rs.string(forColumn: "json"),
114
+ let data = json.data(using: .utf8),
115
+ let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
116
+ results.append(dict)
117
+ }
118
+ }
119
+ }
120
+ return results
121
+ }
122
+
123
+ @objc public func first() -> [String: Any]? {
124
+ var result: [String: Any]?
125
+ dbQueue?.inDatabase { db in
126
+ guard let rs = db.executeQuery("SELECT * FROM locations WHERE locked = 0 ORDER BY timestamp ASC LIMIT 1") else { return }
127
+ defer { rs.close() }
128
+ if rs.next(), let json = rs.string(forColumn: "json"),
129
+ let data = json.data(using: .utf8),
130
+ let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
131
+ result = dict
132
+ }
133
+ }
134
+ return result
135
+ }
136
+
137
+ @objc public func getCount() -> Int {
138
+ var count = 0
139
+ dbQueue?.inDatabase { db in
140
+ guard let rs = db.executeQuery("SELECT COUNT(*) FROM locations") else { return }
141
+ defer { rs.close() }
142
+ if rs.next() { count = Int(rs.int(forColumn: 0)) }
143
+ }
144
+ return count
145
+ }
146
+
147
+ @objc public func destroy(_ uuid: String) -> Bool {
148
+ var result = false
149
+ dbQueue?.inDatabase { db in
150
+ result = db.executeUpdate("DELETE FROM locations WHERE uuid = ?", withArgumentsInArray: [uuid])
151
+ }
152
+ return result
153
+ }
154
+
155
+ @objc public func destroyByUuid(_ uuid: String) -> Bool {
156
+ return destroy(uuid)
157
+ }
158
+
159
+ @objc public func destroyAll(_ uuids: [String]) -> Bool {
160
+ var result = false
161
+ dbQueue?.inDatabase { db in
162
+ let placeholders = uuids.map { _ in "?" }.joined(separator: ", ")
163
+ result = db.executeUpdate("DELETE FROM locations WHERE uuid IN (\(placeholders))", withArgumentsInArray: uuids)
164
+ }
165
+ return result
166
+ }
167
+
168
+ @objc public func clear() {
169
+ dbQueue?.inDatabase { db in
170
+ _ = db.executeUpdate("DELETE FROM locations")
171
+ }
172
+ }
173
+
174
+ @objc public func unlock(_ uuid: String) -> Bool {
175
+ var result = false
176
+ dbQueue?.inDatabase { db in
177
+ result = db.executeUpdate("UPDATE locations SET locked = 0 WHERE uuid = ?", withArgumentsInArray: [uuid])
178
+ }
179
+ return result
180
+ }
181
+
182
+ @objc public func unlockAll(_ uuids: [String]) -> Bool {
183
+ var result = false
184
+ dbQueue?.inDatabase { db in
185
+ let placeholders = uuids.map { _ in "?" }.joined(separator: ", ")
186
+ result = db.executeUpdate("UPDATE locations SET locked = 0 WHERE uuid IN (\(placeholders))", withArgumentsInArray: uuids)
187
+ }
188
+ return result
189
+ }
190
+
191
+ @objc public func unlock() -> Bool {
192
+ var result = false
193
+ dbQueue?.inDatabase { db in
194
+ result = db.executeUpdate("UPDATE locations SET locked = 0")
195
+ }
196
+ return result
197
+ }
198
+
199
+ @objc public func purge(_ query: SQLQuery) -> Bool {
200
+ var result = false
201
+ dbQueue?.inDatabase { db in
202
+ result = db.executeUpdate("DELETE FROM locations", withArgumentsInArray: [])
203
+ }
204
+ return result
205
+ }
206
+
207
+ @objc public func shrink(_ count: Int, db: BGDatabase) {
208
+ let current = getCount()
209
+ if current > count {
210
+ let excess = current - count
211
+ _ = db.executeUpdate("DELETE FROM locations WHERE id IN (SELECT id FROM locations ORDER BY timestamp ASC LIMIT ?)", withArgumentsInArray: [excess])
212
+ }
213
+ }
214
+
215
+ @objc public func inflate(_ dict: [String: Any]) -> BGLocation {
216
+ let location = BGLocation()
217
+ if let uuid = dict["uuid"] as? String { location.uuid = uuid }
218
+ if let event = dict["event"] as? String { location.event = event }
219
+ if let isMoving = dict["is_moving"] as? Bool { location.isMoving = isMoving }
220
+ if let odometer = dict["odometer"] as? Double { location.odometer = odometer }
221
+ if let extras = dict["extras"] as? [String: Any] { location.extras = extras }
222
+ if let coords = dict["coords"] as? [String: Any] {
223
+ let lat = coords["latitude"] as? Double ?? 0
224
+ let lng = coords["longitude"] as? Double ?? 0
225
+ let accuracy = coords["accuracy"] as? Double ?? 0
226
+ let altitude = coords["altitude"] as? Double ?? 0
227
+ let speed = coords["speed"] as? Double ?? -1
228
+ let heading = coords["heading"] as? Double ?? -1
229
+ var timestamp = Date()
230
+ if let ts = dict["timestamp"] as? String {
231
+ let fmt = ISO8601DateFormatter()
232
+ timestamp = fmt.date(from: ts) ?? Date()
233
+ }
234
+ let coord = CLLocationCoordinate2D(latitude: lat, longitude: lng)
235
+ let loc = CLLocation(coordinate: coord, altitude: altitude, horizontalAccuracy: accuracy, verticalAccuracy: -1, course: heading, speed: speed, timestamp: timestamp)
236
+ location.location = loc
237
+ }
238
+ return location
239
+ }
240
+
241
+ @objc public func decodeJSON(_ json: String) -> [String: Any]? {
242
+ guard let data = json.data(using: .utf8) else { return nil }
243
+ return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
244
+ }
245
+
246
+ @objc public func migrate(_ db: BGDatabase) {
247
+ }
248
+
249
+ @objc public func hydrate(_ query: SQLQuery) -> [[String: Any]] {
250
+ return all()
251
+ }
252
+ }