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,161 @@
|
|
|
1
|
+
//
|
|
2
|
+
// BGLocationPushService.swift
|
|
3
|
+
//
|
|
4
|
+
// The principal class of the iOS Location Push Service Extension
|
|
5
|
+
// (CLLocationPushServiceExtension, iOS 15+).
|
|
6
|
+
//
|
|
7
|
+
// ── WHAT THIS IS ──────────────────────────────────────────────────────────
|
|
8
|
+
// When your server sends an APNs push (apns-push-type: location, apns-topic:
|
|
9
|
+
// <bundle-id>.location-query) to the device's location-push token, iOS launches
|
|
10
|
+
// THIS extension in a separate, short-lived process — even if the host app has
|
|
11
|
+
// been force-quit. The extension grabs one location fix and ships it to your
|
|
12
|
+
// server, then signals completion and is torn down.
|
|
13
|
+
//
|
|
14
|
+
// ── DELIVERY ──────────────────────────────────────────────────────────────
|
|
15
|
+
// The push payload carries a `location-query` id. Its presence means "capture
|
|
16
|
+
// and report a location now". We:
|
|
17
|
+
// 1. requestLocation() — one GPS fix.
|
|
18
|
+
// 2. Try Socket.IO (`location:update`) first, if socket config is present.
|
|
19
|
+
// 3. Fall back to REST POST to the configured `url`.
|
|
20
|
+
// The captured location-query id is echoed back so the server can correlate.
|
|
21
|
+
//
|
|
22
|
+
// ── REQUIREMENTS ──────────────────────────────────────────────────────────
|
|
23
|
+
// • Apple-approved `com.apple.developer.location.push` entitlement.
|
|
24
|
+
// • App Group shared with the host app (see BGLocationPushShared).
|
|
25
|
+
// • Host app calls startMonitoringLocationPushes + ships the token to server.
|
|
26
|
+
// • "Always" location authorization.
|
|
27
|
+
//
|
|
28
|
+
// ~30s wall-clock budget. The socket attempt is tightly timed so REST always
|
|
29
|
+
// has room to run.
|
|
30
|
+
//
|
|
31
|
+
|
|
32
|
+
import CoreLocation
|
|
33
|
+
import Foundation
|
|
34
|
+
import os.log
|
|
35
|
+
|
|
36
|
+
@available(iOS 15.0, *)
|
|
37
|
+
@objc(BGLocationPushService)
|
|
38
|
+
public final class BGLocationPushService: NSObject, CLLocationPushServiceExtension {
|
|
39
|
+
|
|
40
|
+
private var completion: (() -> Void)?
|
|
41
|
+
private var manager: CLLocationManager?
|
|
42
|
+
private var fetcher: BGLocationPushFetcher?
|
|
43
|
+
|
|
44
|
+
public override init() {
|
|
45
|
+
super.init()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public func didReceiveLocationPushPayload(
|
|
49
|
+
_ payload: [String: Any],
|
|
50
|
+
completion: @escaping () -> Void
|
|
51
|
+
) {
|
|
52
|
+
BGLocationPushLog.log("didReceiveLocationPushPayload: \(payload)")
|
|
53
|
+
self.completion = completion
|
|
54
|
+
|
|
55
|
+
let queryId = Self.extractLocationQueryId(from: payload)
|
|
56
|
+
guard let queryId = queryId else {
|
|
57
|
+
// No location-query id → nothing to report. Finish cleanly.
|
|
58
|
+
BGLocationPushLog.log("no location-query id in payload — ignoring")
|
|
59
|
+
finish()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
BGLocationPushLog.log("location-query id = \(queryId)")
|
|
63
|
+
|
|
64
|
+
let fetcher = BGLocationPushFetcher(queryId: queryId) { [weak self] in
|
|
65
|
+
self?.finish()
|
|
66
|
+
}
|
|
67
|
+
self.fetcher = fetcher
|
|
68
|
+
|
|
69
|
+
let mgr = CLLocationManager()
|
|
70
|
+
mgr.delegate = fetcher
|
|
71
|
+
mgr.desiredAccuracy = kCLLocationAccuracyBest
|
|
72
|
+
self.manager = mgr
|
|
73
|
+
mgr.requestLocation() // one-shot
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public func serviceExtensionWillTerminate() {
|
|
77
|
+
BGLocationPushLog.log("serviceExtensionWillTerminate — flushing")
|
|
78
|
+
fetcher?.flushPendingIfNeeded()
|
|
79
|
+
finish()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func finish() {
|
|
83
|
+
guard let completion = completion else { return }
|
|
84
|
+
self.completion = nil
|
|
85
|
+
self.manager = nil
|
|
86
|
+
self.fetcher = nil
|
|
87
|
+
completion()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Pull the location-query id out of the push payload. Accepts several key
|
|
91
|
+
/// spellings so it survives server-side naming differences.
|
|
92
|
+
static func extractLocationQueryId(from payload: [String: Any]) -> String? {
|
|
93
|
+
let candidates = ["location-query", "locationQuery", "location_query",
|
|
94
|
+
"locationQueryId", "queryId", "query-id"]
|
|
95
|
+
// Top level, then nested under "aps".
|
|
96
|
+
var scopes: [[String: Any]] = [payload]
|
|
97
|
+
if let aps = payload["aps"] as? [String: Any] { scopes.append(aps) }
|
|
98
|
+
for scope in scopes {
|
|
99
|
+
for key in candidates {
|
|
100
|
+
if let value = scope[key] {
|
|
101
|
+
if let s = value as? String, !s.isEmpty { return s }
|
|
102
|
+
if let n = value as? NSNumber { return n.stringValue }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return nil
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - Location fetcher + delivery
|
|
111
|
+
|
|
112
|
+
@available(iOS 15.0, *)
|
|
113
|
+
final class BGLocationPushFetcher: NSObject, CLLocationManagerDelegate {
|
|
114
|
+
|
|
115
|
+
private let queryId: String
|
|
116
|
+
private let completion: () -> Void
|
|
117
|
+
private var didComplete = false
|
|
118
|
+
|
|
119
|
+
init(queryId: String, completion: @escaping () -> Void) {
|
|
120
|
+
self.queryId = queryId
|
|
121
|
+
self.completion = completion
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
125
|
+
guard !didComplete, let location = locations.last else { return }
|
|
126
|
+
didComplete = true
|
|
127
|
+
BGLocationPushLog.log("got location \(location.coordinate.latitude),\(location.coordinate.longitude)")
|
|
128
|
+
// Delegate to the shared native deliverer (socket → REST), so the
|
|
129
|
+
// extension and the in-app handler use identical delivery logic.
|
|
130
|
+
BGLocationPushDeliverer.deliver(
|
|
131
|
+
latitude: location.coordinate.latitude,
|
|
132
|
+
longitude: location.coordinate.longitude,
|
|
133
|
+
accuracy: max(location.horizontalAccuracy, 0),
|
|
134
|
+
speed: location.speed,
|
|
135
|
+
heading: location.course,
|
|
136
|
+
altitude: location.altitude,
|
|
137
|
+
timestampISO: Self.iso8601(location.timestamp),
|
|
138
|
+
queryId: queryId
|
|
139
|
+
) { [weak self] _ in
|
|
140
|
+
self?.completion()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
145
|
+
guard !didComplete else { return }
|
|
146
|
+
didComplete = true
|
|
147
|
+
BGLocationPushLog.log("CLLocationManager failed: \(error.localizedDescription)")
|
|
148
|
+
completion()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func flushPendingIfNeeded() {
|
|
152
|
+
guard !didComplete else { return }
|
|
153
|
+
BGLocationPushLog.log("terminated before location fix arrived")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private static func iso8601(_ date: Date) -> String {
|
|
157
|
+
let formatter = ISO8601DateFormatter()
|
|
158
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
159
|
+
return formatter.string(from: date)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//
|
|
2
|
+
// BGLocationPushShared.swift
|
|
3
|
+
//
|
|
4
|
+
// Constants shared between the host app (BGLocationManager engine) and the
|
|
5
|
+
// CLLocationPushServiceExtension. Because the extension runs in a SEPARATE
|
|
6
|
+
// process, the only channel between them is an App Group shared UserDefaults
|
|
7
|
+
// suite. Both sides MUST agree on the suite name and the keys below.
|
|
8
|
+
//
|
|
9
|
+
// This file is compiled into BOTH:
|
|
10
|
+
// 1. The `BgGeolocation` pod (so BGLocationManager can WRITE the config)
|
|
11
|
+
// 2. The Location Push Service Extension target (so it can READ the config)
|
|
12
|
+
//
|
|
13
|
+
// ── HOST-APP INTEGRATION ──────────────────────────────────────────────────
|
|
14
|
+
// Consumers of this package must:
|
|
15
|
+
// • Set `BGLocationPushAppGroupIdentifier` in BOTH the host app and extension
|
|
16
|
+
// Info.plist files to the same App Group identifier.
|
|
17
|
+
// • Add that App Group to BOTH the main app target and the extension target.
|
|
18
|
+
//
|
|
19
|
+
|
|
20
|
+
import Foundation
|
|
21
|
+
import os.log
|
|
22
|
+
|
|
23
|
+
// Shared logger for all location-push code (extension + in-app). Compiled into
|
|
24
|
+
// both the pod and the extension target via BGLocationPushShared.swift.
|
|
25
|
+
enum BGLocationPushLog {
|
|
26
|
+
private static let logger = OSLog(subsystem: "com.bggeolocation.locationpush", category: "LocationPush")
|
|
27
|
+
static func log(_ message: String) {
|
|
28
|
+
os_log("%{public}@", log: logger, type: .info, message)
|
|
29
|
+
NSLog("[BGGEO][PushExt] \(message)")
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@objc public final class BGLocationPushShared: NSObject {
|
|
34
|
+
|
|
35
|
+
/// Safe fallback used by the public example. Production apps should set
|
|
36
|
+
/// `BGLocationPushAppGroupIdentifier` in both target Info.plist files.
|
|
37
|
+
public static let defaultAppGroupIdentifier = "group.com.example.bggeolocation"
|
|
38
|
+
public static let infoPlistAppGroupKey = "BGLocationPushAppGroupIdentifier"
|
|
39
|
+
|
|
40
|
+
// UserDefaults keys written by the host app and read by the extension.
|
|
41
|
+
public static let keyAppGroup = "BGLocationPush_appGroup"
|
|
42
|
+
public static let keyUrl = "BGLocationPush_url"
|
|
43
|
+
public static let keyMethod = "BGLocationPush_method"
|
|
44
|
+
public static let keyHeaders = "BGLocationPush_headers"
|
|
45
|
+
public static let keyParams = "BGLocationPush_params"
|
|
46
|
+
public static let keyExtras = "BGLocationPush_extras"
|
|
47
|
+
public static let keyRootProperty = "BGLocationPush_rootProperty"
|
|
48
|
+
public static let keyAuthorization = "BGLocationPush_authorization"
|
|
49
|
+
public static let keyAccessToken = "BGLocationPush_accessToken"
|
|
50
|
+
|
|
51
|
+
// Socket delivery (preferred channel for the extension). Written by the host
|
|
52
|
+
// app via BackgroundGeolocation.setLocationPushConfig({...}). When socketUrl
|
|
53
|
+
// is present the extension tries Socket.IO first, then falls back to REST.
|
|
54
|
+
public static let keySocketUrl = "BGLocationPush_socketUrl"
|
|
55
|
+
public static let keySocketPath = "BGLocationPush_socketPath"
|
|
56
|
+
public static let keySocketEvent = "BGLocationPush_socketEvent"
|
|
57
|
+
public static let keySocketAuthToken = "BGLocationPush_socketAuthToken"
|
|
58
|
+
public static let keySocketTimeout = "BGLocationPush_socketTimeout"
|
|
59
|
+
|
|
60
|
+
// REST fallback used when the socket fails: POST {latitude, longitude,
|
|
61
|
+
// fcmToken, userCurrentTime} to fallbackUrl.
|
|
62
|
+
public static let keyFallbackUrl = "BGLocationPush_fallbackUrl"
|
|
63
|
+
public static let keyFcmToken = "BGLocationPush_fcmToken"
|
|
64
|
+
|
|
65
|
+
// Host-defined payload shape for the upload (socket + REST fallback). A dict
|
|
66
|
+
// whose values may contain tokens like "{latitude}", "{fcmToken}",
|
|
67
|
+
// "{userCurrentTime}" that the deliverer substitutes with live values. When
|
|
68
|
+
// absent, the deliverer uses its built-in default payload.
|
|
69
|
+
public static let keyPayloadTemplate = "BGLocationPush_payloadTemplate"
|
|
70
|
+
|
|
71
|
+
// The location push token (hex) the host app obtained from
|
|
72
|
+
// `startMonitoringLocationPushesWithCompletion`. Stored in BOTH the standard
|
|
73
|
+
// and the shared suite so JS can read it via getLocationPushToken().
|
|
74
|
+
public static let keyLocationPushToken = "BGLocationManager_locationPushToken"
|
|
75
|
+
|
|
76
|
+
/// Resolve the App Group identifier. Allows the host app to override the
|
|
77
|
+
/// default at runtime by writing `keyAppGroup` into standard UserDefaults
|
|
78
|
+
/// BEFORE the engine syncs config (rarely needed — the default is fine for
|
|
79
|
+
/// most apps).
|
|
80
|
+
@objc public static func resolveAppGroupIdentifier() -> String {
|
|
81
|
+
if let override = UserDefaults.standard.string(forKey: keyAppGroup),
|
|
82
|
+
!override.isEmpty {
|
|
83
|
+
return override
|
|
84
|
+
}
|
|
85
|
+
if let configured = Bundle.main.object(
|
|
86
|
+
forInfoDictionaryKey: infoPlistAppGroupKey
|
|
87
|
+
) as? String, !configured.isEmpty {
|
|
88
|
+
return configured
|
|
89
|
+
}
|
|
90
|
+
return defaultAppGroupIdentifier
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// The shared UserDefaults suite, or nil if the App Group is not configured
|
|
94
|
+
/// (entitlement missing). Callers must handle nil gracefully.
|
|
95
|
+
@objc public static func sharedDefaults() -> UserDefaults? {
|
|
96
|
+
UserDefaults(suiteName: resolveAppGroupIdentifier())
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
//
|
|
2
|
+
// BGLocationPushSocketClient.swift
|
|
3
|
+
//
|
|
4
|
+
// A minimal Socket.IO v4 (Engine.IO v4) client implemented directly on
|
|
5
|
+
// URLSessionWebSocketTask. The Location Push Service Extension runs in a tiny,
|
|
6
|
+
// RN-less Swift process, so it cannot load `socket.io-client` (a JS library).
|
|
7
|
+
// This emits a single `location:update` event with an ack, matching the app's
|
|
8
|
+
// JS socket protocol, then tears the connection down.
|
|
9
|
+
//
|
|
10
|
+
// Protocol (Socket.IO v4 over a single WebSocket):
|
|
11
|
+
// • Connect to wss://<host><path>/?EIO=4&transport=websocket
|
|
12
|
+
// • Server → "0{...}" Engine.IO OPEN (we ignore the body)
|
|
13
|
+
// • Client → "40{auth-json}" Socket.IO CONNECT to namespace "/" with auth
|
|
14
|
+
// • Server → "40{\"sid\":...}" CONNECT ack (success) | "44{...}" connect_error
|
|
15
|
+
// • Client → "420[\"event\",payload]" MESSAGE + EVENT with ack id 0
|
|
16
|
+
// • Server → "430[ackPayload]" ack for id 0
|
|
17
|
+
// • Engine.IO PING "2" → we reply PONG "3"
|
|
18
|
+
//
|
|
19
|
+
// Everything is bounded by `timeout` so we never blow the extension budget.
|
|
20
|
+
// Extension-safe: only Foundation + URLSession.
|
|
21
|
+
//
|
|
22
|
+
|
|
23
|
+
import Foundation
|
|
24
|
+
|
|
25
|
+
@available(iOS 15.0, *)
|
|
26
|
+
final class BGLocationPushSocketClient: NSObject {
|
|
27
|
+
|
|
28
|
+
struct Config {
|
|
29
|
+
let url: URL // base, e.g. https://host (ws/http both accepted)
|
|
30
|
+
let path: String // socket.io path, e.g. /socket/location
|
|
31
|
+
let event: String // event name, e.g. location:update
|
|
32
|
+
let authToken: String? // sent in the CONNECT auth payload as {"token": ...}
|
|
33
|
+
let timeout: TimeInterval
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private let config: Config
|
|
37
|
+
private var task: URLSessionWebSocketTask?
|
|
38
|
+
private var session: URLSession?
|
|
39
|
+
private var completion: ((Bool) -> Void)?
|
|
40
|
+
private var didFinish = false
|
|
41
|
+
private var connectAcked = false
|
|
42
|
+
private var payload: [String: Any] = [:]
|
|
43
|
+
private var timeoutWorkItem: DispatchWorkItem?
|
|
44
|
+
|
|
45
|
+
init(config: Config) {
|
|
46
|
+
self.config = config
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Emit one event with `payload` and resolve `true` once the server acks it,
|
|
50
|
+
/// or `false` on any failure / timeout.
|
|
51
|
+
func emit(_ payload: [String: Any], completion: @escaping (Bool) -> Void) {
|
|
52
|
+
self.payload = payload
|
|
53
|
+
self.completion = completion
|
|
54
|
+
|
|
55
|
+
guard let wsURL = makeWebSocketURL() else {
|
|
56
|
+
finish(false)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let session = URLSession(configuration: .ephemeral)
|
|
61
|
+
self.session = session
|
|
62
|
+
let task = session.webSocketTask(with: wsURL)
|
|
63
|
+
self.task = task
|
|
64
|
+
|
|
65
|
+
let deadline = DispatchWorkItem { [weak self] in
|
|
66
|
+
BGLocationPushLog.log("socket timed out")
|
|
67
|
+
self?.finish(false)
|
|
68
|
+
}
|
|
69
|
+
timeoutWorkItem = deadline
|
|
70
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + config.timeout, execute: deadline)
|
|
71
|
+
|
|
72
|
+
task.resume()
|
|
73
|
+
receiveLoop()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// MARK: - URL
|
|
77
|
+
|
|
78
|
+
private func makeWebSocketURL() -> URL? {
|
|
79
|
+
guard var components = URLComponents(url: config.url, resolvingAgainstBaseURL: false) else {
|
|
80
|
+
return nil
|
|
81
|
+
}
|
|
82
|
+
switch components.scheme {
|
|
83
|
+
case "https", "wss": components.scheme = "wss"
|
|
84
|
+
default: components.scheme = "ws"
|
|
85
|
+
}
|
|
86
|
+
// socket.io expects the engine.io handshake at <path>/ with these query items.
|
|
87
|
+
var path = config.path
|
|
88
|
+
if !path.hasPrefix("/") { path = "/" + path }
|
|
89
|
+
if !path.hasSuffix("/") { path += "/" }
|
|
90
|
+
components.path = path
|
|
91
|
+
components.queryItems = [
|
|
92
|
+
URLQueryItem(name: "EIO", value: "4"),
|
|
93
|
+
URLQueryItem(name: "transport", value: "websocket")
|
|
94
|
+
]
|
|
95
|
+
return components.url
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - Receive loop
|
|
99
|
+
|
|
100
|
+
private func receiveLoop() {
|
|
101
|
+
task?.receive { [weak self] result in
|
|
102
|
+
guard let self = self, !self.didFinish else { return }
|
|
103
|
+
switch result {
|
|
104
|
+
case .failure(let error):
|
|
105
|
+
BGLocationPushLog.log("socket receive error: \(error.localizedDescription)")
|
|
106
|
+
self.finish(false)
|
|
107
|
+
case .success(let message):
|
|
108
|
+
switch message {
|
|
109
|
+
case .string(let text): self.handle(text)
|
|
110
|
+
case .data(let data): self.handle(String(decoding: data, as: UTF8.self))
|
|
111
|
+
@unknown default: break
|
|
112
|
+
}
|
|
113
|
+
if !self.didFinish { self.receiveLoop() }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private func handle(_ text: String) {
|
|
119
|
+
guard let first = text.first else { return }
|
|
120
|
+
switch first {
|
|
121
|
+
case "0": // Engine.IO OPEN → send Socket.IO CONNECT with auth
|
|
122
|
+
sendConnect()
|
|
123
|
+
case "2": // Engine.IO PING → PONG
|
|
124
|
+
send("3")
|
|
125
|
+
case "4": // Engine.IO MESSAGE → inspect Socket.IO packet type (2nd char)
|
|
126
|
+
handleSocketIO(text)
|
|
127
|
+
default:
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private func handleSocketIO(_ text: String) {
|
|
133
|
+
// text[0] == "4" (MESSAGE). text[1] == Socket.IO packet type.
|
|
134
|
+
let idx = text.index(text.startIndex, offsetBy: 1)
|
|
135
|
+
guard idx < text.endIndex else { return }
|
|
136
|
+
let type = text[idx]
|
|
137
|
+
switch type {
|
|
138
|
+
case "0": // CONNECT success → now emit the event
|
|
139
|
+
connectAcked = true
|
|
140
|
+
sendEvent()
|
|
141
|
+
case "3": // ACK for our emit → success
|
|
142
|
+
BGLocationPushLog.log("socket ack received")
|
|
143
|
+
finish(true)
|
|
144
|
+
case "4": // CONNECT_ERROR
|
|
145
|
+
BGLocationPushLog.log("socket connect_error: \(text)")
|
|
146
|
+
finish(false)
|
|
147
|
+
default:
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// MARK: - Send
|
|
153
|
+
|
|
154
|
+
private func sendConnect() {
|
|
155
|
+
var frame = "40"
|
|
156
|
+
if let token = config.authToken, !token.isEmpty,
|
|
157
|
+
let data = try? JSONSerialization.data(withJSONObject: ["token": token]),
|
|
158
|
+
let json = String(data: data, encoding: .utf8) {
|
|
159
|
+
frame += json
|
|
160
|
+
}
|
|
161
|
+
send(frame)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func sendEvent() {
|
|
165
|
+
guard let data = try? JSONSerialization.data(withJSONObject: [config.event, payload]),
|
|
166
|
+
let json = String(data: data, encoding: .utf8) else {
|
|
167
|
+
finish(false)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
// "42" = MESSAGE+EVENT, "0" = ack id we expect echoed back as "430[...]".
|
|
171
|
+
send("420" + json)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private func send(_ frame: String) {
|
|
175
|
+
task?.send(.string(frame)) { [weak self] error in
|
|
176
|
+
if let error = error {
|
|
177
|
+
BGLocationPushLog.log("socket send error: \(error.localizedDescription)")
|
|
178
|
+
self?.finish(false)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// MARK: - Teardown
|
|
184
|
+
|
|
185
|
+
private func finish(_ success: Bool) {
|
|
186
|
+
guard !didFinish else { return }
|
|
187
|
+
didFinish = true
|
|
188
|
+
timeoutWorkItem?.cancel()
|
|
189
|
+
timeoutWorkItem = nil
|
|
190
|
+
task?.cancel(with: .goingAway, reason: nil)
|
|
191
|
+
task = nil
|
|
192
|
+
session?.invalidateAndCancel()
|
|
193
|
+
session = nil
|
|
194
|
+
let cb = completion
|
|
195
|
+
completion = nil
|
|
196
|
+
cb?(success)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeBgGeolocation.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;AA2OpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,eAAe,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export const BOOT = 'boot';
|
|
4
|
+
export const TERMINATE = 'terminate';
|
|
5
|
+
export const LOCATION = 'location';
|
|
6
|
+
export const HTTP = 'http';
|
|
7
|
+
export const MOTIONCHANGE = 'motionchange';
|
|
8
|
+
export const PROVIDERCHANGE = 'providerchange';
|
|
9
|
+
export const HEARTBEAT = 'heartbeat';
|
|
10
|
+
export const ACTIVITYCHANGE = 'activitychange';
|
|
11
|
+
export const GEOFENCE = 'geofence';
|
|
12
|
+
export const GEOFENCESCHANGE = 'geofenceschange';
|
|
13
|
+
export const SCHEDULE = 'schedule';
|
|
14
|
+
export const CONNECTIVITYCHANGE = 'connectivitychange';
|
|
15
|
+
export const ENABLEDCHANGE = 'enabledchange';
|
|
16
|
+
export const POWERSAVECHANGE = 'powersavechange';
|
|
17
|
+
export const NOTIFICATIONACTION = 'notificationaction';
|
|
18
|
+
export const AUTHORIZATION = 'authorization';
|
|
19
|
+
export const LOCATIONPUSH = 'locationpush';
|
|
20
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["BOOT","TERMINATE","LOCATION","HTTP","MOTIONCHANGE","PROVIDERCHANGE","HEARTBEAT","ACTIVITYCHANGE","GEOFENCE","GEOFENCESCHANGE","SCHEDULE","CONNECTIVITYCHANGE","ENABLEDCHANGE","POWERSAVECHANGE","NOTIFICATIONACTION","AUTHORIZATION","LOCATIONPUSH"],"sourceRoot":"../../src","sources":["events.ts"],"mappings":";;AAAA,OAAO,MAAMA,IAAI,GAAG,MAAM;AAC1B,OAAO,MAAMC,SAAS,GAAG,WAAW;AACpC,OAAO,MAAMC,QAAQ,GAAG,UAAU;AAClC,OAAO,MAAMC,IAAI,GAAG,MAAM;AAC1B,OAAO,MAAMC,YAAY,GAAG,cAAc;AAC1C,OAAO,MAAMC,cAAc,GAAG,gBAAgB;AAC9C,OAAO,MAAMC,SAAS,GAAG,WAAW;AACpC,OAAO,MAAMC,cAAc,GAAG,gBAAgB;AAC9C,OAAO,MAAMC,QAAQ,GAAG,UAAU;AAClC,OAAO,MAAMC,eAAe,GAAG,iBAAiB;AAChD,OAAO,MAAMC,QAAQ,GAAG,UAAU;AAClC,OAAO,MAAMC,kBAAkB,GAAG,oBAAoB;AACtD,OAAO,MAAMC,aAAa,GAAG,eAAe;AAC5C,OAAO,MAAMC,eAAe,GAAG,iBAAiB;AAChD,OAAO,MAAMC,kBAAkB,GAAG,oBAAoB;AACtD,OAAO,MAAMC,aAAa,GAAG,eAAe;AAC5C,OAAO,MAAMC,YAAY,GAAG,cAAc","ignoreList":[]}
|