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,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
|
+
}
|