@tsachit/react-native-geo-service 1.0.0 → 1.0.2
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/README.md +270 -92
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/geoservice/GeoServiceModule.kt +64 -0
- package/android/src/main/java/com/geoservice/LocationService.kt +111 -9
- package/android/src/main/java/com/geoservice/WatchdogWorker.kt +87 -0
- package/debug-panel.d.ts +1 -0
- package/debug-panel.js +1 -0
- package/ios/RNGeoService.m +223 -30
- package/lib/GeoDebugOverlay.d.ts +9 -0
- package/lib/GeoDebugOverlay.js +86 -0
- package/lib/GeoDebugPanel.d.ts +7 -0
- package/lib/GeoDebugPanel.js +319 -0
- package/lib/autoDebug.d.ts +2 -0
- package/lib/autoDebug.js +22 -0
- package/lib/index.d.ts +19 -1
- package/lib/index.js +60 -3
- package/lib/setup.d.ts +1 -0
- package/lib/setup.js +17 -0
- package/lib/types.d.ts +20 -0
- package/package.json +11 -5
|
@@ -7,6 +7,7 @@ import android.content.Intent
|
|
|
7
7
|
import android.os.Build
|
|
8
8
|
import android.os.IBinder
|
|
9
9
|
import android.util.Log
|
|
10
|
+
import androidx.core.content.ContextCompat
|
|
10
11
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
11
12
|
import com.google.android.gms.location.*
|
|
12
13
|
import org.json.JSONObject
|
|
@@ -24,6 +25,21 @@ class LocationService : Service() {
|
|
|
24
25
|
|
|
25
26
|
var isRunning = false
|
|
26
27
|
private set
|
|
28
|
+
|
|
29
|
+
// Session tracking metrics (readable by GeoServiceModule)
|
|
30
|
+
var updateCount: Long = 0
|
|
31
|
+
private set
|
|
32
|
+
var trackingStartTimeMs: Long = 0
|
|
33
|
+
private set
|
|
34
|
+
// Accumulated GPS-on milliseconds (excludes current open window)
|
|
35
|
+
private var gpsAccumulatedMs: Long = 0
|
|
36
|
+
// When the current GPS-on window started (0 = GPS currently idle)
|
|
37
|
+
private var gpsActiveWindowStartMs: Long = 0
|
|
38
|
+
|
|
39
|
+
/** Total GPS-active ms including any currently open window */
|
|
40
|
+
val currentGpsActiveMs: Long
|
|
41
|
+
get() = gpsAccumulatedMs +
|
|
42
|
+
if (gpsActiveWindowStartMs > 0) System.currentTimeMillis() - gpsActiveWindowStartMs else 0L
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
private lateinit var fusedLocationClient: FusedLocationProviderClient
|
|
@@ -44,12 +60,30 @@ class LocationService : Service() {
|
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
config = if (intent != null) {
|
|
64
|
+
GeoServiceConfig.fromBundle(intent.getBundleExtra("config"))
|
|
65
|
+
} else {
|
|
66
|
+
// START_STICKY restart after force-kill: intent is null, restore from SharedPreferences
|
|
67
|
+
Log.d(TAG, "START_STICKY restart detected — restoring config from SharedPreferences")
|
|
68
|
+
configFromSharedPreferences()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Stop immediately if location permission was revoked while we were away
|
|
72
|
+
if (!hasLocationPermission()) {
|
|
73
|
+
Log.e(TAG, "Location permission not granted — stopping service")
|
|
74
|
+
stopSelf()
|
|
75
|
+
return START_NOT_STICKY
|
|
76
|
+
}
|
|
49
77
|
|
|
50
78
|
slowReadingCount = 0
|
|
51
79
|
isIdle = false
|
|
52
80
|
|
|
81
|
+
// Reset session metrics
|
|
82
|
+
updateCount = 0
|
|
83
|
+
trackingStartTimeMs = System.currentTimeMillis()
|
|
84
|
+
gpsAccumulatedMs = 0
|
|
85
|
+
gpsActiveWindowStartMs = System.currentTimeMillis() // GPS starts active
|
|
86
|
+
|
|
53
87
|
log("Starting — adaptiveAccuracy=${config.adaptiveAccuracy}, accuracy=${config.accuracy}")
|
|
54
88
|
startForeground(NOTIFICATION_ID, buildNotification())
|
|
55
89
|
startLocationUpdates(idleOverride = false)
|
|
@@ -58,6 +92,63 @@ class LocationService : Service() {
|
|
|
58
92
|
return START_STICKY
|
|
59
93
|
}
|
|
60
94
|
|
|
95
|
+
// Called when the user swipes the app away from the recents screen.
|
|
96
|
+
// By default the foreground service keeps running — which is correct for
|
|
97
|
+
// always-on tracking. If stopOnAppClose=true we honour the user's intent.
|
|
98
|
+
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
99
|
+
super.onTaskRemoved(rootIntent)
|
|
100
|
+
val prefs = getSharedPreferences(BootReceiver.PREFS_NAME, Context.MODE_PRIVATE)
|
|
101
|
+
val shouldStop = try {
|
|
102
|
+
val json = prefs.getString("configBundle", null)
|
|
103
|
+
json?.let { org.json.JSONObject(it).optBoolean("stopOnAppClose", false) } ?: false
|
|
104
|
+
} catch (e: Exception) { false }
|
|
105
|
+
|
|
106
|
+
if (shouldStop) {
|
|
107
|
+
log("App removed from recents — stopOnAppClose=true, stopping service")
|
|
108
|
+
prefs.edit().putBoolean(BootReceiver.KEY_IS_TRACKING, false).apply()
|
|
109
|
+
stopSelf()
|
|
110
|
+
} else {
|
|
111
|
+
log("App removed from recents — continuing background tracking")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private fun hasLocationPermission(): Boolean {
|
|
116
|
+
val fine = ContextCompat.checkSelfPermission(
|
|
117
|
+
this, android.Manifest.permission.ACCESS_FINE_LOCATION
|
|
118
|
+
) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
119
|
+
val coarse = ContextCompat.checkSelfPermission(
|
|
120
|
+
this, android.Manifest.permission.ACCESS_COARSE_LOCATION
|
|
121
|
+
) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
122
|
+
return fine || coarse
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private fun configFromSharedPreferences(): GeoServiceConfig {
|
|
126
|
+
val prefs = getSharedPreferences(BootReceiver.PREFS_NAME, Context.MODE_PRIVATE)
|
|
127
|
+
val configJson = prefs.getString("configBundle", null) ?: return GeoServiceConfig()
|
|
128
|
+
return try {
|
|
129
|
+
val json = org.json.JSONObject(configJson)
|
|
130
|
+
val bundle = android.os.Bundle().apply {
|
|
131
|
+
putFloat("minDistanceMeters", json.optDouble("minDistanceMeters", 10.0).toFloat())
|
|
132
|
+
putString("accuracy", json.optString("accuracy", "balanced"))
|
|
133
|
+
putBoolean("stopOnAppClose", json.optBoolean("stopOnAppClose", false))
|
|
134
|
+
putBoolean("restartOnBoot", json.optBoolean("restartOnBoot", false))
|
|
135
|
+
putLong("updateIntervalMs", json.optLong("updateIntervalMs", 5000L))
|
|
136
|
+
putLong("minUpdateIntervalMs", json.optLong("minUpdateIntervalMs", 2000L))
|
|
137
|
+
putString("serviceTitle", json.optString("serviceTitle", "Location Tracking"))
|
|
138
|
+
putString("serviceBody", json.optString("serviceBody", "Your location is being tracked in the background."))
|
|
139
|
+
putString("backgroundTaskName", json.optString("backgroundTaskName", "GeoServiceHeadlessTask"))
|
|
140
|
+
putBoolean("adaptiveAccuracy", json.optBoolean("adaptiveAccuracy", true))
|
|
141
|
+
putFloat("idleSpeedThreshold", json.optDouble("idleSpeedThreshold", 0.5).toFloat())
|
|
142
|
+
putInt("idleSampleCount", json.optInt("idleSampleCount", 3))
|
|
143
|
+
putBoolean("debug", json.optBoolean("debug", false))
|
|
144
|
+
}
|
|
145
|
+
GeoServiceConfig.fromBundle(bundle)
|
|
146
|
+
} catch (e: Exception) {
|
|
147
|
+
Log.e(TAG, "Failed to restore config from SharedPreferences: ${e.message}")
|
|
148
|
+
GeoServiceConfig()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
61
152
|
// ---------------------------------------------------------------------------
|
|
62
153
|
// Location updates
|
|
63
154
|
// ---------------------------------------------------------------------------
|
|
@@ -109,6 +200,7 @@ class LocationService : Service() {
|
|
|
109
200
|
// ---------------------------------------------------------------------------
|
|
110
201
|
|
|
111
202
|
private fun handleLocation(location: android.location.Location) {
|
|
203
|
+
updateCount++
|
|
112
204
|
if (config.adaptiveAccuracy) {
|
|
113
205
|
evaluateMotionState(location)
|
|
114
206
|
}
|
|
@@ -123,6 +215,11 @@ class LocationService : Service() {
|
|
|
123
215
|
if (!isIdle && slowReadingCount >= config.idleSampleCount) {
|
|
124
216
|
isIdle = true
|
|
125
217
|
slowReadingCount = 0
|
|
218
|
+
// Accumulate GPS-on time before going idle
|
|
219
|
+
if (gpsActiveWindowStartMs > 0) {
|
|
220
|
+
gpsAccumulatedMs += System.currentTimeMillis() - gpsActiveWindowStartMs
|
|
221
|
+
gpsActiveWindowStartMs = 0
|
|
222
|
+
}
|
|
126
223
|
log("Device idle — switching to LOW_POWER (GPS off)")
|
|
127
224
|
startLocationUpdates(idleOverride = true)
|
|
128
225
|
}
|
|
@@ -130,6 +227,7 @@ class LocationService : Service() {
|
|
|
130
227
|
if (isIdle) {
|
|
131
228
|
isIdle = false
|
|
132
229
|
slowReadingCount = 0
|
|
230
|
+
gpsActiveWindowStartMs = System.currentTimeMillis() // GPS back on
|
|
133
231
|
log("Movement detected — restoring ${config.accuracy} accuracy")
|
|
134
232
|
startLocationUpdates(idleOverride = false)
|
|
135
233
|
} else {
|
|
@@ -195,11 +293,12 @@ class LocationService : Service() {
|
|
|
195
293
|
putExtra("location", locationJson)
|
|
196
294
|
putExtra("taskName", config.backgroundTaskName)
|
|
197
295
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
296
|
+
// Always use startService — HeadlessJsTaskService does NOT call startForeground().
|
|
297
|
+
// Using startForegroundService() here would crash with
|
|
298
|
+
// ForegroundServiceDidNotStartInTimeException after 5 seconds on Android O+.
|
|
299
|
+
// This is safe because LocationService itself is a foreground service, which
|
|
300
|
+
// allows it to start background services regardless of app state.
|
|
301
|
+
startService(intent)
|
|
203
302
|
}
|
|
204
303
|
|
|
205
304
|
// ---------------------------------------------------------------------------
|
|
@@ -237,9 +336,12 @@ class LocationService : Service() {
|
|
|
237
336
|
PendingIntent.FLAG_UPDATE_CURRENT
|
|
238
337
|
val pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, pendingFlags)
|
|
239
338
|
|
|
339
|
+
val title = if (config.debug) "[DEBUG] ${config.serviceTitle}" else config.serviceTitle
|
|
340
|
+
val body = if (config.debug) "Tracking active — debug mode on" else config.serviceBody
|
|
341
|
+
|
|
240
342
|
return builder
|
|
241
|
-
.setContentTitle(
|
|
242
|
-
.setContentText(
|
|
343
|
+
.setContentTitle(title)
|
|
344
|
+
.setContentText(body)
|
|
243
345
|
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
|
|
244
346
|
.setOngoing(true)
|
|
245
347
|
.setContentIntent(pendingIntent)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package com.geoservice
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import androidx.work.CoroutineWorker
|
|
8
|
+
import androidx.work.WorkerParameters
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Periodic watchdog that detects when the LocationService was killed by the OS
|
|
12
|
+
* (common on Xiaomi, Samsung, Huawei with aggressive battery optimization) and
|
|
13
|
+
* restarts it using the config persisted in SharedPreferences.
|
|
14
|
+
*
|
|
15
|
+
* Scheduled every 15 minutes (WorkManager minimum). If the service is already
|
|
16
|
+
* running nothing happens. If tracking was expected but the service is dead,
|
|
17
|
+
* it is restarted with the user's original config.
|
|
18
|
+
*/
|
|
19
|
+
class WatchdogWorker(
|
|
20
|
+
private val appContext: Context,
|
|
21
|
+
workerParams: WorkerParameters
|
|
22
|
+
) : CoroutineWorker(appContext, workerParams) {
|
|
23
|
+
|
|
24
|
+
companion object {
|
|
25
|
+
const val TAG = "GeoService:Watchdog"
|
|
26
|
+
const val WORK_NAME = "GeoServiceWatchdog"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override suspend fun doWork(): Result {
|
|
30
|
+
val prefs = appContext.getSharedPreferences(
|
|
31
|
+
BootReceiver.PREFS_NAME, Context.MODE_PRIVATE
|
|
32
|
+
)
|
|
33
|
+
val isTrackingExpected = prefs.getBoolean(BootReceiver.KEY_IS_TRACKING, false)
|
|
34
|
+
|
|
35
|
+
if (!isTrackingExpected) {
|
|
36
|
+
Log.d(TAG, "Tracking not expected — nothing to do")
|
|
37
|
+
return Result.success()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (LocationService.isRunning) {
|
|
41
|
+
Log.d(TAG, "Service is running — no action needed")
|
|
42
|
+
return Result.success()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Log.w(TAG, "Service not running but tracking was expected — restarting")
|
|
46
|
+
|
|
47
|
+
val configJson = prefs.getString("configBundle", null)
|
|
48
|
+
val serviceIntent = Intent(appContext, LocationService::class.java)
|
|
49
|
+
|
|
50
|
+
if (configJson != null) {
|
|
51
|
+
try {
|
|
52
|
+
val json = org.json.JSONObject(configJson)
|
|
53
|
+
val bundle = android.os.Bundle().apply {
|
|
54
|
+
putFloat("minDistanceMeters", json.optDouble("minDistanceMeters", 10.0).toFloat())
|
|
55
|
+
putString("accuracy", json.optString("accuracy", "balanced"))
|
|
56
|
+
putBoolean("stopOnAppClose", json.optBoolean("stopOnAppClose", false))
|
|
57
|
+
putBoolean("restartOnBoot", json.optBoolean("restartOnBoot", false))
|
|
58
|
+
putLong("updateIntervalMs", json.optLong("updateIntervalMs", 5000L))
|
|
59
|
+
putLong("minUpdateIntervalMs", json.optLong("minUpdateIntervalMs", 2000L))
|
|
60
|
+
putString("serviceTitle", json.optString("serviceTitle", "Location Tracking"))
|
|
61
|
+
putString("serviceBody", json.optString("serviceBody", "Your location is being tracked in the background."))
|
|
62
|
+
putString("backgroundTaskName", json.optString("backgroundTaskName", "GeoServiceHeadlessTask"))
|
|
63
|
+
putBoolean("adaptiveAccuracy", json.optBoolean("adaptiveAccuracy", true))
|
|
64
|
+
putFloat("idleSpeedThreshold", json.optDouble("idleSpeedThreshold", 0.5).toFloat())
|
|
65
|
+
putInt("idleSampleCount", json.optInt("idleSampleCount", 3))
|
|
66
|
+
putBoolean("debug", json.optBoolean("debug", false))
|
|
67
|
+
}
|
|
68
|
+
serviceIntent.putExtra("config", bundle)
|
|
69
|
+
} catch (e: Exception) {
|
|
70
|
+
Log.e(TAG, "Failed to parse config — starting with defaults: ${e.message}")
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return try {
|
|
75
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
76
|
+
appContext.startForegroundService(serviceIntent)
|
|
77
|
+
} else {
|
|
78
|
+
appContext.startService(serviceIntent)
|
|
79
|
+
}
|
|
80
|
+
Log.d(TAG, "Service restarted successfully")
|
|
81
|
+
Result.success()
|
|
82
|
+
} catch (e: Exception) {
|
|
83
|
+
Log.e(TAG, "Failed to restart service: ${e.message}")
|
|
84
|
+
Result.retry()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/debug-panel.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/debug-panel.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require('./lib/setup');
|