react-native-nitro-location-tracking 0.1.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/LICENSE +20 -0
- package/NitroLocationTracking.podspec +29 -0
- package/README.md +39 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +122 -0
- package/android/src/main/AndroidManifest.xml +18 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/ConnectionManager.kt +137 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/LocationEngine.kt +93 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/LocationForegroundService.kt +65 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NativeDBWriter.kt +80 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTracking.kt +180 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTrackingPackage.kt +22 -0
- package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NotificationService.kt +75 -0
- package/ios/ConnectionManager.swift +144 -0
- package/ios/LocationEngine.swift +146 -0
- package/ios/NativeDBWriter.swift +121 -0
- package/ios/NitroLocationTracking.swift +127 -0
- package/ios/NotificationService.swift +31 -0
- package/lib/module/LocationSmoother.js +33 -0
- package/lib/module/LocationSmoother.js.map +1 -0
- package/lib/module/NitroLocationTracking.nitro.js +4 -0
- package/lib/module/NitroLocationTracking.nitro.js.map +1 -0
- package/lib/module/bearing.js +19 -0
- package/lib/module/bearing.js.map +1 -0
- package/lib/module/db.js +234 -0
- package/lib/module/db.js.map +1 -0
- package/lib/module/index.js +68 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/LocationSmoother.d.ts +19 -0
- package/lib/typescript/src/LocationSmoother.d.ts.map +1 -0
- package/lib/typescript/src/NitroLocationTracking.nitro.d.ts +59 -0
- package/lib/typescript/src/NitroLocationTracking.nitro.d.ts.map +1 -0
- package/lib/typescript/src/bearing.d.ts +9 -0
- package/lib/typescript/src/bearing.d.ts.map +1 -0
- package/lib/typescript/src/db.d.ts +1 -0
- package/lib/typescript/src/db.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +21 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JAccuracyLevel.hpp +61 -0
- package/nitrogen/generated/android/c++/JConnectionConfig.hpp +81 -0
- package/nitrogen/generated/android/c++/JConnectionState.hpp +61 -0
- package/nitrogen/generated/android/c++/JFunc_void_ConnectionState.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_LocationData.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.cpp +179 -0
- package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.hpp +83 -0
- package/nitrogen/generated/android/c++/JLocationConfig.hpp +91 -0
- package/nitrogen/generated/android/c++/JLocationData.hpp +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/AccuracyLevel.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/ConnectionConfig.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/ConnectionState.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_ConnectionState.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_LocationData.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_std__string.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/HybridNitroLocationTrackingSpec.kt +146 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/LocationConfig.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/LocationData.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/nitrolocationtrackingOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrolocationtracking+autolinking.cmake +81 -0
- package/nitrogen/generated/android/nitrolocationtracking+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrolocationtrackingOnLoad.cpp +52 -0
- package/nitrogen/generated/android/nitrolocationtrackingOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/NitroLocationTracking+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Bridge.cpp +73 -0
- package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Bridge.hpp +231 -0
- package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Umbrella.hpp +61 -0
- package/nitrogen/generated/ios/NitroLocationTrackingAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroLocationTrackingAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridNitroLocationTrackingSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroLocationTrackingSpecSwift.hpp +206 -0
- package/nitrogen/generated/ios/swift/AccuracyLevel.swift +44 -0
- package/nitrogen/generated/ios/swift/ConnectionConfig.swift +59 -0
- package/nitrogen/generated/ios/swift/ConnectionState.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void_ConnectionState.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_LocationData.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec.swift +72 -0
- package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec_cxx.swift +362 -0
- package/nitrogen/generated/ios/swift/LocationConfig.swift +69 -0
- package/nitrogen/generated/ios/swift/LocationData.swift +59 -0
- package/nitrogen/generated/shared/c++/AccuracyLevel.hpp +80 -0
- package/nitrogen/generated/shared/c++/ConnectionConfig.hpp +107 -0
- package/nitrogen/generated/shared/c++/ConnectionState.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.cpp +38 -0
- package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.hpp +92 -0
- package/nitrogen/generated/shared/c++/LocationConfig.hpp +117 -0
- package/nitrogen/generated/shared/c++/LocationData.hpp +107 -0
- package/package.json +174 -0
- package/src/LocationSmoother.ts +46 -0
- package/src/NitroLocationTracking.nitro.ts +79 -0
- package/src/bearing.ts +22 -0
- package/src/db.ts +232 -0
- package/src/index.tsx +92 -0
package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTracking.kt
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrolocationtracking
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
import com.margelo.nitro.NitroModules
|
|
6
|
+
import com.margelo.nitro.core.Promise
|
|
7
|
+
import kotlin.coroutines.resume
|
|
8
|
+
import kotlin.coroutines.resumeWithException
|
|
9
|
+
import kotlin.coroutines.suspendCoroutine
|
|
10
|
+
|
|
11
|
+
@DoNotStrip
|
|
12
|
+
class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
|
|
13
|
+
|
|
14
|
+
companion object {
|
|
15
|
+
private const val TAG = "NitroLocationTracking"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private var locationEngine: LocationEngine? = null
|
|
19
|
+
private var connectionManager = ConnectionManager()
|
|
20
|
+
private var dbWriter: NativeDBWriter? = null
|
|
21
|
+
private var notificationService: NotificationService? = null
|
|
22
|
+
|
|
23
|
+
private var locationCallback: ((LocationData) -> Unit)? = null
|
|
24
|
+
private var motionCallback: ((Boolean) -> Unit)? = null
|
|
25
|
+
private var connectionStateCallback: ((ConnectionState) -> Unit)? = null
|
|
26
|
+
private var messageCallback: ((String) -> Unit)? = null
|
|
27
|
+
|
|
28
|
+
private var locationConfig: LocationConfig? = null
|
|
29
|
+
|
|
30
|
+
private fun ensureInitialized(): Boolean {
|
|
31
|
+
if (locationEngine != null) return true
|
|
32
|
+
val context = NitroModules.applicationContext
|
|
33
|
+
if (context == null) {
|
|
34
|
+
Log.e(TAG, "NitroModules.applicationContext is null — cannot initialize")
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
locationEngine = LocationEngine(context)
|
|
38
|
+
dbWriter = NativeDBWriter(context)
|
|
39
|
+
notificationService = NotificationService(context)
|
|
40
|
+
locationEngine?.dbWriter = dbWriter
|
|
41
|
+
connectionManager.dbWriter = dbWriter
|
|
42
|
+
Log.d(TAG, "Components initialized successfully")
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// === Location Engine ===
|
|
47
|
+
|
|
48
|
+
override fun configure(config: LocationConfig) {
|
|
49
|
+
locationConfig = config
|
|
50
|
+
ensureInitialized()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun startTracking() {
|
|
54
|
+
val config = locationConfig ?: run {
|
|
55
|
+
Log.w(TAG, "startTracking called but no config set — call configure() first")
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (!ensureInitialized()) {
|
|
59
|
+
Log.e(TAG, "startTracking failed — could not initialize components")
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
val engine = locationEngine ?: return
|
|
63
|
+
engine.onLocation = { data ->
|
|
64
|
+
locationCallback?.invoke(data)
|
|
65
|
+
}
|
|
66
|
+
engine.onMotionChange = { isMoving ->
|
|
67
|
+
motionCallback?.invoke(isMoving)
|
|
68
|
+
}
|
|
69
|
+
engine.start(config)
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
notificationService?.startForegroundService(
|
|
73
|
+
config.foregroundNotificationTitle,
|
|
74
|
+
config.foregroundNotificationText
|
|
75
|
+
)
|
|
76
|
+
} catch (e: SecurityException) {
|
|
77
|
+
Log.w(TAG, "Could not start foreground service — missing runtime permissions: ${e.message}")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override fun stopTracking() {
|
|
82
|
+
locationEngine?.stop()
|
|
83
|
+
notificationService?.stopForegroundService()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun getCurrentLocation(): Promise<LocationData> {
|
|
87
|
+
return Promise.async {
|
|
88
|
+
suspendCoroutine { cont ->
|
|
89
|
+
if (!ensureInitialized()) {
|
|
90
|
+
cont.resumeWithException(Exception("Location engine not initialized — context unavailable"))
|
|
91
|
+
return@suspendCoroutine
|
|
92
|
+
}
|
|
93
|
+
val engine = locationEngine!!
|
|
94
|
+
engine.getCurrentLocation { data ->
|
|
95
|
+
if (data != null) {
|
|
96
|
+
cont.resume(data)
|
|
97
|
+
} else {
|
|
98
|
+
cont.resumeWithException(Exception("Failed to get location"))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override fun isTracking(): Boolean {
|
|
106
|
+
return locationEngine?.isTracking ?: false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override fun onLocation(callback: (location: LocationData) -> Unit) {
|
|
110
|
+
locationCallback = callback
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override fun onMotionChange(callback: (isMoving: Boolean) -> Unit) {
|
|
114
|
+
motionCallback = callback
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// === Connection Manager ===
|
|
118
|
+
|
|
119
|
+
override fun configureConnection(config: ConnectionConfig) {
|
|
120
|
+
connectionManager.configure(config)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
override fun connectWebSocket() {
|
|
124
|
+
connectionManager.onStateChange = { state ->
|
|
125
|
+
connectionStateCallback?.invoke(state)
|
|
126
|
+
}
|
|
127
|
+
connectionManager.onMessage = { message ->
|
|
128
|
+
messageCallback?.invoke(message)
|
|
129
|
+
}
|
|
130
|
+
connectionManager.connect()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
override fun disconnectWebSocket() {
|
|
134
|
+
connectionManager.disconnect()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
override fun sendMessage(message: String) {
|
|
138
|
+
connectionManager.send(message)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override fun getConnectionState(): ConnectionState {
|
|
142
|
+
return connectionManager.getState()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override fun onConnectionStateChange(callback: (state: ConnectionState) -> Unit) {
|
|
146
|
+
connectionStateCallback = callback
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override fun onMessage(callback: (message: String) -> Unit) {
|
|
150
|
+
messageCallback = callback
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// === Sync Control ===
|
|
154
|
+
|
|
155
|
+
override fun forceSync(): Promise<Boolean> {
|
|
156
|
+
return Promise.async {
|
|
157
|
+
connectionManager.flushQueue()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// === Notifications ===
|
|
162
|
+
|
|
163
|
+
override fun showLocalNotification(title: String, body: String) {
|
|
164
|
+
ensureInitialized()
|
|
165
|
+
notificationService?.showLocalNotification(title, body)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override fun updateForegroundNotification(title: String, body: String) {
|
|
169
|
+
ensureInitialized()
|
|
170
|
+
notificationService?.updateForegroundNotification(title, body)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// === Lifecycle ===
|
|
174
|
+
|
|
175
|
+
override fun destroy() {
|
|
176
|
+
locationEngine?.stop()
|
|
177
|
+
connectionManager.disconnect()
|
|
178
|
+
notificationService?.stopForegroundService()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrolocationtracking
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
|
|
8
|
+
class NitroLocationTrackingPackage : BaseReactPackage() {
|
|
9
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
14
|
+
return ReactModuleInfoProvider { HashMap() }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
init {
|
|
19
|
+
System.loadLibrary("nitrolocationtracking")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrolocationtracking
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import androidx.core.app.NotificationCompat
|
|
9
|
+
import androidx.core.app.NotificationManagerCompat
|
|
10
|
+
|
|
11
|
+
class NotificationService(private val context: Context) {
|
|
12
|
+
|
|
13
|
+
companion object {
|
|
14
|
+
const val LOCAL_CHANNEL_ID = "nitro_location_local"
|
|
15
|
+
private var notificationCounter = 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
init {
|
|
19
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
20
|
+
val channel = NotificationChannel(
|
|
21
|
+
LOCAL_CHANNEL_ID,
|
|
22
|
+
"Location Notifications",
|
|
23
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
24
|
+
)
|
|
25
|
+
(context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
|
|
26
|
+
.createNotificationChannel(channel)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun showLocalNotification(title: String, body: String) {
|
|
31
|
+
val notification = NotificationCompat.Builder(context, LOCAL_CHANNEL_ID)
|
|
32
|
+
.setContentTitle(title)
|
|
33
|
+
.setContentText(body)
|
|
34
|
+
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
|
|
35
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
36
|
+
.setAutoCancel(true)
|
|
37
|
+
.build()
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
NotificationManagerCompat.from(context)
|
|
41
|
+
.notify(++notificationCounter, notification)
|
|
42
|
+
} catch (_: SecurityException) {
|
|
43
|
+
// Permission not granted
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fun updateForegroundNotification(title: String, body: String) {
|
|
48
|
+
val intent = Intent(context, LocationForegroundService::class.java).apply {
|
|
49
|
+
action = LocationForegroundService.ACTION_UPDATE
|
|
50
|
+
putExtra("title", title)
|
|
51
|
+
putExtra("text", body)
|
|
52
|
+
}
|
|
53
|
+
context.startService(intent)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fun startForegroundService(title: String, text: String) {
|
|
57
|
+
val intent = Intent(context, LocationForegroundService::class.java).apply {
|
|
58
|
+
action = LocationForegroundService.ACTION_START
|
|
59
|
+
putExtra("title", title)
|
|
60
|
+
putExtra("text", text)
|
|
61
|
+
}
|
|
62
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
63
|
+
context.startForegroundService(intent)
|
|
64
|
+
} else {
|
|
65
|
+
context.startService(intent)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun stopForegroundService() {
|
|
70
|
+
val intent = Intent(context, LocationForegroundService::class.java).apply {
|
|
71
|
+
action = LocationForegroundService.ACTION_STOP
|
|
72
|
+
}
|
|
73
|
+
context.startService(intent)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
class ConnectionManager: NSObject, URLSessionWebSocketDelegate {
|
|
4
|
+
private var webSocket: URLSessionWebSocketTask?
|
|
5
|
+
private var session: URLSession?
|
|
6
|
+
private var config: ConnectionConfig?
|
|
7
|
+
private var reconnectAttempts = 0
|
|
8
|
+
private var connected = false
|
|
9
|
+
private var syncTimer: Timer?
|
|
10
|
+
|
|
11
|
+
var onStateChange: ((ConnectionState) -> Void)?
|
|
12
|
+
var onMessage: ((String) -> Void)?
|
|
13
|
+
var dbWriter: NativeDBWriter?
|
|
14
|
+
|
|
15
|
+
var isConnected: Bool { connected }
|
|
16
|
+
|
|
17
|
+
func configure(_ config: ConnectionConfig) {
|
|
18
|
+
self.config = config
|
|
19
|
+
session = URLSession(configuration: .default,
|
|
20
|
+
delegate: self, delegateQueue: OperationQueue())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func connect() {
|
|
24
|
+
guard let config = config,
|
|
25
|
+
let url = URL(string: config.wsUrl) else { return }
|
|
26
|
+
var request = URLRequest(url: url)
|
|
27
|
+
request.setValue("Bearer \(config.authToken)",
|
|
28
|
+
forHTTPHeaderField: "Authorization")
|
|
29
|
+
webSocket = session?.webSocketTask(with: request)
|
|
30
|
+
webSocket?.resume()
|
|
31
|
+
startListening()
|
|
32
|
+
startSyncTimer()
|
|
33
|
+
connected = true
|
|
34
|
+
reconnectAttempts = 0
|
|
35
|
+
onStateChange?(.connected)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func disconnect() {
|
|
39
|
+
syncTimer?.invalidate()
|
|
40
|
+
webSocket?.cancel(with: .normalClosure, reason: nil)
|
|
41
|
+
connected = false
|
|
42
|
+
onStateChange?(.disconnected)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func send(_ message: String) {
|
|
46
|
+
webSocket?.send(.string(message)) { [weak self] error in
|
|
47
|
+
if error != nil { self?.handleDisconnect() }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func getState() -> ConnectionState {
|
|
52
|
+
return connected ? .connected : .disconnected
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private func startListening() {
|
|
56
|
+
webSocket?.receive { [weak self] result in
|
|
57
|
+
switch result {
|
|
58
|
+
case .success(let msg):
|
|
59
|
+
if case .string(let text) = msg {
|
|
60
|
+
self?.onMessage?(text)
|
|
61
|
+
}
|
|
62
|
+
self?.startListening()
|
|
63
|
+
case .failure(_):
|
|
64
|
+
self?.handleDisconnect()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private func handleDisconnect() {
|
|
70
|
+
guard let config = config else { return }
|
|
71
|
+
connected = false
|
|
72
|
+
onStateChange?(.reconnecting)
|
|
73
|
+
guard reconnectAttempts < Int(config.maxReconnectAttempts) else {
|
|
74
|
+
onStateChange?(.disconnected)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
reconnectAttempts += 1
|
|
78
|
+
let delay = config.reconnectIntervalMs / 1000.0
|
|
79
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
|
|
80
|
+
[weak self] in self?.connect()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func startSyncTimer() {
|
|
85
|
+
guard let config = config else { return }
|
|
86
|
+
DispatchQueue.main.async {
|
|
87
|
+
self.syncTimer = Timer.scheduledTimer(
|
|
88
|
+
withTimeInterval: config.syncIntervalMs / 1000.0,
|
|
89
|
+
repeats: true
|
|
90
|
+
) { [weak self] _ in self?.flushQueue() }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func flushQueue() -> Bool {
|
|
95
|
+
guard let config = config, let dbWriter = dbWriter else { return false }
|
|
96
|
+
let queued = dbWriter.getUnsyncedBatch(limit: Int(config.batchSize))
|
|
97
|
+
guard !queued.isEmpty else { return true }
|
|
98
|
+
|
|
99
|
+
let ids = queued.map { $0.id }
|
|
100
|
+
let payload: [[String: Any]] = queued.map { item in
|
|
101
|
+
[
|
|
102
|
+
"latitude": item.data.latitude,
|
|
103
|
+
"longitude": item.data.longitude,
|
|
104
|
+
"altitude": item.data.altitude,
|
|
105
|
+
"speed": item.data.speed,
|
|
106
|
+
"bearing": item.data.bearing,
|
|
107
|
+
"accuracy": item.data.accuracy,
|
|
108
|
+
"timestamp": item.data.timestamp
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if connected {
|
|
113
|
+
if let json = try? JSONSerialization.data(withJSONObject: payload),
|
|
114
|
+
let str = String(data: json, encoding: .utf8) {
|
|
115
|
+
send(str)
|
|
116
|
+
dbWriter.markSynced(ids)
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
sendBatchHTTP(payload, ids: ids)
|
|
121
|
+
}
|
|
122
|
+
return true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func sendBatchHTTP(_ locations: [[String: Any]], ids: [String]) {
|
|
126
|
+
guard let config = config,
|
|
127
|
+
let url = URL(string: "\(config.restUrl)/locations/batch")
|
|
128
|
+
else { return }
|
|
129
|
+
|
|
130
|
+
var request = URLRequest(url: url)
|
|
131
|
+
request.httpMethod = "POST"
|
|
132
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
133
|
+
request.setValue("Bearer \(config.authToken)",
|
|
134
|
+
forHTTPHeaderField: "Authorization")
|
|
135
|
+
request.httpBody = try? JSONSerialization.data(withJSONObject: locations)
|
|
136
|
+
|
|
137
|
+
URLSession.shared.dataTask(with: request) { [weak self] _, response, error in
|
|
138
|
+
let http = response as? HTTPURLResponse
|
|
139
|
+
if error == nil && http?.statusCode == 200 {
|
|
140
|
+
self?.dbWriter?.markSynced(ids)
|
|
141
|
+
}
|
|
142
|
+
}.resume()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import CoreLocation
|
|
2
|
+
|
|
3
|
+
class LocationEngine: NSObject, CLLocationManagerDelegate {
|
|
4
|
+
private let locationManager = CLLocationManager()
|
|
5
|
+
private var tracking = false
|
|
6
|
+
|
|
7
|
+
var onLocation: ((LocationData) -> Void)?
|
|
8
|
+
var onMotionChange: ((Bool) -> Void)?
|
|
9
|
+
var dbWriter: NativeDBWriter?
|
|
10
|
+
var currentRideId: String?
|
|
11
|
+
|
|
12
|
+
override init() {
|
|
13
|
+
super.init()
|
|
14
|
+
locationManager.delegate = self
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func configure(_ config: LocationConfig) {
|
|
18
|
+
switch config.desiredAccuracy {
|
|
19
|
+
case .high:
|
|
20
|
+
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
|
21
|
+
case .balanced:
|
|
22
|
+
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
|
23
|
+
default:
|
|
24
|
+
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
locationManager.distanceFilter = config.distanceFilter
|
|
28
|
+
locationManager.allowsBackgroundLocationUpdates = true
|
|
29
|
+
locationManager.showsBackgroundLocationIndicator = true
|
|
30
|
+
locationManager.pausesLocationUpdatesAutomatically = false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func start() {
|
|
34
|
+
guard CLLocationManager.authorizationStatus() == .authorizedAlways ||
|
|
35
|
+
CLLocationManager.authorizationStatus() == .authorizedWhenInUse else {
|
|
36
|
+
locationManager.requestAlwaysAuthorization()
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
locationManager.startUpdatingLocation()
|
|
40
|
+
tracking = true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func stop() {
|
|
44
|
+
locationManager.stopUpdatingLocation()
|
|
45
|
+
tracking = false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var isTracking: Bool { tracking }
|
|
49
|
+
|
|
50
|
+
func getCurrentLocation(completion: @escaping (LocationData?) -> Void) {
|
|
51
|
+
locationManager.requestLocation()
|
|
52
|
+
// Store completion for delegate callback
|
|
53
|
+
pendingLocationCompletion = completion
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private var pendingLocationCompletion: ((LocationData?) -> Void)?
|
|
57
|
+
|
|
58
|
+
func locationManager(_ manager: CLLocationManager,
|
|
59
|
+
didUpdateLocations locations: [CLLocation]) {
|
|
60
|
+
guard let location = locations.last else { return }
|
|
61
|
+
|
|
62
|
+
let data = LocationData(
|
|
63
|
+
latitude: location.coordinate.latitude,
|
|
64
|
+
longitude: location.coordinate.longitude,
|
|
65
|
+
altitude: location.altitude,
|
|
66
|
+
speed: max(location.speed, 0),
|
|
67
|
+
bearing: max(location.course, 0),
|
|
68
|
+
accuracy: location.horizontalAccuracy,
|
|
69
|
+
timestamp: location.timestamp.timeIntervalSince1970 * 1000
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
print(data,"dataaas")
|
|
74
|
+
|
|
75
|
+
// Handle pending one-shot request
|
|
76
|
+
if let completion = pendingLocationCompletion {
|
|
77
|
+
completion(data)
|
|
78
|
+
pendingLocationCompletion = nil
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Save to SQLite immediately (offline-first)
|
|
82
|
+
dbWriter?.insert(data, rideId: currentRideId)
|
|
83
|
+
|
|
84
|
+
// Notify JS via Nitro callback
|
|
85
|
+
onLocation?(data)
|
|
86
|
+
|
|
87
|
+
// Motion detection
|
|
88
|
+
onMotionChange?(location.speed >= 0.5)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func locationManager(_ manager: CLLocationManager,
|
|
92
|
+
didFailWithError error: Error) {
|
|
93
|
+
if let completion = pendingLocationCompletion {
|
|
94
|
+
completion(nil)
|
|
95
|
+
pendingLocationCompletion = nil
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//// Simple struct to pass location data between components
|
|
101
|
+
//struct LocationData {
|
|
102
|
+
// let latitude: Double
|
|
103
|
+
// let longitude: Double
|
|
104
|
+
// let altitude: Double
|
|
105
|
+
// let speed: Double
|
|
106
|
+
// let bearing: Double
|
|
107
|
+
// let accuracy: Double
|
|
108
|
+
// let timestamp: Double
|
|
109
|
+
//
|
|
110
|
+
// var id: String = ""
|
|
111
|
+
//
|
|
112
|
+
// func toJSON() -> [String: Any] {
|
|
113
|
+
// return [
|
|
114
|
+
// "latitude": latitude,
|
|
115
|
+
// "longitude": longitude,
|
|
116
|
+
// "altitude": altitude,
|
|
117
|
+
// "speed": speed,
|
|
118
|
+
// "bearing": bearing,
|
|
119
|
+
// "accuracy": accuracy,
|
|
120
|
+
// "timestamp": timestamp
|
|
121
|
+
// ]
|
|
122
|
+
// }
|
|
123
|
+
//}
|
|
124
|
+
//
|
|
125
|
+
//// Config structs
|
|
126
|
+
//struct LocationConfig {
|
|
127
|
+
// let desiredAccuracy: String
|
|
128
|
+
// let distanceFilter: Double
|
|
129
|
+
// let intervalMs: Double
|
|
130
|
+
// let fastestIntervalMs: Double
|
|
131
|
+
// let stopTimeout: Double
|
|
132
|
+
// let stopOnTerminate: Bool
|
|
133
|
+
// let startOnBoot: Bool
|
|
134
|
+
// let foregroundNotificationTitle: String
|
|
135
|
+
// let foregroundNotificationText: String
|
|
136
|
+
//}
|
|
137
|
+
//
|
|
138
|
+
//struct ConnectionConfig {
|
|
139
|
+
// let wsUrl: String
|
|
140
|
+
// let restUrl: String
|
|
141
|
+
// let authToken: String
|
|
142
|
+
// let reconnectIntervalMs: Double
|
|
143
|
+
// let maxReconnectAttempts: Double
|
|
144
|
+
// let batchSize: Double
|
|
145
|
+
// let syncIntervalMs: Double
|
|
146
|
+
//}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import SQLite3
|
|
2
|
+
|
|
3
|
+
class NativeDBWriter {
|
|
4
|
+
private var db: OpaquePointer?
|
|
5
|
+
|
|
6
|
+
init() {
|
|
7
|
+
let documentsDir = FileManager.default.urls(
|
|
8
|
+
for: .documentDirectory, in: .userDomainMask).first!
|
|
9
|
+
let dbPath = documentsDir
|
|
10
|
+
.appendingPathComponent("nitro_location.db").path
|
|
11
|
+
sqlite3_open(dbPath, &db)
|
|
12
|
+
sqlite3_exec(db, "PRAGMA journal_mode = WAL", nil, nil, nil)
|
|
13
|
+
createTables()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private func createTables() {
|
|
17
|
+
let sql = """
|
|
18
|
+
CREATE TABLE IF NOT EXISTS locations (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
latitude REAL NOT NULL,
|
|
21
|
+
longitude REAL NOT NULL,
|
|
22
|
+
altitude REAL,
|
|
23
|
+
speed REAL,
|
|
24
|
+
bearing REAL,
|
|
25
|
+
accuracy REAL,
|
|
26
|
+
timestamp INTEGER NOT NULL,
|
|
27
|
+
ride_id TEXT,
|
|
28
|
+
synced INTEGER DEFAULT 0,
|
|
29
|
+
retry_count INTEGER DEFAULT 0,
|
|
30
|
+
created_at INTEGER DEFAULT (strftime('%s','now') * 1000)
|
|
31
|
+
);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_locations_timestamp ON locations(timestamp);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_locations_ride_id ON locations(ride_id);
|
|
35
|
+
"""
|
|
36
|
+
sqlite3_exec(db, sql, nil, nil, nil)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func insert(_ location: LocationData, rideId: String? = nil) {
|
|
40
|
+
let sql = """
|
|
41
|
+
INSERT INTO locations
|
|
42
|
+
(id, latitude, longitude, altitude, speed,
|
|
43
|
+
bearing, accuracy, timestamp, ride_id, synced)
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
45
|
+
"""
|
|
46
|
+
var stmt: OpaquePointer?
|
|
47
|
+
if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
|
|
48
|
+
let id = UUID().uuidString
|
|
49
|
+
sqlite3_bind_text(stmt, 1, (id as NSString).utf8String, -1, nil)
|
|
50
|
+
sqlite3_bind_double(stmt, 2, location.latitude)
|
|
51
|
+
sqlite3_bind_double(stmt, 3, location.longitude)
|
|
52
|
+
sqlite3_bind_double(stmt, 4, location.altitude)
|
|
53
|
+
sqlite3_bind_double(stmt, 5, location.speed)
|
|
54
|
+
sqlite3_bind_double(stmt, 6, location.bearing)
|
|
55
|
+
sqlite3_bind_double(stmt, 7, location.accuracy)
|
|
56
|
+
sqlite3_bind_int64(stmt, 8, Int64(location.timestamp))
|
|
57
|
+
if let rideId = rideId {
|
|
58
|
+
sqlite3_bind_text(stmt, 9,
|
|
59
|
+
(rideId as NSString).utf8String, -1, nil)
|
|
60
|
+
} else {
|
|
61
|
+
sqlite3_bind_null(stmt, 9)
|
|
62
|
+
}
|
|
63
|
+
sqlite3_step(stmt)
|
|
64
|
+
}
|
|
65
|
+
sqlite3_finalize(stmt)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func getUnsyncedBatch(limit: Int) -> [(id: String, data: LocationData)] {
|
|
69
|
+
let sql = """
|
|
70
|
+
SELECT id, latitude, longitude, altitude, speed,
|
|
71
|
+
bearing, accuracy, timestamp
|
|
72
|
+
FROM locations WHERE synced = 0
|
|
73
|
+
ORDER BY timestamp ASC LIMIT ?
|
|
74
|
+
"""
|
|
75
|
+
var results: [(id: String, data: LocationData)] = []
|
|
76
|
+
var stmt: OpaquePointer?
|
|
77
|
+
if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
|
|
78
|
+
sqlite3_bind_int(stmt, 1, Int32(limit))
|
|
79
|
+
while sqlite3_step(stmt) == SQLITE_ROW {
|
|
80
|
+
let id = String(cString: sqlite3_column_text(stmt, 0))
|
|
81
|
+
let data = LocationData(
|
|
82
|
+
latitude: sqlite3_column_double(stmt, 1),
|
|
83
|
+
longitude: sqlite3_column_double(stmt, 2),
|
|
84
|
+
altitude: sqlite3_column_double(stmt, 3),
|
|
85
|
+
speed: sqlite3_column_double(stmt, 4),
|
|
86
|
+
bearing: sqlite3_column_double(stmt, 5),
|
|
87
|
+
accuracy: sqlite3_column_double(stmt, 6),
|
|
88
|
+
timestamp: Double(sqlite3_column_int64(stmt, 7))
|
|
89
|
+
)
|
|
90
|
+
results.append((id: id, data: data))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
sqlite3_finalize(stmt)
|
|
94
|
+
return results
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func markSynced(_ ids: [String]) {
|
|
98
|
+
let placeholders = ids.map { _ in "?" }.joined(separator: ",")
|
|
99
|
+
let sql = "UPDATE locations SET synced = 1 WHERE id IN (\(placeholders))"
|
|
100
|
+
var stmt: OpaquePointer?
|
|
101
|
+
if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
|
|
102
|
+
for (i, id) in ids.enumerated() {
|
|
103
|
+
sqlite3_bind_text(stmt, Int32(i + 1),
|
|
104
|
+
(id as NSString).utf8String, -1, nil)
|
|
105
|
+
}
|
|
106
|
+
sqlite3_step(stmt)
|
|
107
|
+
}
|
|
108
|
+
sqlite3_finalize(stmt)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func clearOldSynced() {
|
|
112
|
+
sqlite3_exec(db, """
|
|
113
|
+
DELETE FROM locations WHERE synced = 1
|
|
114
|
+
AND timestamp < (strftime('%s','now') * 1000 - 86400000)
|
|
115
|
+
""", nil, nil, nil)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
deinit {
|
|
119
|
+
sqlite3_close(db)
|
|
120
|
+
}
|
|
121
|
+
}
|