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.
- package/BgGeolocation.podspec +39 -0
- package/LICENSE +20 -0
- package/README.md +366 -0
- package/android/build.gradle +69 -0
- package/android/src/main/AndroidManifest.xml +53 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationActivityRecognitionReceiver.kt +116 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationBootReceiver.kt +44 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationForegroundService.kt +373 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationGeofenceReceiver.kt +55 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationHeadlessTask.kt +138 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationModule.kt +1030 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationMotionStateMachine.kt +159 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationPackage.kt +31 -0
- package/android/src/main/res/drawable/bg_geo_notification.xml +9 -0
- package/ios/BgGeolocation.h +14 -0
- package/ios/BgGeolocation.mm +709 -0
- package/ios/engine/AtomicBoolean.swift +48 -0
- package/ios/engine/BGActivityChangeEvent.swift +20 -0
- package/ios/engine/BGActivityConfig.swift +71 -0
- package/ios/engine/BGAppConfig.swift +92 -0
- package/ios/engine/BGAppState.swift +147 -0
- package/ios/engine/BGAuthorization.swift +85 -0
- package/ios/engine/BGAuthorizationAlertPresenter.swift +39 -0
- package/ios/engine/BGAuthorizationConfig.swift +50 -0
- package/ios/engine/BGAuthorizationEvent.swift +40 -0
- package/ios/engine/BGBackgroundTaskManager.swift +143 -0
- package/ios/engine/BGCLRouter.swift +101 -0
- package/ios/engine/BGCallback.swift +19 -0
- package/ios/engine/BGConfig.swift +440 -0
- package/ios/engine/BGConfigModuleBase.swift +180 -0
- package/ios/engine/BGConfigOLD.swift +582 -0
- package/ios/engine/BGConnectivityChangeEvent.swift +15 -0
- package/ios/engine/BGCrashDetector.swift +122 -0
- package/ios/engine/BGCurrentPositionRequest.swift +87 -0
- package/ios/engine/BGDataStore.swift +75 -0
- package/ios/engine/BGDatabase.swift +677 -0
- package/ios/engine/BGDatabasePool.swift +220 -0
- package/ios/engine/BGDatabaseQueue.swift +215 -0
- package/ios/engine/BGDateUtils.swift +26 -0
- package/ios/engine/BGDeviceInfo.swift +54 -0
- package/ios/engine/BGDeviceManager.swift +65 -0
- package/ios/engine/BGEnabledChangeEvent.swift +11 -0
- package/ios/engine/BGEnv.swift +17 -0
- package/ios/engine/BGEventBus.swift +83 -0
- package/ios/engine/BGEventManager.swift +169 -0
- package/ios/engine/BGEventNames.swift +51 -0
- package/ios/engine/BGGeofence.swift +233 -0
- package/ios/engine/BGGeofenceDAO.swift +152 -0
- package/ios/engine/BGGeofenceEvent.swift +42 -0
- package/ios/engine/BGGeofenceLocationRequest.swift +94 -0
- package/ios/engine/BGGeofenceManager.swift +315 -0
- package/ios/engine/BGGeofenceTransition.swift +97 -0
- package/ios/engine/BGGeofencesChangeEvent.swift +26 -0
- package/ios/engine/BGGeolocationConfig.swift +136 -0
- package/ios/engine/BGHeartbeatEvent.swift +31 -0
- package/ios/engine/BGHeartbeatService.swift +51 -0
- package/ios/engine/BGHttpConfig.swift +105 -0
- package/ios/engine/BGHttpErrorCodes.swift +63 -0
- package/ios/engine/BGHttpEvent.swift +34 -0
- package/ios/engine/BGHttpRequest.swift +126 -0
- package/ios/engine/BGHttpResponse.swift +93 -0
- package/ios/engine/BGHttpService.swift +428 -0
- package/ios/engine/BGKalmanFilter.swift +105 -0
- package/ios/engine/BGLMActionNames.swift +55 -0
- package/ios/engine/BGLicenseManager.swift +26 -0
- package/ios/engine/BGLiveActivityManager.swift +327 -0
- package/ios/engine/BGLocation.swift +311 -0
- package/ios/engine/BGLocationAuthorization.swift +427 -0
- package/ios/engine/BGLocationDAO.swift +252 -0
- package/ios/engine/BGLocationErrors.swift +28 -0
- package/ios/engine/BGLocationEvent.swift +43 -0
- package/ios/engine/BGLocationFilter.swift +82 -0
- package/ios/engine/BGLocationFilterConfig.swift +57 -0
- package/ios/engine/BGLocationHelper.swift +54 -0
- package/ios/engine/BGLocationManager.swift +662 -0
- package/ios/engine/BGLocationMetricsEngine.swift +116 -0
- package/ios/engine/BGLocationRequestService.swift +459 -0
- package/ios/engine/BGLocationSatisfier.swift +14 -0
- package/ios/engine/BGLocationStreamEvent.swift +27 -0
- package/ios/engine/BGLog.swift +337 -0
- package/ios/engine/BGLogLevel.swift +26 -0
- package/ios/engine/BGLoggerConfig.swift +60 -0
- package/ios/engine/BGMotionActivity.swift +31 -0
- package/ios/engine/BGMotionActivityClassifier.swift +108 -0
- package/ios/engine/BGMotionActivityManagerAdapter.swift +40 -0
- package/ios/engine/BGMotionActivitySource.swift +46 -0
- package/ios/engine/BGMotionDetector.swift +377 -0
- package/ios/engine/BGMotionPermissionManager.swift +50 -0
- package/ios/engine/BGNativeLogger.swift +48 -0
- package/ios/engine/BGNotificaitons.swift +37 -0
- package/ios/engine/BGOdometer.swift +66 -0
- package/ios/engine/BGPersistenceConfig.swift +29 -0
- package/ios/engine/BGPolygonStreamRequest.swift +48 -0
- package/ios/engine/BGPowerSaveChangeEvent.swift +12 -0
- package/ios/engine/BGPropertySpec.swift +29 -0
- package/ios/engine/BGProviderChangeEvent.swift +31 -0
- package/ios/engine/BGQueue.swift +50 -0
- package/ios/engine/BGRPC.swift +194 -0
- package/ios/engine/BGReachability.swift +58 -0
- package/ios/engine/BGResultSet.swift +157 -0
- package/ios/engine/BGSchedule.swift +228 -0
- package/ios/engine/BGScheduleEvent.swift +13 -0
- package/ios/engine/BGScheduler.swift +116 -0
- package/ios/engine/BGSingleLocationRequest.swift +49 -0
- package/ios/engine/BGStreamLocationRequest.swift +42 -0
- package/ios/engine/BGTemplate.swift +54 -0
- package/ios/engine/BGTimerService.swift +46 -0
- package/ios/engine/BGTrackingAudioManager.swift +286 -0
- package/ios/engine/BGTrackingService.swift +879 -0
- package/ios/engine/BGWatchPositionRequest.swift +63 -0
- package/ios/engine/DatabaseQueue.swift +47 -0
- package/ios/engine/LogQuery.swift +10 -0
- package/ios/engine/SQLQuery.swift +65 -0
- package/ios/engine/TransistorAuthorizationToken.swift +182 -0
- package/ios/liveactivity/BGLiveTrackingAttributes.swift +52 -0
- package/ios/locationpush/BGLocationPushDeliverer.swift +260 -0
- package/ios/locationpush/BGLocationPushService.swift +161 -0
- package/ios/locationpush/BGLocationPushShared.swift +98 -0
- package/ios/locationpush/BGLocationPushSocketClient.swift +198 -0
- package/lib/module/NativeBgGeolocation.js +5 -0
- package/lib/module/NativeBgGeolocation.js.map +1 -0
- package/lib/module/events.js +20 -0
- package/lib/module/events.js.map +1 -0
- package/lib/module/index.js +706 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeBgGeolocation.d.ts +57 -0
- package/lib/typescript/src/NativeBgGeolocation.d.ts.map +1 -0
- package/lib/typescript/src/events.d.ts +18 -0
- package/lib/typescript/src/events.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +238 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +229 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +141 -0
- package/src/NativeBgGeolocation.ts +236 -0
- package/src/events.ts +17 -0
- package/src/index.tsx +935 -0
- 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
|
+
}
|