react-native-bg-geolocation 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/BgGeolocation.podspec +39 -0
  2. package/LICENSE +20 -0
  3. package/README.md +366 -0
  4. package/android/build.gradle +69 -0
  5. package/android/src/main/AndroidManifest.xml +53 -0
  6. package/android/src/main/java/com/bggeolocation/BgGeolocationActivityRecognitionReceiver.kt +116 -0
  7. package/android/src/main/java/com/bggeolocation/BgGeolocationBootReceiver.kt +44 -0
  8. package/android/src/main/java/com/bggeolocation/BgGeolocationForegroundService.kt +373 -0
  9. package/android/src/main/java/com/bggeolocation/BgGeolocationGeofenceReceiver.kt +55 -0
  10. package/android/src/main/java/com/bggeolocation/BgGeolocationHeadlessTask.kt +138 -0
  11. package/android/src/main/java/com/bggeolocation/BgGeolocationModule.kt +1030 -0
  12. package/android/src/main/java/com/bggeolocation/BgGeolocationMotionStateMachine.kt +159 -0
  13. package/android/src/main/java/com/bggeolocation/BgGeolocationPackage.kt +31 -0
  14. package/android/src/main/res/drawable/bg_geo_notification.xml +9 -0
  15. package/ios/BgGeolocation.h +14 -0
  16. package/ios/BgGeolocation.mm +709 -0
  17. package/ios/engine/AtomicBoolean.swift +48 -0
  18. package/ios/engine/BGActivityChangeEvent.swift +20 -0
  19. package/ios/engine/BGActivityConfig.swift +71 -0
  20. package/ios/engine/BGAppConfig.swift +92 -0
  21. package/ios/engine/BGAppState.swift +147 -0
  22. package/ios/engine/BGAuthorization.swift +85 -0
  23. package/ios/engine/BGAuthorizationAlertPresenter.swift +39 -0
  24. package/ios/engine/BGAuthorizationConfig.swift +50 -0
  25. package/ios/engine/BGAuthorizationEvent.swift +40 -0
  26. package/ios/engine/BGBackgroundTaskManager.swift +143 -0
  27. package/ios/engine/BGCLRouter.swift +101 -0
  28. package/ios/engine/BGCallback.swift +19 -0
  29. package/ios/engine/BGConfig.swift +440 -0
  30. package/ios/engine/BGConfigModuleBase.swift +180 -0
  31. package/ios/engine/BGConfigOLD.swift +582 -0
  32. package/ios/engine/BGConnectivityChangeEvent.swift +15 -0
  33. package/ios/engine/BGCrashDetector.swift +122 -0
  34. package/ios/engine/BGCurrentPositionRequest.swift +87 -0
  35. package/ios/engine/BGDataStore.swift +75 -0
  36. package/ios/engine/BGDatabase.swift +677 -0
  37. package/ios/engine/BGDatabasePool.swift +220 -0
  38. package/ios/engine/BGDatabaseQueue.swift +215 -0
  39. package/ios/engine/BGDateUtils.swift +26 -0
  40. package/ios/engine/BGDeviceInfo.swift +54 -0
  41. package/ios/engine/BGDeviceManager.swift +65 -0
  42. package/ios/engine/BGEnabledChangeEvent.swift +11 -0
  43. package/ios/engine/BGEnv.swift +17 -0
  44. package/ios/engine/BGEventBus.swift +83 -0
  45. package/ios/engine/BGEventManager.swift +169 -0
  46. package/ios/engine/BGEventNames.swift +51 -0
  47. package/ios/engine/BGGeofence.swift +233 -0
  48. package/ios/engine/BGGeofenceDAO.swift +152 -0
  49. package/ios/engine/BGGeofenceEvent.swift +42 -0
  50. package/ios/engine/BGGeofenceLocationRequest.swift +94 -0
  51. package/ios/engine/BGGeofenceManager.swift +315 -0
  52. package/ios/engine/BGGeofenceTransition.swift +97 -0
  53. package/ios/engine/BGGeofencesChangeEvent.swift +26 -0
  54. package/ios/engine/BGGeolocationConfig.swift +136 -0
  55. package/ios/engine/BGHeartbeatEvent.swift +31 -0
  56. package/ios/engine/BGHeartbeatService.swift +51 -0
  57. package/ios/engine/BGHttpConfig.swift +105 -0
  58. package/ios/engine/BGHttpErrorCodes.swift +63 -0
  59. package/ios/engine/BGHttpEvent.swift +34 -0
  60. package/ios/engine/BGHttpRequest.swift +126 -0
  61. package/ios/engine/BGHttpResponse.swift +93 -0
  62. package/ios/engine/BGHttpService.swift +428 -0
  63. package/ios/engine/BGKalmanFilter.swift +105 -0
  64. package/ios/engine/BGLMActionNames.swift +55 -0
  65. package/ios/engine/BGLicenseManager.swift +26 -0
  66. package/ios/engine/BGLiveActivityManager.swift +327 -0
  67. package/ios/engine/BGLocation.swift +311 -0
  68. package/ios/engine/BGLocationAuthorization.swift +427 -0
  69. package/ios/engine/BGLocationDAO.swift +252 -0
  70. package/ios/engine/BGLocationErrors.swift +28 -0
  71. package/ios/engine/BGLocationEvent.swift +43 -0
  72. package/ios/engine/BGLocationFilter.swift +82 -0
  73. package/ios/engine/BGLocationFilterConfig.swift +57 -0
  74. package/ios/engine/BGLocationHelper.swift +54 -0
  75. package/ios/engine/BGLocationManager.swift +662 -0
  76. package/ios/engine/BGLocationMetricsEngine.swift +116 -0
  77. package/ios/engine/BGLocationRequestService.swift +459 -0
  78. package/ios/engine/BGLocationSatisfier.swift +14 -0
  79. package/ios/engine/BGLocationStreamEvent.swift +27 -0
  80. package/ios/engine/BGLog.swift +337 -0
  81. package/ios/engine/BGLogLevel.swift +26 -0
  82. package/ios/engine/BGLoggerConfig.swift +60 -0
  83. package/ios/engine/BGMotionActivity.swift +31 -0
  84. package/ios/engine/BGMotionActivityClassifier.swift +108 -0
  85. package/ios/engine/BGMotionActivityManagerAdapter.swift +40 -0
  86. package/ios/engine/BGMotionActivitySource.swift +46 -0
  87. package/ios/engine/BGMotionDetector.swift +377 -0
  88. package/ios/engine/BGMotionPermissionManager.swift +50 -0
  89. package/ios/engine/BGNativeLogger.swift +48 -0
  90. package/ios/engine/BGNotificaitons.swift +37 -0
  91. package/ios/engine/BGOdometer.swift +66 -0
  92. package/ios/engine/BGPersistenceConfig.swift +29 -0
  93. package/ios/engine/BGPolygonStreamRequest.swift +48 -0
  94. package/ios/engine/BGPowerSaveChangeEvent.swift +12 -0
  95. package/ios/engine/BGPropertySpec.swift +29 -0
  96. package/ios/engine/BGProviderChangeEvent.swift +31 -0
  97. package/ios/engine/BGQueue.swift +50 -0
  98. package/ios/engine/BGRPC.swift +194 -0
  99. package/ios/engine/BGReachability.swift +58 -0
  100. package/ios/engine/BGResultSet.swift +157 -0
  101. package/ios/engine/BGSchedule.swift +228 -0
  102. package/ios/engine/BGScheduleEvent.swift +13 -0
  103. package/ios/engine/BGScheduler.swift +116 -0
  104. package/ios/engine/BGSingleLocationRequest.swift +49 -0
  105. package/ios/engine/BGStreamLocationRequest.swift +42 -0
  106. package/ios/engine/BGTemplate.swift +54 -0
  107. package/ios/engine/BGTimerService.swift +46 -0
  108. package/ios/engine/BGTrackingAudioManager.swift +286 -0
  109. package/ios/engine/BGTrackingService.swift +879 -0
  110. package/ios/engine/BGWatchPositionRequest.swift +63 -0
  111. package/ios/engine/DatabaseQueue.swift +47 -0
  112. package/ios/engine/LogQuery.swift +10 -0
  113. package/ios/engine/SQLQuery.swift +65 -0
  114. package/ios/engine/TransistorAuthorizationToken.swift +182 -0
  115. package/ios/liveactivity/BGLiveTrackingAttributes.swift +52 -0
  116. package/ios/locationpush/BGLocationPushDeliverer.swift +260 -0
  117. package/ios/locationpush/BGLocationPushService.swift +161 -0
  118. package/ios/locationpush/BGLocationPushShared.swift +98 -0
  119. package/ios/locationpush/BGLocationPushSocketClient.swift +198 -0
  120. package/lib/module/NativeBgGeolocation.js +5 -0
  121. package/lib/module/NativeBgGeolocation.js.map +1 -0
  122. package/lib/module/events.js +20 -0
  123. package/lib/module/events.js.map +1 -0
  124. package/lib/module/index.js +706 -0
  125. package/lib/module/index.js.map +1 -0
  126. package/lib/module/package.json +1 -0
  127. package/lib/module/types.js +2 -0
  128. package/lib/module/types.js.map +1 -0
  129. package/lib/typescript/package.json +1 -0
  130. package/lib/typescript/src/NativeBgGeolocation.d.ts +57 -0
  131. package/lib/typescript/src/NativeBgGeolocation.d.ts.map +1 -0
  132. package/lib/typescript/src/events.d.ts +18 -0
  133. package/lib/typescript/src/events.d.ts.map +1 -0
  134. package/lib/typescript/src/index.d.ts +238 -0
  135. package/lib/typescript/src/index.d.ts.map +1 -0
  136. package/lib/typescript/src/types.d.ts +229 -0
  137. package/lib/typescript/src/types.d.ts.map +1 -0
  138. package/package.json +141 -0
  139. package/src/NativeBgGeolocation.ts +236 -0
  140. package/src/events.ts +17 -0
  141. package/src/index.tsx +935 -0
  142. package/src/types.ts +254 -0
@@ -0,0 +1,44 @@
1
+ package com.bggeolocation
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.util.Log
7
+
8
+ /**
9
+ * Restarts the foreground location service after a device reboot if tracking was
10
+ * active before. The service re-requests FusedLocation updates on start, so
11
+ * tracking resumes automatically.
12
+ *
13
+ * Registered in AndroidManifest for BOOT_COMPLETED and QUICKBOOT_POWERON.
14
+ */
15
+ class BgGeolocationBootReceiver : BroadcastReceiver() {
16
+
17
+ companion object {
18
+ private const val TAG = "BgGeoBootReceiver"
19
+ private const val PREF = "bg_geolocation_prefs"
20
+ private const val KEY_ENABLED = "tracking_enabled"
21
+
22
+ fun setTrackingEnabled(context: Context, enabled: Boolean) {
23
+ context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
24
+ .edit().putBoolean(KEY_ENABLED, enabled).apply()
25
+ }
26
+
27
+ fun isTrackingEnabled(context: Context): Boolean {
28
+ return context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
29
+ .getBoolean(KEY_ENABLED, false)
30
+ }
31
+ }
32
+
33
+ override fun onReceive(context: Context, intent: Intent) {
34
+ val action = intent.action ?: return
35
+ if (action != Intent.ACTION_BOOT_COMPLETED &&
36
+ action != "android.intent.action.QUICKBOOT_POWERON") return
37
+
38
+ if (!isTrackingEnabled(context)) return
39
+
40
+ Log.d(TAG, "Boot detected — restarting location foreground service")
41
+ // The service re-requests FusedLocation updates on start.
42
+ BgGeolocationForegroundService.start(context)
43
+ }
44
+ }
@@ -0,0 +1,373 @@
1
+ package com.bggeolocation
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.app.AlarmManager
6
+ import android.app.Notification
7
+ import android.app.NotificationChannel
8
+ import android.app.NotificationManager
9
+ import android.app.PendingIntent
10
+ import android.app.Service
11
+ import android.content.Context
12
+ import android.content.Intent
13
+ import android.content.SharedPreferences
14
+ import android.content.pm.PackageManager
15
+ import android.location.Location
16
+ import android.os.Build
17
+ import android.os.Handler
18
+ import android.os.IBinder
19
+ import android.os.Looper
20
+ import android.util.Log
21
+ import androidx.core.app.NotificationCompat
22
+ import androidx.core.content.ContextCompat
23
+ import com.facebook.react.bridge.Arguments
24
+ import com.facebook.react.bridge.WritableMap
25
+ import com.facebook.react.modules.core.DeviceEventManagerModule
26
+ import com.google.android.gms.location.*
27
+ import com.google.android.gms.tasks.CancellationTokenSource
28
+ import org.json.JSONArray
29
+ import org.json.JSONObject
30
+ import java.util.UUID
31
+
32
+ /**
33
+ * BgGeolocationForegroundService
34
+ *
35
+ * The SINGLE active location source for all states.
36
+ *
37
+ * Because it runs as a foreground service with foregroundServiceType="location"
38
+ * AND actively requests FusedLocation updates, Android grants it continuous
39
+ * high-accuracy location in the background and after the app is killed (this is
40
+ * what makes the iOS-style blue location indicator appear). START_STICKY +
41
+ * onTaskRemoved(AlarmManager) make it survive swipe-kill.
42
+ *
43
+ * On each fresh location:
44
+ * 1. Run the motion state machine (moving/stationary).
45
+ * 2. Stamp the latest activity.
46
+ * 3. Persist to disk (getLocations works after relaunch).
47
+ * 4. Deliver:
48
+ * - React alive → emit "location" to JS (foreground/background)
49
+ * - App killed → fire the HeadlessTask (JS connects socket + sends)
50
+ */
51
+ class BgGeolocationForegroundService : Service() {
52
+
53
+ companion object {
54
+ const val CHANNEL_ID = "bg_geolocation_channel"
55
+ const val NOTIF_ID = 1001
56
+ const val ACTION_START = "com.bggeolocation.START"
57
+ const val ACTION_STOP = "com.bggeolocation.STOP"
58
+ const val EXTRA_TITLE = "notif_title"
59
+ const val EXTRA_TEXT = "notif_text"
60
+ private const val TAG = "BgGeoService"
61
+
62
+ fun start(context: Context, title: String = "BG Geolocation", text: String = "Tracking location") {
63
+ val intent = Intent(context, BgGeolocationForegroundService::class.java).apply {
64
+ action = ACTION_START
65
+ putExtra(EXTRA_TITLE, title)
66
+ putExtra(EXTRA_TEXT, text)
67
+ }
68
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
69
+ context.startForegroundService(intent)
70
+ } else {
71
+ context.startService(intent)
72
+ }
73
+ }
74
+
75
+ fun stop(context: Context) {
76
+ context.startService(
77
+ Intent(context, BgGeolocationForegroundService::class.java).apply {
78
+ action = ACTION_STOP
79
+ }
80
+ )
81
+ }
82
+ }
83
+
84
+ private lateinit var fusedClient: FusedLocationProviderClient
85
+ private var locationCallback: LocationCallback? = null // continuous → keeps the OS location indicator
86
+ private var lastContinuous: Location? = null // fallback for the periodic fetch
87
+ private var periodHandler: Handler? = null // periodic FRESH-fix timer
88
+ private var periodRunnable: Runnable? = null
89
+ private var periodMs: Long = 60_000L
90
+
91
+ override fun onBind(intent: Intent?): IBinder? = null
92
+
93
+ override fun onCreate() {
94
+ super.onCreate()
95
+ createNotificationChannel()
96
+ fusedClient = LocationServices.getFusedLocationProviderClient(this)
97
+ }
98
+
99
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
100
+ when (intent?.action) {
101
+ ACTION_STOP -> {
102
+ Log.d(TAG, "Stopping foreground service")
103
+ stopLocationUpdates()
104
+ stopForeground(STOP_FOREGROUND_REMOVE)
105
+ stopSelf()
106
+ return START_NOT_STICKY
107
+ }
108
+ else -> {
109
+ val title = intent?.getStringExtra(EXTRA_TITLE) ?: "BG Geolocation"
110
+ val text = intent?.getStringExtra(EXTRA_TEXT) ?: "Tracking location"
111
+ Log.d(TAG, "Starting foreground service + location updates")
112
+ // startForeground() MUST succeed and be called promptly — otherwise
113
+ // Android 12+ throws ForegroundServiceDidNotStartInTimeException and
114
+ // fatally crashes the app. buildNotification() is guaranteed not to throw.
115
+ try {
116
+ startForeground(NOTIF_ID, buildNotification(title, text))
117
+ } catch (e: Exception) {
118
+ Log.e(TAG, "startForeground failed: ${e.message}", e)
119
+ stopSelf()
120
+ return START_NOT_STICKY
121
+ }
122
+ // Location updates are started separately — a failure here must NOT
123
+ // prevent startForeground above from having run.
124
+ try {
125
+ startLocationUpdates()
126
+ } catch (e: Exception) {
127
+ Log.e(TAG, "startLocationUpdates failed: ${e.message}", e)
128
+ }
129
+ }
130
+ }
131
+ return START_STICKY
132
+ }
133
+
134
+ override fun onTaskRemoved(rootIntent: Intent?) {
135
+ Log.d(TAG, "App killed — scheduling service restart")
136
+ val restartIntent = Intent(applicationContext, BgGeolocationForegroundService::class.java).apply {
137
+ action = ACTION_START
138
+ }
139
+ val pending = PendingIntent.getService(
140
+ this, 1, restartIntent,
141
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
142
+ )
143
+ val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
144
+ alarmManager.set(
145
+ AlarmManager.ELAPSED_REALTIME,
146
+ android.os.SystemClock.elapsedRealtime() + 1000,
147
+ pending
148
+ )
149
+ super.onTaskRemoved(rootIntent)
150
+ }
151
+
152
+ override fun onDestroy() {
153
+ stopLocationUpdates()
154
+ super.onDestroy()
155
+ }
156
+
157
+ // ─── Active location updates ────────────────────────────────────────────────
158
+ //
159
+ // TWO mechanisms run together:
160
+ //
161
+ // 1. Continuous requestLocationUpdates — keeps the OS location indicator
162
+ // (blue dot) on and caches the most recent fix. It does NOT deliver.
163
+ //
164
+ // 2. A periodic timer (period = trackingPeriodMs) that calls
165
+ // getCurrentLocation(PRIORITY_HIGH_ACCURACY) — this FORCES a brand-new
166
+ // fix every period (never a cached one) and is the single delivery point.
167
+ // getCurrentLocation bypasses the background throttling that makes
168
+ // continuous updates return stale locations after the app is killed.
169
+ //
170
+ // The timer runs on the service's main looper. Because the service is
171
+ // START_STICKY it stays alive after kill, so fresh fixes keep flowing.
172
+
173
+ @SuppressLint("MissingPermission")
174
+ private fun startLocationUpdates() {
175
+ if (!hasLocationPermission()) {
176
+ Log.w(TAG, "Cannot start location updates: permission not granted")
177
+ return
178
+ }
179
+
180
+ val prefs = getSharedPreferences("bg_geolocation_prefs", Context.MODE_PRIVATE)
181
+ periodMs = prefs.getLong("trackingPeriodMs", 60_000L).coerceAtLeast(1_000L)
182
+ val distFilter = prefs.getFloat("distanceFilter", 0f)
183
+
184
+ // 1. Continuous updates — indicator + cache only (no delivery).
185
+ val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, periodMs)
186
+ .setMinUpdateDistanceMeters(distFilter)
187
+ .setWaitForAccurateLocation(false)
188
+ .build()
189
+ locationCallback = object : LocationCallback() {
190
+ override fun onLocationResult(result: LocationResult) {
191
+ result.lastLocation?.let { lastContinuous = it }
192
+ }
193
+ }
194
+ fusedClient.requestLocationUpdates(request, locationCallback!!, Looper.getMainLooper())
195
+
196
+ // 2. Periodic FRESH-fix timer — the delivery point.
197
+ periodHandler = Handler(Looper.getMainLooper())
198
+ periodRunnable = object : Runnable {
199
+ override fun run() {
200
+ fetchFreshLocation(prefs)
201
+ periodHandler?.postDelayed(this, periodMs)
202
+ }
203
+ }
204
+ // First fetch immediately, then every period.
205
+ periodHandler?.post(periodRunnable!!)
206
+
207
+ Log.d(TAG, "Tracking started — fresh fix every ${periodMs}ms (distFilter=${distFilter}m)")
208
+ }
209
+
210
+ /** Force a brand-new high-accuracy fix and deliver it. */
211
+ @SuppressLint("MissingPermission")
212
+ private fun fetchFreshLocation(prefs: SharedPreferences) {
213
+ if (!hasLocationPermission()) return
214
+ val cts = CancellationTokenSource()
215
+ fusedClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, cts.token)
216
+ .addOnSuccessListener { fresh ->
217
+ val location = fresh ?: lastContinuous
218
+ if (location != null) {
219
+ Log.d("BgGeoTest", "[SERVICE] 🛰️ fresh fix acquired (fromCache=${fresh == null})")
220
+ handleLocation(prefs, location)
221
+ } else {
222
+ Log.w("BgGeoTest", "[SERVICE] getCurrentLocation returned null and no cache")
223
+ }
224
+ }
225
+ .addOnFailureListener { e ->
226
+ Log.w("BgGeoTest", "[SERVICE] getCurrentLocation failed: ${e.message}")
227
+ lastContinuous?.let { handleLocation(prefs, it) }
228
+ }
229
+ }
230
+
231
+ private fun stopLocationUpdates() {
232
+ locationCallback?.let { if (::fusedClient.isInitialized) fusedClient.removeLocationUpdates(it) }
233
+ locationCallback = null
234
+ periodRunnable?.let { periodHandler?.removeCallbacks(it) }
235
+ periodRunnable = null
236
+ periodHandler = null
237
+ }
238
+
239
+ /** Process one fresh location fix. */
240
+ private fun handleLocation(prefs: SharedPreferences, location: Location) {
241
+ val alive = BgGeolocationModule.getReactContext() != null
242
+ val state = if (alive) "ALIVE" else "KILLED"
243
+
244
+ // 1. Motion state machine (moving/stationary). Emits motionchange on transition.
245
+ BgGeolocationMotionStateMachine.update(applicationContext, location, prefs)
246
+
247
+ // 2. Latest activity (persisted by the activity-recognition receiver)
248
+ val activityType = BgGeolocationActivityRecognitionReceiver.readActivityType(applicationContext)
249
+ val activityConf = BgGeolocationActivityRecognitionReceiver.readActivityConfidence(applicationContext)
250
+ val isMoving = BgGeolocationActivityRecognitionReceiver.readIsMoving(applicationContext)
251
+
252
+ val uuid = UUID.randomUUID().toString()
253
+ val timestamp = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US)
254
+ .apply { timeZone = java.util.TimeZone.getTimeZone("UTC") }
255
+ .format(java.util.Date(location.time))
256
+
257
+ Log.d("BgGeoTest",
258
+ "[SERVICE/$state] 📍 lat=${location.latitude} lng=${location.longitude} " +
259
+ "acc=${location.accuracy}m speed=${location.speed} moving=$isMoving activity=$activityType"
260
+ )
261
+
262
+ val map = buildLocationMap(location, uuid, timestamp, isMoving, activityType, activityConf)
263
+
264
+ // 3. Persist to disk (single writer → getLocations works after relaunch)
265
+ persistLocationToDisk(prefs, map)
266
+
267
+ // 4. Deliver
268
+ if (alive) {
269
+ // Emit to JS — foreground & background. The module's JS listeners receive it.
270
+ try {
271
+ BgGeolocationModule.getReactContext()
272
+ ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
273
+ ?.emit("location", map)
274
+ } catch (e: Exception) {
275
+ Log.w(TAG, "emit location failed: ${e.message}")
276
+ }
277
+ } else {
278
+ // Kill state → HeadlessTask (JS connects socket + sends)
279
+ BgGeolocationHeadlessTask.onLocation(applicationContext, map)
280
+ }
281
+ }
282
+
283
+ private fun buildLocationMap(
284
+ location: Location, uuid: String, timestamp: String,
285
+ isMoving: Boolean, activityType: String, activityConf: Int
286
+ ): WritableMap = Arguments.createMap().apply {
287
+ putString("uuid", uuid)
288
+ putString("timestamp", timestamp)
289
+ putMap("coords", Arguments.createMap().apply {
290
+ putDouble("latitude", location.latitude)
291
+ putDouble("longitude", location.longitude)
292
+ putDouble("accuracy", location.accuracy.toDouble())
293
+ putDouble("altitude", location.altitude)
294
+ putDouble("altitudeAccuracy",
295
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
296
+ location.verticalAccuracyMeters.toDouble() else -1.0)
297
+ putDouble("heading", location.bearing.toDouble())
298
+ putDouble("speed", location.speed.toDouble())
299
+ })
300
+ putBoolean("is_moving", isMoving)
301
+ putDouble("odometer", 0.0)
302
+ putMap("activity", Arguments.createMap().apply {
303
+ putString("type", activityType)
304
+ putInt("confidence", activityConf)
305
+ })
306
+ putMap("battery", Arguments.createMap().apply {
307
+ putDouble("level", -1.0)
308
+ putBoolean("is_charging", false)
309
+ })
310
+ }
311
+
312
+ private fun persistLocationToDisk(prefs: SharedPreferences, map: WritableMap) {
313
+ try {
314
+ val arr = JSONArray(prefs.getString("persistedLocations", "[]") ?: "[]")
315
+ arr.put(JSONObject(map.toHashMap()))
316
+ while (arr.length() > 500) arr.remove(0)
317
+ prefs.edit().putString("persistedLocations", arr.toString()).apply()
318
+ } catch (e: Exception) {
319
+ Log.w(TAG, "persistLocationToDisk failed: ${e.message}")
320
+ }
321
+ }
322
+
323
+ private fun hasLocationPermission(): Boolean {
324
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
325
+ PackageManager.PERMISSION_GRANTED ||
326
+ ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) ==
327
+ PackageManager.PERMISSION_GRANTED
328
+ }
329
+
330
+ // ─── Notification ─────────────────────────────────────────────────────────
331
+
332
+ private fun buildNotification(title: String, text: String): Notification {
333
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
334
+ val pendingIntent = if (launchIntent != null) {
335
+ PendingIntent.getActivity(this, 0, launchIntent,
336
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
337
+ } else null
338
+
339
+ return NotificationCompat.Builder(this, CHANNEL_ID)
340
+ .setContentTitle(title)
341
+ .setContentText(text)
342
+ .setSmallIcon(resolveSmallIcon())
343
+ .setOngoing(true)
344
+ .setPriority(NotificationCompat.PRIORITY_LOW)
345
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
346
+ .setContentIntent(pendingIntent)
347
+ .build()
348
+ }
349
+
350
+ /**
351
+ * Pick a small-icon that is guaranteed to be a valid, renderable resource.
352
+ * A vector drawable can fail as a notification small-icon on some OEM ROMs and
353
+ * cause startForeground() to throw "Bad notification" → fatal FGS crash.
354
+ * The app's own launcher icon is always present and safe.
355
+ */
356
+ private fun resolveSmallIcon(): Int {
357
+ val appIcon = try { applicationInfo.icon } catch (e: Exception) { 0 }
358
+ if (appIcon != 0) return appIcon
359
+ return android.R.drawable.ic_menu_mylocation
360
+ }
361
+
362
+ private fun createNotificationChannel() {
363
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
364
+ val channel = NotificationChannel(
365
+ CHANNEL_ID, "Background Location", NotificationManager.IMPORTANCE_LOW
366
+ ).apply {
367
+ description = "Keeps location tracking active in the background"
368
+ setShowBadge(false)
369
+ }
370
+ getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
371
+ }
372
+ }
373
+ }
@@ -0,0 +1,55 @@
1
+ package com.bggeolocation
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.util.Log
7
+ import com.google.android.gms.location.Geofence
8
+ import com.google.android.gms.location.GeofencingEvent
9
+
10
+ /**
11
+ * Receives geofence transition broadcasts from the GeofencingClient and forwards
12
+ * ENTER / EXIT / DWELL events to a registered callback in BgGeolocationModule.
13
+ *
14
+ * This fires even when the app is in the background or killed, because the
15
+ * PendingIntent is owned by the OS.
16
+ *
17
+ * Declared in AndroidManifest.xml as a non-exported receiver.
18
+ */
19
+ class BgGeolocationGeofenceReceiver : BroadcastReceiver() {
20
+
21
+ companion object {
22
+ private const val TAG = "BgGeoGeofence"
23
+
24
+ @Volatile
25
+ private var callback: ((identifier: String, action: String) -> Unit)? = null
26
+
27
+ fun setCallback(cb: (identifier: String, action: String) -> Unit) {
28
+ callback = cb
29
+ }
30
+
31
+ fun clearCallback() {
32
+ callback = null
33
+ }
34
+ }
35
+
36
+ override fun onReceive(context: Context, intent: Intent) {
37
+ val event = GeofencingEvent.fromIntent(intent) ?: return
38
+ if (event.hasError()) {
39
+ Log.w(TAG, "Geofence error code: ${event.errorCode}")
40
+ return
41
+ }
42
+
43
+ val action = when (event.geofenceTransition) {
44
+ Geofence.GEOFENCE_TRANSITION_ENTER -> "ENTER"
45
+ Geofence.GEOFENCE_TRANSITION_EXIT -> "EXIT"
46
+ Geofence.GEOFENCE_TRANSITION_DWELL -> "DWELL"
47
+ else -> return
48
+ }
49
+
50
+ event.triggeringGeofences?.forEach { geofence ->
51
+ Log.d(TAG, "Geofence $action: ${geofence.requestId}")
52
+ callback?.invoke(geofence.requestId, action)
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,138 @@
1
+ package com.bggeolocation
2
+
3
+ import android.content.Context
4
+ import android.content.Intent
5
+ import android.os.Bundle
6
+ import android.util.Log
7
+ import com.facebook.react.HeadlessJsTaskService
8
+ import com.facebook.react.bridge.Arguments
9
+ import com.facebook.react.bridge.WritableMap
10
+ import com.facebook.react.jstasks.HeadlessJsTaskConfig
11
+ import java.util.concurrent.ConcurrentLinkedQueue
12
+
13
+ /**
14
+ * BgGeolocationHeadlessTask
15
+ *
16
+ * Allows JavaScript to run in a headless context when the app is killed but
17
+ * the ForegroundService is still alive and delivering location updates.
18
+ *
19
+ * On the JS side the app must register:
20
+ * AppRegistry.registerHeadlessTask('BackgroundGeolocation', () => require('./headlessTask'));
21
+ *
22
+ * The headless task function receives { name: 'location', params: <locationObject> }.
23
+ *
24
+ * Flow:
25
+ * 1. BgGeolocationModule (or ForegroundService) calls BgGeolocationHeadlessTask.onLocation()
26
+ * when a new location arrives.
27
+ * 2. onLocation() enqueues the event bundle and starts this service via an Intent.
28
+ * 3. Android calls onStartCommand → getTaskConfig → HeadlessJsTaskService manages the JS runtime.
29
+ * 4. JS task runs, finishes, and the runtime is cleaned up automatically.
30
+ */
31
+ class BgGeolocationHeadlessTask : HeadlessJsTaskService() {
32
+
33
+ companion object {
34
+ private const val TAG = "BgGeoHeadlessTask"
35
+ private const val TASK_NAME = "BackgroundGeolocation"
36
+ private const val TASK_TIMEOUT_MS = 30_000L
37
+
38
+ /** Thread-safe queue of pending location events to be dispatched as headless tasks. */
39
+ private val pendingEvents = ConcurrentLinkedQueue<Bundle>()
40
+
41
+ /**
42
+ * Called from BgGeolocationModule / ForegroundService whenever a location update arrives.
43
+ * If the React context is alive the normal event emitter path is used in parallel;
44
+ * this path ensures JS executes even when the React bridge is not yet available
45
+ * (i.e. the app is in the killed/terminated state).
46
+ */
47
+ fun onLocation(context: Context, locationMap: WritableMap) {
48
+ onEvent(context, "location", locationMap)
49
+ }
50
+
51
+ /**
52
+ * Generic dispatcher for any event type (location, geofence, motionchange, …).
53
+ * Enqueues { name, params } and starts the service so getTaskConfig fires.
54
+ */
55
+ fun onEvent(context: Context, eventName: String, params: WritableMap) {
56
+ try {
57
+ val bundle = writableMapToBundle(params)
58
+ val taskBundle = Bundle().apply {
59
+ putString("name", eventName)
60
+ putBundle("params", bundle)
61
+ }
62
+ pendingEvents.offer(taskBundle)
63
+
64
+ val intent = Intent(context, BgGeolocationHeadlessTask::class.java)
65
+ context.startService(intent)
66
+ HeadlessJsTaskService.acquireWakeLockNow(context)
67
+ Log.d(TAG, "onEvent($eventName): queued headless task (queue size=${pendingEvents.size})")
68
+ } catch (e: Exception) {
69
+ Log.w(TAG, "onEvent($eventName): failed to queue headless task: ${e.message}")
70
+ }
71
+ }
72
+
73
+ // ─── Serialisation helpers ─────────────────────────────────────────────
74
+
75
+ private fun writableMapToBundle(map: WritableMap): Bundle {
76
+ val bundle = Bundle()
77
+ // Iterate using the underlying HashMap representation
78
+ val hashMap = map.toHashMap()
79
+ for ((key, value) in hashMap) {
80
+ when (value) {
81
+ is Boolean -> bundle.putBoolean(key, value)
82
+ is Int -> bundle.putInt(key, value)
83
+ is Long -> bundle.putLong(key, value)
84
+ is Double -> bundle.putDouble(key, value)
85
+ is Float -> bundle.putFloat(key, value)
86
+ is String -> bundle.putString(key, value)
87
+ is Map<*, *> -> {
88
+ @Suppress("UNCHECKED_CAST")
89
+ val nestedMap = Arguments.makeNativeMap(value as Map<String, Any>)
90
+ bundle.putBundle(key, writableMapToBundle(nestedMap))
91
+ }
92
+ else -> bundle.putString(key, value?.toString())
93
+ }
94
+ }
95
+ return bundle
96
+ }
97
+ }
98
+
99
+ // ─── HeadlessJsTaskService ────────────────────────────────────────────────
100
+
101
+ /**
102
+ * Called by the Android framework after startService(). Dequeue the next
103
+ * pending event and return a task config for the React headless JS runtime.
104
+ * If the queue is empty there is nothing to run — return null to stop the service.
105
+ */
106
+ override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
107
+ val eventBundle = pendingEvents.poll()
108
+ if (eventBundle == null) {
109
+ Log.d(TAG, "getTaskConfig: no pending events — stopping service")
110
+ return null
111
+ }
112
+
113
+ val taskData = Arguments.fromBundle(eventBundle)
114
+ Log.d(TAG, "getTaskConfig: dispatching headless task '${eventBundle.getString("name")}'")
115
+
116
+ return HeadlessJsTaskConfig(
117
+ TASK_NAME,
118
+ taskData,
119
+ TASK_TIMEOUT_MS,
120
+ true // allowedInForeground — safe to run even if app is in the foreground
121
+ )
122
+ }
123
+
124
+ override fun onHeadlessJsTaskStart(taskId: Int) {
125
+ super.onHeadlessJsTaskStart(taskId)
126
+ Log.d(TAG, "Headless JS task started (id=$taskId)")
127
+ }
128
+
129
+ override fun onHeadlessJsTaskFinish(taskId: Int) {
130
+ super.onHeadlessJsTaskFinish(taskId)
131
+ Log.d(TAG, "Headless JS task finished (id=$taskId)")
132
+ // If more events are waiting, re-start so getTaskConfig is called again
133
+ if (pendingEvents.isNotEmpty()) {
134
+ val intent = Intent(this, BgGeolocationHeadlessTask::class.java)
135
+ startService(intent)
136
+ }
137
+ }
138
+ }