react-native-background-live-tracking 1.0.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.
@@ -0,0 +1,197 @@
1
+ package com.reactnativebackgroundlivetracking
2
+
3
+ import android.content.Context
4
+ import android.net.ConnectivityManager
5
+ import android.net.NetworkCapabilities
6
+ import java.net.URI
7
+ import java.util.concurrent.TimeUnit
8
+ import okhttp3.Headers
9
+ import okhttp3.MediaType.Companion.toMediaType
10
+ import okhttp3.OkHttpClient
11
+ import okhttp3.Request
12
+ import okhttp3.RequestBody.Companion.toRequestBody
13
+ import okhttp3.Response
14
+ import okhttp3.WebSocket
15
+ import okhttp3.WebSocketListener
16
+ import org.json.JSONObject
17
+ import io.socket.client.IO
18
+ import io.socket.client.Socket
19
+
20
+ internal class LocationNetworkClient(
21
+ private val appContext: Context,
22
+ private val prefs: TrackingPreferences,
23
+ private val queue: LocationQueueStore,
24
+ ) {
25
+ private val jsonMedia = "application/json; charset=utf-8".toMediaType()
26
+ private val client =
27
+ OkHttpClient.Builder()
28
+ .connectTimeout(25, TimeUnit.SECONDS)
29
+ .readTimeout(25, TimeUnit.SECONDS)
30
+ .writeTimeout(25, TimeUnit.SECONDS)
31
+ .retryOnConnectionFailure(true)
32
+ .build()
33
+
34
+ @Volatile private var webSocket: WebSocket? = null
35
+ @Volatile private var socketIo: Socket? = null
36
+
37
+ fun connectSocketsIfNeeded() {
38
+ val url = prefs.socketUrl ?: return
39
+ when (prefs.socketTransport) {
40
+ "websocket" -> connectWebSocket(url)
41
+ "socket.io" -> connectSocketIo(url)
42
+ }
43
+ }
44
+
45
+ fun disconnectSockets() {
46
+ try {
47
+ webSocket?.close(1000, "stop")
48
+ } catch (_: Exception) {
49
+ }
50
+ webSocket = null
51
+ try {
52
+ socketIo?.disconnect()
53
+ socketIo?.off()
54
+ } catch (_: Exception) {
55
+ }
56
+ socketIo = null
57
+ }
58
+
59
+ fun sendLocationJson(jsonBody: String): Boolean {
60
+ val online = isOnline()
61
+ if (!online) {
62
+ queue.enqueue(jsonBody)
63
+ return false
64
+ }
65
+ connectSocketsIfNeeded()
66
+ val restOk = postRestWithRetry(jsonBody)
67
+ sendOverRealtime(jsonBody)
68
+ if (!restOk) {
69
+ queue.enqueue(jsonBody)
70
+ }
71
+ return restOk
72
+ }
73
+
74
+ fun flushQueue(): Int {
75
+ if (!isOnline()) return 0
76
+ connectSocketsIfNeeded()
77
+ return queue.drain { line ->
78
+ val ok = postRestWithRetry(line)
79
+ if (ok) {
80
+ sendOverRealtime(line)
81
+ }
82
+ ok
83
+ }
84
+ }
85
+
86
+ private fun sendOverRealtime(jsonBody: String) {
87
+ try {
88
+ webSocket?.send(jsonBody)
89
+ socketIo?.emit("location", JSONObject(jsonBody))
90
+ } catch (_: Exception) {
91
+ }
92
+ }
93
+
94
+ private fun postRestWithRetry(body: String): Boolean {
95
+ repeat(3) { attempt ->
96
+ try {
97
+ if (postRestOnce(body)) return true
98
+ } catch (_: Exception) {
99
+ }
100
+ try {
101
+ Thread.sleep((300L * (attempt + 1)).coerceAtMost(2000L))
102
+ } catch (_: InterruptedException) {
103
+ return false
104
+ }
105
+ }
106
+ return false
107
+ }
108
+
109
+ private fun postRestOnce(body: String): Boolean {
110
+ val url = prefs.serverUrl
111
+ if (url.isBlank()) return false
112
+ val headersBuilder = Headers.Builder()
113
+ try {
114
+ val headersJson = JSONObject(prefs.restHeadersJson)
115
+ val keys = headersJson.keys()
116
+ while (keys.hasNext()) {
117
+ val k = keys.next()
118
+ headersBuilder.add(k, headersJson.optString(k))
119
+ }
120
+ } catch (_: Exception) {
121
+ }
122
+ val request =
123
+ Request.Builder()
124
+ .url(url)
125
+ .headers(headersBuilder.build())
126
+ .post(body.toRequestBody(jsonMedia))
127
+ .build()
128
+ client.newCall(request).execute().use { response ->
129
+ return response.isSuccessful
130
+ }
131
+ }
132
+
133
+ private fun connectWebSocket(rawUrl: String) {
134
+ if (webSocket != null) return
135
+ val wsUrl =
136
+ when {
137
+ rawUrl.startsWith("https://") -> "wss://" + rawUrl.removePrefix("https://")
138
+ rawUrl.startsWith("http://") -> "ws://" + rawUrl.removePrefix("http://")
139
+ else -> rawUrl
140
+ }
141
+ val request = Request.Builder().url(wsUrl).build()
142
+ webSocket =
143
+ client.newWebSocket(
144
+ request,
145
+ object : WebSocketListener() {
146
+ override fun onFailure(
147
+ webSocket: WebSocket,
148
+ t: Throwable,
149
+ response: Response?,
150
+ ) {
151
+ this@LocationNetworkClient.webSocket = null
152
+ }
153
+
154
+ override fun onClosed(
155
+ webSocket: WebSocket,
156
+ code: Int,
157
+ reason: String,
158
+ ) {
159
+ this@LocationNetworkClient.webSocket = null
160
+ }
161
+ },
162
+ )
163
+ }
164
+
165
+ private fun connectSocketIo(rawUrl: String) {
166
+ if (socketIo?.connected() == true) return
167
+ try {
168
+ socketIo?.disconnect()
169
+ socketIo?.off()
170
+ } catch (_: Exception) {
171
+ }
172
+ try {
173
+ val uri = URI.create(toHttpScheme(rawUrl))
174
+ val opts = IO.Options()
175
+ opts.reconnection = true
176
+ socketIo = IO.socket(uri, opts)
177
+ socketIo?.connect()
178
+ } catch (_: Exception) {
179
+ socketIo = null
180
+ }
181
+ }
182
+
183
+ private fun toHttpScheme(url: String): String {
184
+ return when {
185
+ url.startsWith("wss://") -> "https://" + url.removePrefix("wss://")
186
+ url.startsWith("ws://") -> "http://" + url.removePrefix("ws://")
187
+ else -> url
188
+ }
189
+ }
190
+
191
+ private fun isOnline(): Boolean {
192
+ val cm = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
193
+ val network = cm.activeNetwork ?: return false
194
+ val caps = cm.getNetworkCapabilities(network) ?: return false
195
+ return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
196
+ }
197
+ }
@@ -0,0 +1,53 @@
1
+ package com.reactnativebackgroundlivetracking
2
+
3
+ import android.content.Context
4
+ import java.io.File
5
+ import java.util.concurrent.locks.ReentrantLock
6
+ import kotlin.concurrent.withLock
7
+
8
+ internal class LocationQueueStore(context: Context) {
9
+ private val file = File(context.filesDir, "rn_blt_location_queue.jsonl")
10
+ private val lock = ReentrantLock()
11
+ private val maxLines = 2000
12
+
13
+ fun enqueue(line: String) {
14
+ lock.withLock {
15
+ if (!file.exists()) {
16
+ file.createNewFile()
17
+ }
18
+ val lines = file.readLines().toMutableList()
19
+ lines.add(line)
20
+ while (lines.size > maxLines) {
21
+ lines.removeAt(0)
22
+ }
23
+ file.writeText(lines.joinToString("\n") + if (lines.isNotEmpty()) "\n" else "")
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Attempts to process queued payloads. [processor] returns true if the entry was delivered.
29
+ */
30
+ fun drain(processor: (String) -> Boolean): Int {
31
+ lock.withLock {
32
+ if (!file.exists() || file.length() == 0L) return 0
33
+ val lines = file.readLines()
34
+ if (lines.isEmpty()) return 0
35
+ val remaining = mutableListOf<String>()
36
+ var sent = 0
37
+ for (line in lines) {
38
+ if (line.isBlank()) continue
39
+ if (processor(line)) {
40
+ sent++
41
+ } else {
42
+ remaining.add(line)
43
+ }
44
+ }
45
+ if (remaining.isEmpty()) {
46
+ file.delete()
47
+ } else {
48
+ file.writeText(remaining.joinToString("\n") + "\n")
49
+ }
50
+ return sent
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,56 @@
1
+ package com.reactnativebackgroundlivetracking
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.BitmapFactory
5
+ import java.util.concurrent.TimeUnit
6
+ import okhttp3.HttpUrl.Companion.toHttpUrl
7
+ import okhttp3.OkHttpClient
8
+ import okhttp3.Request
9
+
10
+ /** Fetches a map bitmap via Google Static Maps API (not an interactive MapView). */
11
+ internal object StaticMapFetcher {
12
+ private val client =
13
+ OkHttpClient.Builder()
14
+ .connectTimeout(18, TimeUnit.SECONDS)
15
+ .readTimeout(18, TimeUnit.SECONDS)
16
+ .build()
17
+
18
+ fun fetch(
19
+ driverLat: Double,
20
+ driverLng: Double,
21
+ pickupLat: Double?,
22
+ pickupLng: Double?,
23
+ apiKey: String,
24
+ ): Bitmap? {
25
+ if (apiKey.isBlank()) return null
26
+ val urlBuilder =
27
+ "https://maps.googleapis.com/maps/api/staticmap".toHttpUrl().newBuilder()
28
+ .addQueryParameter("center", "$driverLat,$driverLng")
29
+ .addQueryParameter("zoom", "15")
30
+ .addQueryParameter("size", "480x240")
31
+ .addQueryParameter("scale", "2")
32
+ .addQueryParameter("maptype", "roadmap")
33
+ .addQueryParameter(
34
+ "markers",
35
+ "color:0x4285F4|size:mid|$driverLat,$driverLng",
36
+ )
37
+ .addQueryParameter("key", apiKey)
38
+
39
+ if (pickupLat != null && pickupLng != null) {
40
+ urlBuilder.addQueryParameter(
41
+ "markers",
42
+ "color:0x0F9D58|label:P|size:mid|$pickupLat,$pickupLng",
43
+ )
44
+ }
45
+
46
+ val request = Request.Builder().url(urlBuilder.build()).get().build()
47
+ return try {
48
+ client.newCall(request).execute().use { response ->
49
+ if (!response.isSuccessful) return null
50
+ response.body?.byteStream()?.use { BitmapFactory.decodeStream(it) }
51
+ }
52
+ } catch (_: Exception) {
53
+ null
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,58 @@
1
+ package com.reactnativebackgroundlivetracking
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.modules.core.DeviceEventManagerModule
8
+
9
+ internal object TrackingEventBridge {
10
+ @Volatile
11
+ var reactContext: ReactApplicationContext? = null
12
+
13
+ private val mainHandler = Handler(Looper.getMainLooper())
14
+
15
+ fun emitLocation(
16
+ latitude: Double,
17
+ longitude: Double,
18
+ timestamp: Long,
19
+ driverId: String,
20
+ accuracy: Float,
21
+ speed: Float,
22
+ bearing: Float,
23
+ ) {
24
+ val ctx = reactContext ?: return
25
+ val map =
26
+ Arguments.createMap().apply {
27
+ putDouble("latitude", latitude)
28
+ putDouble("longitude", longitude)
29
+ putDouble("timestamp", timestamp.toDouble())
30
+ putString("driverId", driverId)
31
+ putDouble("accuracy", accuracy.toDouble())
32
+ putDouble("speed", speed.toDouble())
33
+ putDouble("bearing", bearing.toDouble())
34
+ }
35
+ mainHandler.post {
36
+ try {
37
+ ctx
38
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
39
+ .emit("RNBackgroundLiveTracking_location", map)
40
+ } catch (_: Exception) {
41
+ // Bridge may be torn down
42
+ }
43
+ }
44
+ }
45
+
46
+ fun emitStatus(active: Boolean) {
47
+ val ctx = reactContext ?: return
48
+ val map = Arguments.createMap().apply { putBoolean("active", active) }
49
+ mainHandler.post {
50
+ try {
51
+ ctx
52
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
53
+ .emit("RNBackgroundLiveTracking_status", map)
54
+ } catch (_: Exception) {
55
+ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,203 @@
1
+ package com.reactnativebackgroundlivetracking
2
+
3
+ import android.Manifest
4
+ import android.content.Intent
5
+ import android.content.pm.PackageManager
6
+ import android.net.Uri
7
+ import android.os.Build
8
+ import android.provider.Settings
9
+ import androidx.core.content.ContextCompat
10
+ import com.facebook.react.bridge.Promise
11
+ import com.facebook.react.bridge.ReactApplicationContext
12
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
13
+ import com.facebook.react.bridge.ReactMethod
14
+ import com.facebook.react.module.annotations.ReactModule
15
+ import org.json.JSONObject
16
+
17
+ @ReactModule(name = RNBackgroundLiveTrackingModule.NAME)
18
+ class RNBackgroundLiveTrackingModule(
19
+ private val reactContext: ReactApplicationContext,
20
+ ) : ReactContextBaseJavaModule(reactContext) {
21
+
22
+ override fun getName(): String = NAME
23
+
24
+ override fun initialize() {
25
+ super.initialize()
26
+ TrackingEventBridge.reactContext = reactContext
27
+ }
28
+
29
+ override fun invalidate() {
30
+ TrackingEventBridge.reactContext = null
31
+ super.invalidate()
32
+ }
33
+
34
+ @ReactMethod
35
+ fun startTracking(configJson: String, promise: Promise) {
36
+ try {
37
+ if (!hasFineLocation()) {
38
+ promise.reject(
39
+ "E_PERMISSION",
40
+ "ACCESS_FINE_LOCATION is required. Request it from JavaScript before starting.",
41
+ )
42
+ return
43
+ }
44
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !hasBackgroundLocation()) {
45
+ promise.reject(
46
+ "E_PERMISSION",
47
+ "ACCESS_BACKGROUND_LOCATION is required for tracking when the app is killed (Android 10+).",
48
+ )
49
+ return
50
+ }
51
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasPostNotifications()) {
52
+ promise.reject(
53
+ "E_PERMISSION",
54
+ "POST_NOTIFICATIONS is required to show the foreground service notification (Android 13+).",
55
+ )
56
+ return
57
+ }
58
+
59
+ val json = JSONObject(configJson)
60
+ val driverId = json.getString("driverId")
61
+ val intervalMs = json.getLong("intervalMs")
62
+ val serverUrl = json.getString("serverUrl")
63
+ val socketUrl =
64
+ when {
65
+ !json.has("socketUrl") || json.isNull("socketUrl") -> null
66
+ else -> json.getString("socketUrl").takeIf { it.isNotBlank() }
67
+ }
68
+ val socketTransportJs =
69
+ when {
70
+ !json.has("socketTransport") || json.isNull("socketTransport") -> null
71
+ else -> json.getString("socketTransport").takeIf { it.isNotBlank() }
72
+ }
73
+ val notificationTitle = json.getString("notificationTitle")
74
+ val notificationBody = json.getString("notificationBody")
75
+ val autoStartOnBoot = json.optBoolean("autoStartOnBoot", true)
76
+ val restHeaders =
77
+ if (json.has("restHeaders")) json.getJSONObject("restHeaders").toString() else "{}"
78
+
79
+ val notificationMapPreview = json.optBoolean("notificationMapPreview", false)
80
+ val staticMapsApiKey =
81
+ if (json.has("googleStaticMapsApiKey")) {
82
+ json.optString("googleStaticMapsApiKey", "")
83
+ } else {
84
+ ""
85
+ }
86
+
87
+ val prefs = TrackingPreferences(reactContext)
88
+ prefs.driverId = driverId
89
+ prefs.intervalMs = intervalMs.coerceAtLeast(1000L)
90
+ prefs.serverUrl = serverUrl
91
+ prefs.socketUrl = socketUrl
92
+ prefs.socketTransport = resolveTransport(socketUrl, socketTransportJs)
93
+ prefs.notificationTitle = notificationTitle
94
+ prefs.notificationBody = notificationBody
95
+ prefs.autoStartOnBoot = autoStartOnBoot
96
+ prefs.restHeadersJson = restHeaders
97
+ prefs.notificationMapPreview = notificationMapPreview
98
+ prefs.staticMapsApiKey = staticMapsApiKey
99
+ if (json.has("pickup") && !json.isNull("pickup")) {
100
+ val pu = json.getJSONObject("pickup")
101
+ prefs.pickupLatitude = pu.getDouble("latitude")
102
+ prefs.pickupLongitude = pu.getDouble("longitude")
103
+ prefs.hasPickup = true
104
+ } else {
105
+ prefs.hasPickup = false
106
+ }
107
+ prefs.isTrackingActive = true
108
+
109
+ LocationForegroundService.start(reactContext.applicationContext)
110
+ TrackingEventBridge.emitStatus(true)
111
+ promise.resolve(null)
112
+ } catch (e: Exception) {
113
+ promise.reject("E_START", e.message, e)
114
+ }
115
+ }
116
+
117
+ @ReactMethod
118
+ fun stopTracking(promise: Promise) {
119
+ try {
120
+ val prefs = TrackingPreferences(reactContext)
121
+ prefs.clearTrackingFlag()
122
+ LocationForegroundService.stop(reactContext.applicationContext)
123
+ TrackingEventBridge.emitStatus(false)
124
+ promise.resolve(null)
125
+ } catch (e: Exception) {
126
+ promise.reject("E_STOP", e.message, e)
127
+ }
128
+ }
129
+
130
+ @ReactMethod
131
+ fun isTracking(promise: Promise) {
132
+ promise.resolve(TrackingPreferences(reactContext).isTrackingActive)
133
+ }
134
+
135
+ @ReactMethod
136
+ fun openBatteryOptimizationSettings(promise: Promise) {
137
+ try {
138
+ val intent =
139
+ Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
140
+ data = Uri.parse("package:${reactContext.packageName}")
141
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
142
+ }
143
+ reactContext.startActivity(intent)
144
+ promise.resolve(null)
145
+ } catch (e: Exception) {
146
+ try {
147
+ val fallback =
148
+ Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
149
+ data = Uri.fromParts("package", reactContext.packageName, null)
150
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
151
+ }
152
+ reactContext.startActivity(fallback)
153
+ promise.resolve(null)
154
+ } catch (e2: Exception) {
155
+ promise.reject("E_BATTERY", e2.message, e2)
156
+ }
157
+ }
158
+ }
159
+
160
+ private fun resolveTransport(socketUrl: String?, jsTransport: String?): String {
161
+ if (socketUrl.isNullOrBlank()) return "rest"
162
+ if (!jsTransport.isNullOrBlank()) {
163
+ return when (jsTransport.lowercase()) {
164
+ "socket.io" -> "socket.io"
165
+ "websocket" -> "websocket"
166
+ else -> "websocket"
167
+ }
168
+ }
169
+ return if (
170
+ socketUrl.contains("/socket.io", ignoreCase = true) ||
171
+ socketUrl.contains("socket.io", ignoreCase = true)
172
+ ) {
173
+ "socket.io"
174
+ } else {
175
+ "websocket"
176
+ }
177
+ }
178
+
179
+ private fun hasFineLocation(): Boolean {
180
+ return ContextCompat.checkSelfPermission(
181
+ reactContext,
182
+ Manifest.permission.ACCESS_FINE_LOCATION,
183
+ ) == PackageManager.PERMISSION_GRANTED
184
+ }
185
+
186
+ private fun hasBackgroundLocation(): Boolean {
187
+ return ContextCompat.checkSelfPermission(
188
+ reactContext,
189
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION,
190
+ ) == PackageManager.PERMISSION_GRANTED
191
+ }
192
+
193
+ private fun hasPostNotifications(): Boolean {
194
+ return ContextCompat.checkSelfPermission(
195
+ reactContext,
196
+ Manifest.permission.POST_NOTIFICATIONS,
197
+ ) == PackageManager.PERMISSION_GRANTED
198
+ }
199
+
200
+ companion object {
201
+ const val NAME = "RNBackgroundLiveTracking"
202
+ }
203
+ }
@@ -0,0 +1,16 @@
1
+ package com.reactnativebackgroundlivetracking
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class TrackingPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(RNBackgroundLiveTrackingModule(reactContext))
11
+ }
12
+
13
+ override fun createViewManagers(
14
+ reactContext: ReactApplicationContext,
15
+ ): List<ViewManager<*, *>> = emptyList()
16
+ }
@@ -0,0 +1,100 @@
1
+ package com.reactnativebackgroundlivetracking
2
+
3
+ import android.content.Context
4
+
5
+ internal class TrackingPreferences(context: Context) {
6
+ private val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
7
+
8
+ var isTrackingActive: Boolean
9
+ get() = p.getBoolean(KEY_ACTIVE, false)
10
+ set(value) = p.edit().putBoolean(KEY_ACTIVE, value).apply()
11
+
12
+ var autoStartOnBoot: Boolean
13
+ get() = p.getBoolean(KEY_AUTO_BOOT, true)
14
+ set(value) = p.edit().putBoolean(KEY_AUTO_BOOT, value).apply()
15
+
16
+ var driverId: String
17
+ get() = p.getString(KEY_DRIVER_ID, "") ?: ""
18
+ set(value) = p.edit().putString(KEY_DRIVER_ID, value).apply()
19
+
20
+ var intervalMs: Long
21
+ get() = p.getLong(KEY_INTERVAL_MS, 5000L)
22
+ set(value) = p.edit().putLong(KEY_INTERVAL_MS, value).apply()
23
+
24
+ var serverUrl: String
25
+ get() = p.getString(KEY_SERVER_URL, "") ?: ""
26
+ set(value) = p.edit().putString(KEY_SERVER_URL, value).apply()
27
+
28
+ var socketUrl: String?
29
+ get() = p.getString(KEY_SOCKET_URL, null)
30
+ set(value) = p.edit().putString(KEY_SOCKET_URL, value).apply()
31
+
32
+ /** rest | websocket | socket.io */
33
+ var socketTransport: String
34
+ get() = p.getString(KEY_SOCKET_TRANSPORT, "rest") ?: "rest"
35
+ set(value) = p.edit().putString(KEY_SOCKET_TRANSPORT, value).apply()
36
+
37
+ var notificationTitle: String
38
+ get() = p.getString(KEY_NOTIF_TITLE, "Driver tracking") ?: "Driver tracking"
39
+ set(value) = p.edit().putString(KEY_NOTIF_TITLE, value).apply()
40
+
41
+ var notificationBody: String
42
+ get() = p.getString(KEY_NOTIF_BODY, "") ?: ""
43
+ set(value) = p.edit().putString(KEY_NOTIF_BODY, value).apply()
44
+
45
+ var restHeadersJson: String
46
+ get() = p.getString(KEY_REST_HEADERS, "{}") ?: "{}"
47
+ set(value) = p.edit().putString(KEY_REST_HEADERS, value).apply()
48
+
49
+ /** When true, notification expanded view shows a Static Maps image (requires API key). */
50
+ var notificationMapPreview: Boolean
51
+ get() = p.getBoolean(KEY_MAP_PREVIEW, false)
52
+ set(value) = p.edit().putBoolean(KEY_MAP_PREVIEW, value).apply()
53
+
54
+ var staticMapsApiKey: String
55
+ get() = p.getString(KEY_STATIC_MAP_KEY, "") ?: ""
56
+ set(value) = p.edit().putString(KEY_STATIC_MAP_KEY, value).apply()
57
+
58
+ var hasPickup: Boolean
59
+ get() = p.getBoolean(KEY_HAS_PICKUP, false)
60
+ set(value) = p.edit().putBoolean(KEY_HAS_PICKUP, value).apply()
61
+
62
+ var pickupLatitude: Double
63
+ get() =
64
+ java.lang.Double.longBitsToDouble(
65
+ p.getLong(KEY_PICKUP_LAT_BITS, 0L),
66
+ )
67
+ set(value) =
68
+ p.edit().putLong(KEY_PICKUP_LAT_BITS, java.lang.Double.doubleToRawLongBits(value)).apply()
69
+
70
+ var pickupLongitude: Double
71
+ get() =
72
+ java.lang.Double.longBitsToDouble(
73
+ p.getLong(KEY_PICKUP_LNG_BITS, 0L),
74
+ )
75
+ set(value) =
76
+ p.edit().putLong(KEY_PICKUP_LNG_BITS, java.lang.Double.doubleToRawLongBits(value)).apply()
77
+
78
+ fun clearTrackingFlag() {
79
+ p.edit().putBoolean(KEY_ACTIVE, false).apply()
80
+ }
81
+
82
+ companion object {
83
+ private const val PREFS = "rn_blt_prefs"
84
+ private const val KEY_ACTIVE = "tracking_active"
85
+ private const val KEY_AUTO_BOOT = "auto_boot"
86
+ private const val KEY_DRIVER_ID = "driver_id"
87
+ private const val KEY_INTERVAL_MS = "interval_ms"
88
+ private const val KEY_SERVER_URL = "server_url"
89
+ private const val KEY_SOCKET_URL = "socket_url"
90
+ private const val KEY_SOCKET_TRANSPORT = "socket_transport"
91
+ private const val KEY_NOTIF_TITLE = "notif_title"
92
+ private const val KEY_NOTIF_BODY = "notif_body"
93
+ private const val KEY_REST_HEADERS = "rest_headers_json"
94
+ private const val KEY_MAP_PREVIEW = "map_preview"
95
+ private const val KEY_STATIC_MAP_KEY = "static_maps_api_key"
96
+ private const val KEY_HAS_PICKUP = "has_pickup"
97
+ private const val KEY_PICKUP_LAT_BITS = "pickup_lat_bits"
98
+ private const val KEY_PICKUP_LNG_BITS = "pickup_lng_bits"
99
+ }
100
+ }
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <string name="rn_blt_channel_id">rn_background_live_tracking</string>
4
+ <string name="rn_blt_channel_name">Live location tracking</string>
5
+ <string name="rn_blt_channel_id_map">rn_background_live_tracking_map</string>
6
+ <string name="rn_blt_channel_name_map">Live location (map preview)</string>
7
+ </resources>