react-native-geo-activity-kit 1.1.2 → 1.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/android/src/main/AndroidManifest.xml +10 -5
- package/android/src/main/java/com/rngeoactivitykit/LocationHelper.kt +115 -0
- package/android/src/main/java/com/rngeoactivitykit/MotionDetector.kt +115 -0
- package/android/src/main/java/com/rngeoactivitykit/NotificationHelper.kt +101 -0
- package/android/src/main/java/com/rngeoactivitykit/SensorModule.kt +91 -294
- package/android/src/main/java/com/rngeoactivitykit/TrackingService.kt +135 -0
- package/lib/module/index.js +8 -7
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +16 -10
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
4
4
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
5
|
-
|
|
6
|
-
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
7
|
-
|
|
8
5
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
9
6
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
|
10
|
-
|
|
7
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
11
8
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
<application>
|
|
11
|
+
<service
|
|
12
|
+
android:name=".TrackingService"
|
|
13
|
+
android:enabled="true"
|
|
14
|
+
android:exported="false"
|
|
15
|
+
android:foregroundServiceType="location" />
|
|
16
|
+
</application>
|
|
17
|
+
|
|
18
|
+
</manifest>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package com.rngeoactivitykit
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.annotation.SuppressLint
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import androidx.core.content.ContextCompat
|
|
9
|
+
import com.facebook.react.bridge.Arguments
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
12
|
+
import com.google.android.gms.location.*
|
|
13
|
+
import java.text.SimpleDateFormat
|
|
14
|
+
import java.util.Date
|
|
15
|
+
import java.util.TimeZone
|
|
16
|
+
|
|
17
|
+
class LocationHelper(private val context: ReactApplicationContext) {
|
|
18
|
+
|
|
19
|
+
private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
|
|
20
|
+
private var locationCallback: LocationCallback
|
|
21
|
+
private var locationRequest: LocationRequest
|
|
22
|
+
|
|
23
|
+
var isLocationClientRunning: Boolean = false
|
|
24
|
+
private set
|
|
25
|
+
|
|
26
|
+
private val isoFormatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").apply {
|
|
27
|
+
timeZone = TimeZone.getTimeZone("UTC")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
init {
|
|
31
|
+
locationRequest = LocationRequest.create().apply {
|
|
32
|
+
interval = 30000
|
|
33
|
+
fastestInterval = 30000
|
|
34
|
+
priority = Priority.PRIORITY_BALANCED_POWER_ACCURACY
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
locationCallback = object : LocationCallback() {
|
|
38
|
+
override fun onLocationResult(locationResult: LocationResult) {
|
|
39
|
+
locationResult.lastLocation ?: return
|
|
40
|
+
val location = locationResult.lastLocation!!
|
|
41
|
+
|
|
42
|
+
val params = Arguments.createMap()
|
|
43
|
+
params.putDouble("latitude", location.latitude)
|
|
44
|
+
params.putDouble("longitude", location.longitude)
|
|
45
|
+
params.putString("timestamp", isoFormatter.format(Date(location.time)))
|
|
46
|
+
params.putDouble("accuracy", location.accuracy.toDouble())
|
|
47
|
+
|
|
48
|
+
sendEvent("onLocationLog", params)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fun updateLocationRequest(priority: Int, intervalMs: Long) {
|
|
54
|
+
if (locationRequest.priority == priority && locationRequest.interval == intervalMs) return
|
|
55
|
+
|
|
56
|
+
Log.i("LocationHelper", "Switching Mode -> Priority: $priority, Interval: $intervalMs")
|
|
57
|
+
|
|
58
|
+
locationRequest = LocationRequest.create().apply {
|
|
59
|
+
this.interval = intervalMs
|
|
60
|
+
this.fastestInterval = intervalMs
|
|
61
|
+
this.priority = priority
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Restart if running to apply changes
|
|
65
|
+
if (isLocationClientRunning) {
|
|
66
|
+
stopLocationUpdates()
|
|
67
|
+
startLocationUpdates()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@SuppressLint("MissingPermission")
|
|
72
|
+
fun startLocationUpdates() {
|
|
73
|
+
if (isLocationClientRunning) return
|
|
74
|
+
if (!hasLocationPermission()) {
|
|
75
|
+
val params = Arguments.createMap()
|
|
76
|
+
params.putString("error", "LOCATION_PERMISSION_DENIED")
|
|
77
|
+
params.putString("message", "Location permission is not granted.")
|
|
78
|
+
sendEvent("onLocationError", params)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
|
|
83
|
+
isLocationClientRunning = true
|
|
84
|
+
Log.i("LocationHelper", "Location updates started.")
|
|
85
|
+
} catch (e: Exception) {
|
|
86
|
+
val params = Arguments.createMap()
|
|
87
|
+
params.putString("error", "START_LOCATION_FAILED")
|
|
88
|
+
params.putString("message", "Error starting location: ${e.message}")
|
|
89
|
+
sendEvent("onLocationError", params)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fun stopLocationUpdates() {
|
|
94
|
+
if (!isLocationClientRunning) return
|
|
95
|
+
try {
|
|
96
|
+
fusedLocationClient.removeLocationUpdates(locationCallback)
|
|
97
|
+
isLocationClientRunning = false
|
|
98
|
+
Log.i("LocationHelper", "Location updates stopped.")
|
|
99
|
+
} catch (e: Exception) {
|
|
100
|
+
Log.e("LocationHelper", "Failed to stop location updates: " + e.message)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun hasLocationPermission(): Boolean {
|
|
105
|
+
val fine = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
|
|
106
|
+
val coarse = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
107
|
+
return fine == PackageManager.PERMISSION_GRANTED || coarse == PackageManager.PERMISSION_GRANTED
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun sendEvent(eventName: String, params: Any?) {
|
|
111
|
+
if (context.hasActiveCatalystInstance()) {
|
|
112
|
+
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(eventName, params)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package com.rngeoactivitykit
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.hardware.Sensor
|
|
5
|
+
import android.hardware.SensorEvent
|
|
6
|
+
import android.hardware.SensorEventListener
|
|
7
|
+
import android.hardware.SensorManager
|
|
8
|
+
import com.facebook.react.bridge.Arguments
|
|
9
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
10
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
11
|
+
import kotlin.math.sqrt
|
|
12
|
+
|
|
13
|
+
class MotionDetector(private val context: ReactApplicationContext, private val onStateChange: (String) -> Unit) : SensorEventListener {
|
|
14
|
+
|
|
15
|
+
private val sensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
16
|
+
private var accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
|
17
|
+
|
|
18
|
+
private val gravity = floatArrayOf(0f, 0f, 0f)
|
|
19
|
+
private val linearAcceleration = floatArrayOf(0f, 0f, 0f)
|
|
20
|
+
private val alpha: Float = 0.8f
|
|
21
|
+
|
|
22
|
+
var motionThreshold: Float = 0.8f
|
|
23
|
+
var startStabilityThreshold: Int = 20
|
|
24
|
+
var stopStabilityThreshold: Int = 3000
|
|
25
|
+
var samplingPeriodUs: Int = 100_000
|
|
26
|
+
|
|
27
|
+
private var currentState: String = "STATIONARY"
|
|
28
|
+
private var potentialState: String = "STATIONARY"
|
|
29
|
+
private var consecutiveCount = 0
|
|
30
|
+
private var isStarted = false
|
|
31
|
+
|
|
32
|
+
fun start(threshold: Double): Boolean {
|
|
33
|
+
if (accelerometer == null) return false
|
|
34
|
+
|
|
35
|
+
motionThreshold = threshold.toFloat()
|
|
36
|
+
currentState = "STATIONARY"
|
|
37
|
+
potentialState = "STATIONARY"
|
|
38
|
+
consecutiveCount = 0
|
|
39
|
+
isStarted = true
|
|
40
|
+
|
|
41
|
+
sensorManager.registerListener(this, accelerometer, samplingPeriodUs)
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun stop() {
|
|
46
|
+
isStarted = false
|
|
47
|
+
sensorManager.unregisterListener(this)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fun setUpdateInterval(ms: Int) {
|
|
51
|
+
samplingPeriodUs = ms * 1000
|
|
52
|
+
if (isStarted) {
|
|
53
|
+
stop()
|
|
54
|
+
sensorManager.registerListener(this, accelerometer, samplingPeriodUs)
|
|
55
|
+
isStarted = true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun isSensorAvailable(): Boolean = accelerometer != null
|
|
60
|
+
|
|
61
|
+
override fun onSensorChanged(event: SensorEvent?) {
|
|
62
|
+
event ?: return
|
|
63
|
+
if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
|
|
64
|
+
|
|
65
|
+
// Isolate Gravity
|
|
66
|
+
gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0]
|
|
67
|
+
gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1]
|
|
68
|
+
gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2]
|
|
69
|
+
|
|
70
|
+
// Remove Gravity
|
|
71
|
+
linearAcceleration[0] = event.values[0] - gravity[0]
|
|
72
|
+
linearAcceleration[1] = event.values[1] - gravity[1]
|
|
73
|
+
linearAcceleration[2] = event.values[2] - gravity[2]
|
|
74
|
+
|
|
75
|
+
val magnitude = sqrt(
|
|
76
|
+
(linearAcceleration[0] * linearAcceleration[0] +
|
|
77
|
+
linearAcceleration[1] * linearAcceleration[1] +
|
|
78
|
+
linearAcceleration[2] * linearAcceleration[2]).toDouble()
|
|
79
|
+
).toFloat()
|
|
80
|
+
|
|
81
|
+
val newState = if (magnitude > motionThreshold) "MOVING" else "STATIONARY"
|
|
82
|
+
|
|
83
|
+
if (newState == potentialState) {
|
|
84
|
+
consecutiveCount++
|
|
85
|
+
} else {
|
|
86
|
+
potentialState = newState
|
|
87
|
+
consecutiveCount = 1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var stabilityMet = false
|
|
91
|
+
if (potentialState == "MOVING" && consecutiveCount >= startStabilityThreshold) {
|
|
92
|
+
stabilityMet = true
|
|
93
|
+
} else if (potentialState == "STATIONARY" && consecutiveCount >= stopStabilityThreshold) {
|
|
94
|
+
stabilityMet = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (stabilityMet && potentialState != currentState) {
|
|
98
|
+
currentState = potentialState
|
|
99
|
+
|
|
100
|
+
// Notify Listener (SensorModule)
|
|
101
|
+
onStateChange(currentState)
|
|
102
|
+
|
|
103
|
+
// Emit to JS
|
|
104
|
+
val params = Arguments.createMap()
|
|
105
|
+
params.putString("state", currentState)
|
|
106
|
+
if (context.hasActiveCatalystInstance()) {
|
|
107
|
+
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
108
|
+
.emit("onMotionStateChanged", params)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
|
115
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
package com.rngeoactivitykit
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import androidx.core.app.NotificationCompat
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
|
|
12
|
+
class NotificationHelper(private val context: ReactApplicationContext) {
|
|
13
|
+
|
|
14
|
+
private val notificationManager: NotificationManager =
|
|
15
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
const val GEOFENCE_CHANNEL_ID = "geofence-channel-id"
|
|
19
|
+
const val GEOFENCE_OUT_ID = 101
|
|
20
|
+
const val GEOFENCE_IN_ID = 102
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private val appIcon: Int = context.applicationInfo.icon.let {
|
|
24
|
+
if (it != 0) it else android.R.drawable.ic_dialog_info
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
init {
|
|
28
|
+
createNotificationChannel()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private fun createNotificationChannel() {
|
|
32
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
33
|
+
val name = "Geofence Alerts"
|
|
34
|
+
val descriptionText = "Notifications for geofence and work reminders."
|
|
35
|
+
val importance = NotificationManager.IMPORTANCE_HIGH
|
|
36
|
+
val channel = NotificationChannel(GEOFENCE_CHANNEL_ID, name, importance).apply {
|
|
37
|
+
description = descriptionText
|
|
38
|
+
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
39
|
+
enableVibration(true)
|
|
40
|
+
vibrationPattern = longArrayOf(300, 500)
|
|
41
|
+
}
|
|
42
|
+
notificationManager.createNotificationChannel(channel)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun fireGeofenceAlert(type: String, userName: String) {
|
|
47
|
+
val pendingIntent = createPendingIntent(0)
|
|
48
|
+
|
|
49
|
+
if (type == "OUT") {
|
|
50
|
+
val notification = NotificationCompat.Builder(context, GEOFENCE_CHANNEL_ID)
|
|
51
|
+
.setSmallIcon(appIcon)
|
|
52
|
+
.setContentTitle("Geofence Alert 🔔")
|
|
53
|
+
.setContentText("$userName, you seem to have moved out of your designated work area.")
|
|
54
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
55
|
+
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
|
56
|
+
.setContentIntent(pendingIntent)
|
|
57
|
+
.setOngoing(true)
|
|
58
|
+
.setAutoCancel(false)
|
|
59
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
60
|
+
.build()
|
|
61
|
+
notificationManager.notify(GEOFENCE_OUT_ID, notification)
|
|
62
|
+
} else if (type == "IN") {
|
|
63
|
+
notificationManager.cancel(GEOFENCE_OUT_ID)
|
|
64
|
+
val notification = NotificationCompat.Builder(context, GEOFENCE_CHANNEL_ID)
|
|
65
|
+
.setSmallIcon(appIcon)
|
|
66
|
+
.setContentTitle("You are in again ✅")
|
|
67
|
+
.setContentText("$userName, you have moved back into your designated work area.")
|
|
68
|
+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
69
|
+
.setContentIntent(pendingIntent)
|
|
70
|
+
.setAutoCancel(true)
|
|
71
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
72
|
+
.build()
|
|
73
|
+
notificationManager.notify(GEOFENCE_IN_ID, notification)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fun fireGenericAlert(title: String, body: String, id: Int) {
|
|
78
|
+
val pendingIntent = createPendingIntent(id)
|
|
79
|
+
val notification = NotificationCompat.Builder(context, GEOFENCE_CHANNEL_ID)
|
|
80
|
+
.setSmallIcon(appIcon)
|
|
81
|
+
.setContentTitle(title)
|
|
82
|
+
.setContentText(body)
|
|
83
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
84
|
+
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
|
85
|
+
.setContentIntent(pendingIntent)
|
|
86
|
+
.setAutoCancel(true)
|
|
87
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
88
|
+
.build()
|
|
89
|
+
notificationManager.notify(id, notification)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fun cancelGenericAlert(id: Int) {
|
|
93
|
+
notificationManager.cancel(id)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun createPendingIntent(requestCode: Int): PendingIntent? {
|
|
97
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
98
|
+
val pendingIntentFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT
|
|
99
|
+
return launchIntent?.let { PendingIntent.getActivity(context, requestCode, it, pendingIntentFlag) }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -1,376 +1,173 @@
|
|
|
1
1
|
package com.rngeoactivitykit
|
|
2
2
|
|
|
3
|
-
import android.
|
|
4
|
-
import android.annotation.SuppressLint
|
|
5
|
-
import android.app.Notification
|
|
6
|
-
import android.app.NotificationChannel
|
|
7
|
-
import android.app.NotificationManager
|
|
8
|
-
import android.app.PendingIntent
|
|
9
|
-
import android.content.Context
|
|
10
|
-
import android.content.pm.PackageManager
|
|
11
|
-
import android.hardware.Sensor
|
|
12
|
-
import android.hardware.SensorEvent
|
|
13
|
-
import android.hardware.SensorEventListener
|
|
14
|
-
import android.hardware.SensorManager
|
|
3
|
+
import android.content.Intent
|
|
15
4
|
import android.os.Build
|
|
16
|
-
import android.os.Looper
|
|
17
|
-
import android.util.Log
|
|
18
|
-
import androidx.core.app.NotificationCompat
|
|
19
|
-
import androidx.core.content.ContextCompat
|
|
20
5
|
import com.facebook.react.bridge.*
|
|
21
|
-
import com.
|
|
22
|
-
import com.google.android.gms.location.*
|
|
23
|
-
import java.text.SimpleDateFormat
|
|
24
|
-
import java.util.Date
|
|
25
|
-
import java.util.TimeZone
|
|
26
|
-
import kotlin.math.sqrt
|
|
6
|
+
import com.google.android.gms.location.Priority
|
|
27
7
|
|
|
28
|
-
class SensorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext)
|
|
8
|
+
class SensorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
29
9
|
|
|
30
|
-
private val
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
private lateinit var fusedLocationClient: FusedLocationProviderClient
|
|
34
|
-
private var locationCallback: LocationCallback
|
|
35
|
-
private lateinit var locationRequest: LocationRequest
|
|
36
|
-
|
|
37
|
-
// --- CONFIGURATION: Default to 30s ---
|
|
38
|
-
@Volatile private var locationInterval: Long = 30000
|
|
39
|
-
private var isLocationClientRunning: Boolean = false
|
|
40
|
-
|
|
41
|
-
private val gravity = floatArrayOf(0f, 0f, 0f)
|
|
42
|
-
private val linearAcceleration = floatArrayOf(0f, 0f, 0f)
|
|
43
|
-
private val alpha: Float = 0.8f
|
|
10
|
+
private val notificationHelper = NotificationHelper(reactContext)
|
|
11
|
+
private val locationHelper = LocationHelper(reactContext)
|
|
44
12
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@Volatile private var potentialState: String = "STATIONARY"
|
|
49
|
-
@Volatile private var consecutiveCount = 0
|
|
50
|
-
|
|
51
|
-
@Volatile private var startStabilityThreshold: Int = 20
|
|
52
|
-
@Volatile private var stopStabilityThreshold: Int = 3000
|
|
53
|
-
@Volatile private var samplingPeriodUs: Int = 100_000
|
|
54
|
-
|
|
55
|
-
private val isoFormatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").apply {
|
|
56
|
-
timeZone = TimeZone.getTimeZone("UTC")
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private val notificationManager: NotificationManager =
|
|
60
|
-
reactContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
61
|
-
private val GEOFENCE_CHANNEL_ID = "geofence-channel-id"
|
|
62
|
-
private val GEOFENCE_OUT_ID = 101
|
|
63
|
-
private val GEOFENCE_IN_ID = 102
|
|
64
|
-
|
|
65
|
-
private val appIcon: Int = reactApplicationContext.applicationInfo.icon.let {
|
|
66
|
-
if (it != 0) it else android.R.drawable.ic_dialog_info
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
init {
|
|
70
|
-
fusedLocationClient = LocationServices.getFusedLocationProviderClient(reactContext)
|
|
71
|
-
|
|
72
|
-
locationRequest = LocationRequest.create().apply {
|
|
73
|
-
interval = locationInterval
|
|
74
|
-
fastestInterval = locationInterval
|
|
75
|
-
priority = Priority.PRIORITY_HIGH_ACCURACY
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
locationCallback = object : LocationCallback() {
|
|
79
|
-
override fun onLocationResult(locationResult: LocationResult) {
|
|
80
|
-
locationResult.lastLocation ?: return
|
|
81
|
-
|
|
82
|
-
val location = locationResult.lastLocation!!
|
|
83
|
-
val params = Arguments.createMap()
|
|
84
|
-
params.putDouble("latitude", location.latitude)
|
|
85
|
-
params.putDouble("longitude", location.longitude)
|
|
86
|
-
params.putString("timestamp", isoFormatter.format(Date(location.time)))
|
|
87
|
-
params.putDouble("accuracy", location.accuracy.toDouble())
|
|
88
|
-
|
|
89
|
-
emitEvent(reactApplicationContext, "onLocationLog", params)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
createNotificationChannel()
|
|
13
|
+
// Connect Motion Logic to Location Logic
|
|
14
|
+
private val motionDetector = MotionDetector(reactContext) { newState ->
|
|
15
|
+
onMotionStateChanged(newState)
|
|
94
16
|
}
|
|
95
17
|
|
|
96
|
-
private
|
|
97
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
98
|
-
val name = "Geofence Alerts"
|
|
99
|
-
val descriptionText = "Notifications for geofence and work reminders."
|
|
100
|
-
val importance = NotificationManager.IMPORTANCE_HIGH
|
|
101
|
-
val channel = NotificationChannel(GEOFENCE_CHANNEL_ID, name, importance).apply {
|
|
102
|
-
description = descriptionText
|
|
103
|
-
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
104
|
-
enableVibration(true)
|
|
105
|
-
vibrationPattern = longArrayOf(300, 500)
|
|
106
|
-
}
|
|
107
|
-
notificationManager.createNotificationChannel(channel)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
18
|
+
private var locationInterval: Long = 30000
|
|
110
19
|
|
|
111
20
|
override fun getName(): String = "RNSensorModule"
|
|
112
21
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (isLocationClientRunning) {
|
|
125
|
-
try {
|
|
126
|
-
fusedLocationClient.removeLocationUpdates(locationCallback)
|
|
127
|
-
fusedLocationClient.requestLocationUpdates(
|
|
128
|
-
locationRequest,
|
|
129
|
-
locationCallback,
|
|
130
|
-
Looper.getMainLooper()
|
|
131
|
-
)
|
|
132
|
-
} catch (e: Exception) {
|
|
133
|
-
Log.e("SensorModule", "Error applying new location request: ${e.message}")
|
|
134
|
-
}
|
|
22
|
+
// --- The "Smart Switch" Logic ---
|
|
23
|
+
private fun onMotionStateChanged(state: String) {
|
|
24
|
+
if (state == "MOVING") {
|
|
25
|
+
// High Power
|
|
26
|
+
locationHelper.updateLocationRequest(Priority.PRIORITY_HIGH_ACCURACY, locationInterval)
|
|
27
|
+
locationHelper.startLocationUpdates()
|
|
28
|
+
} else {
|
|
29
|
+
// Low Power (Cell/Wifi) & Slow Updates
|
|
30
|
+
locationHelper.updateLocationRequest(Priority.PRIORITY_BALANCED_POWER_ACCURACY, 180000)
|
|
31
|
+
locationHelper.startLocationUpdates()
|
|
135
32
|
}
|
|
136
33
|
}
|
|
137
34
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
35
|
+
// --- Service Methods ---
|
|
36
|
+
@ReactMethod
|
|
37
|
+
fun startForegroundService(title: String, body: String, promise: Promise) {
|
|
38
|
+
try {
|
|
39
|
+
val intent = Intent(reactApplicationContext, TrackingService::class.java)
|
|
40
|
+
intent.action = TrackingService.ACTION_START
|
|
41
|
+
intent.putExtra("title", title)
|
|
42
|
+
intent.putExtra("body", body)
|
|
141
43
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2]
|
|
145
|
-
|
|
146
|
-
linearAcceleration[0] = event.values[0] - gravity[0]
|
|
147
|
-
linearAcceleration[1] = event.values[1] - gravity[1]
|
|
148
|
-
linearAcceleration[2] = event.values[2] - gravity[2]
|
|
149
|
-
|
|
150
|
-
val magnitude = sqrt(
|
|
151
|
-
(linearAcceleration[0] * linearAcceleration[0] +
|
|
152
|
-
linearAcceleration[1] * linearAcceleration[1] +
|
|
153
|
-
linearAcceleration[2] * linearAcceleration[2]).toDouble()
|
|
154
|
-
).toFloat()
|
|
155
|
-
|
|
156
|
-
val newState = if (magnitude > motionThreshold) "MOVING" else "STATIONARY"
|
|
157
|
-
|
|
158
|
-
if (newState == potentialState) {
|
|
159
|
-
consecutiveCount++
|
|
44
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
45
|
+
reactApplicationContext.startForegroundService(intent)
|
|
160
46
|
} else {
|
|
161
|
-
|
|
162
|
-
consecutiveCount = 1
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
var stabilityMet = false
|
|
166
|
-
if (potentialState == "MOVING" && consecutiveCount >= startStabilityThreshold) {
|
|
167
|
-
stabilityMet = true
|
|
168
|
-
} else if (potentialState == "STATIONARY" && consecutiveCount >= stopStabilityThreshold) {
|
|
169
|
-
stabilityMet = true
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (stabilityMet && potentialState != currentState) {
|
|
173
|
-
currentState = potentialState
|
|
174
|
-
|
|
175
|
-
if (currentState == "MOVING") {
|
|
176
|
-
// Moving: High Accuracy GPS.
|
|
177
|
-
updateLocationRequest(Priority.PRIORITY_HIGH_ACCURACY, locationInterval)
|
|
178
|
-
startLocationUpdates()
|
|
179
|
-
} else {
|
|
180
|
-
// Stationary: Balanced Power (Cell/Wifi). 3-minute heartbeat.
|
|
181
|
-
updateLocationRequest(Priority.PRIORITY_BALANCED_POWER_ACCURACY, 180000)
|
|
182
|
-
startLocationUpdates()
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
val params = Arguments.createMap()
|
|
186
|
-
params.putString("state", currentState)
|
|
187
|
-
emitEvent(reactApplicationContext, "onMotionStateChanged", params)
|
|
47
|
+
reactApplicationContext.startService(intent)
|
|
188
48
|
}
|
|
49
|
+
promise.resolve(true)
|
|
50
|
+
} catch (e: Exception) {
|
|
51
|
+
promise.reject("START_FAILED", e.message)
|
|
189
52
|
}
|
|
190
53
|
}
|
|
191
54
|
|
|
192
|
-
|
|
55
|
+
@ReactMethod
|
|
56
|
+
fun stopForegroundService(promise: Promise) {
|
|
57
|
+
try {
|
|
58
|
+
val intent = Intent(reactApplicationContext, TrackingService::class.java)
|
|
59
|
+
intent.action = TrackingService.ACTION_STOP
|
|
60
|
+
reactApplicationContext.startService(intent)
|
|
61
|
+
promise.resolve(true)
|
|
62
|
+
} catch (e: Exception) {
|
|
63
|
+
promise.reject("STOP_FAILED", e.message)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
193
66
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
super.onCatalystInstanceDestroy()
|
|
67
|
+
@ReactMethod
|
|
68
|
+
fun updateServiceNotification(title: String, body: String, promise: Promise) {
|
|
197
69
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
70
|
+
if (TrackingService.instance != null) {
|
|
71
|
+
val intent = Intent(reactApplicationContext, TrackingService::class.java)
|
|
72
|
+
intent.action = TrackingService.ACTION_UPDATE
|
|
73
|
+
intent.putExtra("title", title)
|
|
74
|
+
intent.putExtra("body", body)
|
|
75
|
+
reactApplicationContext.startService(intent)
|
|
76
|
+
promise.resolve(true)
|
|
77
|
+
} else {
|
|
78
|
+
promise.resolve(false)
|
|
205
79
|
}
|
|
206
|
-
Log.i("SensorModule", "Cleaned up sensors and location updates.")
|
|
207
80
|
} catch (e: Exception) {
|
|
208
|
-
|
|
81
|
+
promise.reject("UPDATE_FAILED", e.message)
|
|
209
82
|
}
|
|
210
83
|
}
|
|
211
84
|
|
|
85
|
+
// --- Sensor Methods ---
|
|
212
86
|
@ReactMethod
|
|
213
87
|
fun startMotionDetector(threshold: Double, promise: Promise) {
|
|
214
|
-
|
|
215
|
-
|
|
88
|
+
val success = motionDetector.start(threshold)
|
|
89
|
+
if (!success) {
|
|
90
|
+
promise.reject("NO_SENSOR", "Accelerometer not available")
|
|
216
91
|
return
|
|
217
92
|
}
|
|
218
|
-
motionThreshold = threshold.toFloat()
|
|
219
|
-
isMotionDetectorStarted = true
|
|
220
|
-
currentState = "STATIONARY"
|
|
221
|
-
potentialState = "STATIONARY"
|
|
222
|
-
consecutiveCount = 0
|
|
223
|
-
|
|
224
|
-
sensorManager.registerListener(this, accelerometer, samplingPeriodUs)
|
|
225
93
|
|
|
226
|
-
|
|
227
|
-
|
|
94
|
+
// Start Location immediately (Balanced Mode)
|
|
95
|
+
locationHelper.updateLocationRequest(Priority.PRIORITY_BALANCED_POWER_ACCURACY, 180000)
|
|
96
|
+
locationHelper.startLocationUpdates()
|
|
228
97
|
|
|
229
98
|
promise.resolve(true)
|
|
230
99
|
}
|
|
231
100
|
|
|
232
101
|
@ReactMethod
|
|
233
102
|
fun stopMotionDetector(promise: Promise) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// Explicit stop from JS (End Shift)
|
|
237
|
-
stopLocationUpdates()
|
|
103
|
+
motionDetector.stop()
|
|
104
|
+
locationHelper.stopLocationUpdates()
|
|
238
105
|
promise.resolve(true)
|
|
239
106
|
}
|
|
240
107
|
|
|
241
108
|
@ReactMethod
|
|
242
109
|
fun setLocationUpdateInterval(interval: Double, promise: Promise) {
|
|
243
110
|
locationInterval = interval.toLong()
|
|
244
|
-
if (currentState == "MOVING" && isLocationClientRunning) {
|
|
245
|
-
updateLocationRequest(Priority.PRIORITY_HIGH_ACCURACY, locationInterval)
|
|
246
|
-
}
|
|
247
111
|
promise.resolve(true)
|
|
248
112
|
}
|
|
249
|
-
|
|
250
|
-
// ... Keep all other methods (fireGeofenceAlert, isAvailable, etc.) exactly as before ...
|
|
251
|
-
|
|
252
|
-
// BOILERPLATE BELOW (Shortened for brevity, but you keep it in your file)
|
|
253
|
-
private fun hasLocationPermission(): Boolean {
|
|
254
|
-
val finePermission = ContextCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.ACCESS_FINE_LOCATION)
|
|
255
|
-
val coarsePermission = ContextCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
256
|
-
return finePermission == PackageManager.PERMISSION_GRANTED || coarsePermission == PackageManager.PERMISSION_GRANTED
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
@SuppressLint("MissingPermission")
|
|
260
|
-
private fun startLocationUpdates() {
|
|
261
|
-
if (isLocationClientRunning) return
|
|
262
|
-
if (!hasLocationPermission()) {
|
|
263
|
-
val params = Arguments.createMap()
|
|
264
|
-
params.putString("error", "LOCATION_PERMISSION_DENIED")
|
|
265
|
-
params.putString("message", "Location permission is not granted.")
|
|
266
|
-
emitEvent(reactApplicationContext, "onLocationError", params)
|
|
267
|
-
return
|
|
268
|
-
}
|
|
269
|
-
try {
|
|
270
|
-
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
|
|
271
|
-
isLocationClientRunning = true
|
|
272
|
-
Log.i("SensorModule", "Location updates started.")
|
|
273
|
-
} catch (e: Exception) {
|
|
274
|
-
val params = Arguments.createMap()
|
|
275
|
-
params.putString("error", "START_LOCATION_FAILED")
|
|
276
|
-
params.putString("message", "Error starting location: ${e.message}")
|
|
277
|
-
emitEvent(reactApplicationContext, "onLocationError", params)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private fun stopLocationUpdates() {
|
|
282
|
-
if (!isLocationClientRunning) return
|
|
283
|
-
try {
|
|
284
|
-
fusedLocationClient.removeLocationUpdates(locationCallback)
|
|
285
|
-
isLocationClientRunning = false
|
|
286
|
-
Log.i("SensorModule", "Location updates stopped.")
|
|
287
|
-
} catch (e: Exception) {
|
|
288
|
-
Log.e("SensorModule", "Failed to stop location updates: " + e.message)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private fun emitEvent(reactContext: ReactContext, eventName: String, params: WritableMap?) {
|
|
293
|
-
if (reactContext.hasActiveCatalystInstance()) {
|
|
294
|
-
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(eventName, params)
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
private fun createPendingIntent(requestCode: Int): PendingIntent? {
|
|
299
|
-
val launchIntent = reactApplicationContext.packageManager.getLaunchIntentForPackage(reactApplicationContext.packageName)
|
|
300
|
-
val pendingIntentFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT
|
|
301
|
-
return launchIntent?.let { PendingIntent.getActivity(reactApplicationContext, requestCode, it, pendingIntentFlag) }
|
|
302
|
-
}
|
|
303
113
|
|
|
304
114
|
@ReactMethod
|
|
305
115
|
fun setStabilityThresholds(startThreshold: Int, stopThreshold: Int, promise: Promise) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
promise.resolve(true)
|
|
310
|
-
} catch (e: Exception) { promise.reject("CONFIG_ERROR", "Failed: ${e.message}") }
|
|
116
|
+
motionDetector.startStabilityThreshold = startThreshold
|
|
117
|
+
motionDetector.stopStabilityThreshold = stopThreshold
|
|
118
|
+
promise.resolve(true)
|
|
311
119
|
}
|
|
312
120
|
|
|
313
121
|
@ReactMethod
|
|
314
122
|
fun setUpdateInterval(ms: Int, promise: Promise) {
|
|
315
|
-
|
|
316
|
-
if (isMotionDetectorStarted) {
|
|
317
|
-
sensorManager.unregisterListener(this, accelerometer)
|
|
318
|
-
sensorManager.registerListener(this, accelerometer, samplingPeriodUs)
|
|
319
|
-
}
|
|
123
|
+
motionDetector.setUpdateInterval(ms)
|
|
320
124
|
promise.resolve(true)
|
|
321
125
|
}
|
|
322
|
-
|
|
126
|
+
|
|
323
127
|
@ReactMethod
|
|
324
128
|
fun isAvailable(promise: Promise) {
|
|
325
129
|
val map = Arguments.createMap()
|
|
326
|
-
map.putBoolean("accelerometer",
|
|
327
|
-
map.putBoolean("gyroscope", false)
|
|
130
|
+
map.putBoolean("accelerometer", motionDetector.isSensorAvailable())
|
|
131
|
+
map.putBoolean("gyroscope", false)
|
|
328
132
|
promise.resolve(map)
|
|
329
133
|
}
|
|
330
134
|
|
|
331
135
|
@ReactMethod
|
|
332
136
|
fun fireGeofenceAlert(type: String, userName: String, promise: Promise) {
|
|
333
137
|
try {
|
|
334
|
-
|
|
335
|
-
if (type == "OUT") {
|
|
336
|
-
val notification = NotificationCompat.Builder(reactApplicationContext, GEOFENCE_CHANNEL_ID)
|
|
337
|
-
.setSmallIcon(appIcon).setContentTitle("Geofence Alert 🔔")
|
|
338
|
-
.setContentText("$userName, you seem to have moved out of your designated work area.")
|
|
339
|
-
.setPriority(NotificationCompat.PRIORITY_HIGH).setCategory(NotificationCompat.CATEGORY_ALARM)
|
|
340
|
-
.setContentIntent(pendingIntent).setOngoing(true).setAutoCancel(false)
|
|
341
|
-
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC).build()
|
|
342
|
-
notificationManager.notify(GEOFENCE_OUT_ID, notification)
|
|
343
|
-
} else if (type == "IN") {
|
|
344
|
-
notificationManager.cancel(GEOFENCE_OUT_ID)
|
|
345
|
-
val notification = NotificationCompat.Builder(reactApplicationContext, GEOFENCE_CHANNEL_ID)
|
|
346
|
-
.setSmallIcon(appIcon).setContentTitle("You are in again ✅")
|
|
347
|
-
.setContentText("$userName, you have moved back into your designated work area.")
|
|
348
|
-
.setPriority(NotificationCompat.PRIORITY_DEFAULT).setContentIntent(pendingIntent)
|
|
349
|
-
.setAutoCancel(true).setVisibility(NotificationCompat.VISIBILITY_PUBLIC).build()
|
|
350
|
-
notificationManager.notify(GEOFENCE_IN_ID, notification)
|
|
351
|
-
}
|
|
138
|
+
notificationHelper.fireGeofenceAlert(type, userName)
|
|
352
139
|
promise.resolve(true)
|
|
353
140
|
} catch (e: Exception) { promise.reject("NOTIFY_FAILED", e.message) }
|
|
354
141
|
}
|
|
355
|
-
|
|
142
|
+
|
|
356
143
|
@ReactMethod
|
|
357
144
|
fun fireGenericAlert(title: String, body: String, id: Int, promise: Promise) {
|
|
358
145
|
try {
|
|
359
|
-
|
|
360
|
-
val notification = NotificationCompat.Builder(reactApplicationContext, GEOFENCE_CHANNEL_ID)
|
|
361
|
-
.setSmallIcon(appIcon).setContentTitle(title).setContentText(body)
|
|
362
|
-
.setPriority(NotificationCompat.PRIORITY_HIGH).setCategory(NotificationCompat.CATEGORY_REMINDER)
|
|
363
|
-
.setContentIntent(pendingIntent).setAutoCancel(true).setVisibility(NotificationCompat.VISIBILITY_PUBLIC).build()
|
|
364
|
-
notificationManager.notify(id, notification)
|
|
146
|
+
notificationHelper.fireGenericAlert(title, body, id)
|
|
365
147
|
promise.resolve(true)
|
|
366
148
|
} catch (e: Exception) { promise.reject("NOTIFY_FAILED", e.message) }
|
|
367
149
|
}
|
|
368
150
|
|
|
369
151
|
@ReactMethod
|
|
370
152
|
fun cancelGenericAlert(id: Int, promise: Promise) {
|
|
371
|
-
try {
|
|
153
|
+
try {
|
|
154
|
+
notificationHelper.cancelGenericAlert(id)
|
|
155
|
+
promise.resolve(true)
|
|
156
|
+
} catch (e: Exception) { promise.reject("CANCEL_FAILED", e.message) }
|
|
372
157
|
}
|
|
373
158
|
|
|
374
159
|
@ReactMethod fun addListener(eventName: String) {}
|
|
375
160
|
@ReactMethod fun removeListeners(count: Int) {}
|
|
161
|
+
|
|
162
|
+
// Cleanup
|
|
163
|
+
override fun onCatalystInstanceDestroy() {
|
|
164
|
+
super.onCatalystInstanceDestroy()
|
|
165
|
+
motionDetector.stop()
|
|
166
|
+
locationHelper.stopLocationUpdates()
|
|
167
|
+
|
|
168
|
+
// Optional: Stop service if you want app death to kill service
|
|
169
|
+
val intent = Intent(reactApplicationContext, TrackingService::class.java)
|
|
170
|
+
intent.action = TrackingService.ACTION_STOP
|
|
171
|
+
reactApplicationContext.startService(intent)
|
|
172
|
+
}
|
|
376
173
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
package com.rngeoactivitykit
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.app.Service
|
|
8
|
+
import android.content.Context
|
|
9
|
+
import android.content.Intent
|
|
10
|
+
import android.content.pm.ServiceInfo
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.os.IBinder
|
|
13
|
+
import android.os.PowerManager
|
|
14
|
+
import androidx.core.app.NotificationCompat
|
|
15
|
+
|
|
16
|
+
class TrackingService : Service() {
|
|
17
|
+
|
|
18
|
+
companion object {
|
|
19
|
+
var instance: TrackingService? = null
|
|
20
|
+
const val NOTIFICATION_ID = 9991
|
|
21
|
+
const val CHANNEL_ID = "geo_activity_kit_channel"
|
|
22
|
+
|
|
23
|
+
const val ACTION_START = "ACTION_START"
|
|
24
|
+
const val ACTION_STOP = "ACTION_STOP"
|
|
25
|
+
const val ACTION_UPDATE = "ACTION_UPDATE"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private var wakeLock: PowerManager.WakeLock? = null
|
|
29
|
+
|
|
30
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
31
|
+
|
|
32
|
+
override fun onCreate() {
|
|
33
|
+
super.onCreate()
|
|
34
|
+
instance = this
|
|
35
|
+
createNotificationChannel()
|
|
36
|
+
|
|
37
|
+
// Acquire WakeLock to keep CPU running
|
|
38
|
+
try {
|
|
39
|
+
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
|
|
40
|
+
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "GeoKit::TrackingLock")
|
|
41
|
+
wakeLock?.acquire(10 * 60 * 1000L /* 10 minutes timeout safety */)
|
|
42
|
+
} catch (e: Exception) {
|
|
43
|
+
e.printStackTrace()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
48
|
+
if (intent == null) return START_NOT_STICKY
|
|
49
|
+
|
|
50
|
+
when (intent.action) {
|
|
51
|
+
ACTION_START -> {
|
|
52
|
+
val title = intent.getStringExtra("title") ?: "Location Active"
|
|
53
|
+
val body = intent.getStringExtra("body") ?: "Monitoring in background..."
|
|
54
|
+
startForegroundService(title, body)
|
|
55
|
+
}
|
|
56
|
+
ACTION_STOP -> {
|
|
57
|
+
stopForegroundService()
|
|
58
|
+
}
|
|
59
|
+
ACTION_UPDATE -> {
|
|
60
|
+
val title = intent.getStringExtra("title")
|
|
61
|
+
val body = intent.getStringExtra("body")
|
|
62
|
+
updateNotification(title, body)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return START_STICKY
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun startForegroundService(title: String, body: String) {
|
|
69
|
+
val notification = buildNotification(title, body)
|
|
70
|
+
|
|
71
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
72
|
+
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)
|
|
73
|
+
} else {
|
|
74
|
+
startForeground(NOTIFICATION_ID, notification)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun stopForegroundService() {
|
|
79
|
+
try {
|
|
80
|
+
stopForeground(true)
|
|
81
|
+
stopSelf()
|
|
82
|
+
} catch (e: Exception) {
|
|
83
|
+
e.printStackTrace()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private fun updateNotification(title: String?, body: String?) {
|
|
88
|
+
val notification = buildNotification(title ?: "Location Active", body ?: "Monitoring...")
|
|
89
|
+
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
90
|
+
manager.notify(NOTIFICATION_ID, notification)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private fun buildNotification(title: String, body: String): Notification {
|
|
94
|
+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
95
|
+
val pendingIntent = if (launchIntent != null) {
|
|
96
|
+
PendingIntent.getActivity(
|
|
97
|
+
this, 0, launchIntent,
|
|
98
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT
|
|
99
|
+
)
|
|
100
|
+
} else null
|
|
101
|
+
|
|
102
|
+
val appIcon = applicationInfo.icon.let { if (it != 0) it else android.R.drawable.ic_menu_myplaces }
|
|
103
|
+
|
|
104
|
+
return NotificationCompat.Builder(this, CHANNEL_ID)
|
|
105
|
+
.setContentTitle(title)
|
|
106
|
+
.setContentText(body)
|
|
107
|
+
.setSmallIcon(appIcon)
|
|
108
|
+
.setContentIntent(pendingIntent)
|
|
109
|
+
.setOngoing(true)
|
|
110
|
+
.setOnlyAlertOnce(true)
|
|
111
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
112
|
+
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
|
113
|
+
.build()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun createNotificationChannel() {
|
|
117
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
118
|
+
val serviceChannel = NotificationChannel(
|
|
119
|
+
CHANNEL_ID,
|
|
120
|
+
"Background Tracking Service",
|
|
121
|
+
NotificationManager.IMPORTANCE_LOW
|
|
122
|
+
)
|
|
123
|
+
val manager = getSystemService(NotificationManager::class.java)
|
|
124
|
+
manager.createNotificationChannel(serviceChannel)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override fun onDestroy() {
|
|
129
|
+
super.onDestroy()
|
|
130
|
+
instance = null
|
|
131
|
+
if (wakeLock?.isHeld == true) {
|
|
132
|
+
try { wakeLock?.release() } catch (_: Exception) {}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
package/lib/module/index.js
CHANGED
|
@@ -5,8 +5,6 @@ const LINKING_ERROR = `The package 'react-native-geo-activity-kit' doesn't seem
|
|
|
5
5
|
ios: "- You have run 'pod install'\n",
|
|
6
6
|
default: ''
|
|
7
7
|
}) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n';
|
|
8
|
-
|
|
9
|
-
// The name must match getName() in SensorModule.kt
|
|
10
8
|
const RNSensorModule = NativeModules.RNSensorModule ? NativeModules.RNSensorModule : new Proxy({}, {
|
|
11
9
|
get() {
|
|
12
10
|
throw new Error(LINKING_ERROR);
|
|
@@ -14,19 +12,22 @@ const RNSensorModule = NativeModules.RNSensorModule ? NativeModules.RNSensorModu
|
|
|
14
12
|
});
|
|
15
13
|
const emitter = new NativeEventEmitter(RNSensorModule);
|
|
16
14
|
export default {
|
|
15
|
+
// Service Control
|
|
16
|
+
startForegroundService: (title, body) => RNSensorModule.startForegroundService(title, body),
|
|
17
|
+
stopForegroundService: () => RNSensorModule.stopForegroundService(),
|
|
18
|
+
updateServiceNotification: (title, body) => RNSensorModule.updateServiceNotification(title, body),
|
|
19
|
+
// Sensors & Configuration
|
|
17
20
|
startMotionDetector: (threshold = 0.8) => RNSensorModule.startMotionDetector(threshold),
|
|
18
21
|
stopMotionDetector: () => RNSensorModule.stopMotionDetector(),
|
|
19
22
|
setUpdateInterval: (ms = 100) => RNSensorModule.setUpdateInterval(ms),
|
|
20
23
|
setLocationUpdateInterval: (ms = 90000) => RNSensorModule.setLocationUpdateInterval(ms),
|
|
21
|
-
setStabilityThresholds: (
|
|
22
|
-
//
|
|
24
|
+
setStabilityThresholds: (start = 20, stop = 3000) => RNSensorModule.setStabilityThresholds(start, stop),
|
|
25
|
+
// Alerts
|
|
23
26
|
fireGeofenceAlert: (type, userName) => RNSensorModule.fireGeofenceAlert(type, userName),
|
|
24
|
-
// Added types: string for text, number for ID (assuming Android notification ID)
|
|
25
27
|
fireGenericAlert: (title, body, id) => RNSensorModule.fireGenericAlert(title, body, id),
|
|
26
|
-
// Added type: number to match the ID above
|
|
27
28
|
cancelGenericAlert: id => RNSensorModule.cancelGenericAlert(id),
|
|
28
29
|
isAvailable: () => RNSensorModule.isAvailable(),
|
|
29
|
-
//
|
|
30
|
+
// Listeners
|
|
30
31
|
addMotionListener: cb => emitter.addListener('onMotionStateChanged', cb),
|
|
31
32
|
addLocationLogListener: cb => emitter.addListener('onLocationLog', cb),
|
|
32
33
|
addLocationErrorListener: cb => emitter.addListener('onLocationError', cb)
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NativeModules","NativeEventEmitter","Platform","LINKING_ERROR","select","ios","default","RNSensorModule","Proxy","get","Error","emitter","startMotionDetector","threshold","stopMotionDetector","setUpdateInterval","ms","setLocationUpdateInterval","setStabilityThresholds","
|
|
1
|
+
{"version":3,"names":["NativeModules","NativeEventEmitter","Platform","LINKING_ERROR","select","ios","default","RNSensorModule","Proxy","get","Error","emitter","startForegroundService","title","body","stopForegroundService","updateServiceNotification","startMotionDetector","threshold","stopMotionDetector","setUpdateInterval","ms","setLocationUpdateInterval","setStabilityThresholds","start","stop","fireGeofenceAlert","type","userName","fireGenericAlert","id","cancelGenericAlert","isAvailable","addMotionListener","cb","addListener","addLocationLogListener","addLocationErrorListener"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,aAAa,EAAEC,kBAAkB,EAAEC,QAAQ,QAAQ,cAAc;AAE1E,MAAMC,aAAa,GACjB,wFAAwF,GACxFD,QAAQ,CAACE,MAAM,CAAC;EAAEC,GAAG,EAAE,gCAAgC;EAAEC,OAAO,EAAE;AAAG,CAAC,CAAC,GACvE,sDAAsD,GACtD,+BAA+B;AAEjC,MAAMC,cAAc,GAAGP,aAAa,CAACO,cAAc,GAC/CP,aAAa,CAACO,cAAc,GAC5B,IAAIC,KAAK,CACP,CAAC,CAAC,EACF;EACEC,GAAGA,CAAA,EAAG;IACJ,MAAM,IAAIC,KAAK,CAACP,aAAa,CAAC;EAChC;AACF,CACF,CAAC;AAEL,MAAMQ,OAAO,GAAG,IAAIV,kBAAkB,CAACM,cAAc,CAAC;AAEtD,eAAe;EACb;EACAK,sBAAsB,EAAEA,CAACC,KAAa,EAAEC,IAAY,KAClDP,cAAc,CAACK,sBAAsB,CAACC,KAAK,EAAEC,IAAI,CAAC;EAEpDC,qBAAqB,EAAEA,CAAA,KACrBR,cAAc,CAACQ,qBAAqB,CAAC,CAAC;EAExCC,yBAAyB,EAAEA,CAACH,KAAa,EAAEC,IAAY,KACrDP,cAAc,CAACS,yBAAyB,CAACH,KAAK,EAAEC,IAAI,CAAC;EAEvD;EACAG,mBAAmB,EAAEA,CAACC,SAAiB,GAAG,GAAG,KAC3CX,cAAc,CAACU,mBAAmB,CAACC,SAAS,CAAC;EAE/CC,kBAAkB,EAAEA,CAAA,KAAMZ,cAAc,CAACY,kBAAkB,CAAC,CAAC;EAE7DC,iBAAiB,EAAEA,CAACC,EAAU,GAAG,GAAG,KAAKd,cAAc,CAACa,iBAAiB,CAACC,EAAE,CAAC;EAE7EC,yBAAyB,EAAEA,CAACD,EAAU,GAAG,KAAK,KAC5Cd,cAAc,CAACe,yBAAyB,CAACD,EAAE,CAAC;EAE9CE,sBAAsB,EAAEA,CAACC,KAAa,GAAG,EAAE,EAAEC,IAAY,GAAG,IAAI,KAC9DlB,cAAc,CAACgB,sBAAsB,CAACC,KAAK,EAAEC,IAAI,CAAC;EAEpD;EACAC,iBAAiB,EAAEA,CAACC,IAAY,EAAEC,QAAgB,KAChDrB,cAAc,CAACmB,iBAAiB,CAACC,IAAI,EAAEC,QAAQ,CAAC;EAElDC,gBAAgB,EAAEA,CAAChB,KAAa,EAAEC,IAAY,EAAEgB,EAAU,KACxDvB,cAAc,CAACsB,gBAAgB,CAAChB,KAAK,EAAEC,IAAI,EAAEgB,EAAE,CAAC;EAElDC,kBAAkB,EAAGD,EAAU,IAAKvB,cAAc,CAACwB,kBAAkB,CAACD,EAAE,CAAC;EAEzEE,WAAW,EAAEA,CAAA,KAAMzB,cAAc,CAACyB,WAAW,CAAC,CAAC;EAE/C;EACAC,iBAAiB,EAAGC,EAAwB,IAC1CvB,OAAO,CAACwB,WAAW,CAAC,sBAAsB,EAAED,EAAE,CAAC;EAEjDE,sBAAsB,EAAGF,EAAwB,IAC/CvB,OAAO,CAACwB,WAAW,CAAC,eAAe,EAAED,EAAE,CAAC;EAE1CG,wBAAwB,EAAGH,EAAwB,IACjDvB,OAAO,CAACwB,WAAW,CAAC,iBAAiB,EAAED,EAAE;AAC7C,CAAC","ignoreList":[]}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
|
+
startForegroundService: (title: string, body: string) => Promise<boolean>;
|
|
3
|
+
stopForegroundService: () => Promise<boolean>;
|
|
4
|
+
updateServiceNotification: (title: string, body: string) => Promise<boolean>;
|
|
2
5
|
startMotionDetector: (threshold?: number) => any;
|
|
3
6
|
stopMotionDetector: () => any;
|
|
4
7
|
setUpdateInterval: (ms?: number) => any;
|
|
5
8
|
setLocationUpdateInterval: (ms?: number) => any;
|
|
6
|
-
setStabilityThresholds: (
|
|
9
|
+
setStabilityThresholds: (start?: number, stop?: number) => any;
|
|
7
10
|
fireGeofenceAlert: (type: string, userName: string) => any;
|
|
8
11
|
fireGenericAlert: (title: string, body: string, id: number) => any;
|
|
9
12
|
cancelGenericAlert: (id: number) => any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":";oCAuBkC,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;iCAG5C,OAAO,CAAC,OAAO,CAAC;uCAGR,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;sCAIzC,MAAM;;6BAKf,MAAM;qCAEE,MAAM;qCAGN,MAAM,SAAa,MAAM;8BAI/B,MAAM,YAAY,MAAM;8BAGxB,MAAM,QAAQ,MAAM,MAAM,MAAM;6BAGjC,MAAM;;4BAKP,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI;iCAGf,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI;mCAGlB,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI;;AA3CrD,wBA6CE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-geo-activity-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Battery-efficient location tracking with motion detection and native notifications.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
package/src/index.tsx
CHANGED
|
@@ -6,7 +6,6 @@ const LINKING_ERROR =
|
|
|
6
6
|
'- You rebuilt the app after installing the package\n' +
|
|
7
7
|
'- You are not using Expo Go\n';
|
|
8
8
|
|
|
9
|
-
// The name must match getName() in SensorModule.kt
|
|
10
9
|
const RNSensorModule = NativeModules.RNSensorModule
|
|
11
10
|
? NativeModules.RNSensorModule
|
|
12
11
|
: new Proxy(
|
|
@@ -21,6 +20,17 @@ const RNSensorModule = NativeModules.RNSensorModule
|
|
|
21
20
|
const emitter = new NativeEventEmitter(RNSensorModule);
|
|
22
21
|
|
|
23
22
|
export default {
|
|
23
|
+
// Service Control
|
|
24
|
+
startForegroundService: (title: string, body: string): Promise<boolean> =>
|
|
25
|
+
RNSensorModule.startForegroundService(title, body),
|
|
26
|
+
|
|
27
|
+
stopForegroundService: (): Promise<boolean> =>
|
|
28
|
+
RNSensorModule.stopForegroundService(),
|
|
29
|
+
|
|
30
|
+
updateServiceNotification: (title: string, body: string): Promise<boolean> =>
|
|
31
|
+
RNSensorModule.updateServiceNotification(title, body),
|
|
32
|
+
|
|
33
|
+
// Sensors & Configuration
|
|
24
34
|
startMotionDetector: (threshold: number = 0.8) =>
|
|
25
35
|
RNSensorModule.startMotionDetector(threshold),
|
|
26
36
|
|
|
@@ -31,25 +41,21 @@ export default {
|
|
|
31
41
|
setLocationUpdateInterval: (ms: number = 90000) =>
|
|
32
42
|
RNSensorModule.setLocationUpdateInterval(ms),
|
|
33
43
|
|
|
34
|
-
setStabilityThresholds: (
|
|
35
|
-
|
|
36
|
-
stopThreshold: number = 3000
|
|
37
|
-
) => RNSensorModule.setStabilityThresholds(startThreshold, stopThreshold),
|
|
44
|
+
setStabilityThresholds: (start: number = 20, stop: number = 3000) =>
|
|
45
|
+
RNSensorModule.setStabilityThresholds(start, stop),
|
|
38
46
|
|
|
39
|
-
//
|
|
47
|
+
// Alerts
|
|
40
48
|
fireGeofenceAlert: (type: string, userName: string) =>
|
|
41
49
|
RNSensorModule.fireGeofenceAlert(type, userName),
|
|
42
50
|
|
|
43
|
-
// Added types: string for text, number for ID (assuming Android notification ID)
|
|
44
51
|
fireGenericAlert: (title: string, body: string, id: number) =>
|
|
45
52
|
RNSensorModule.fireGenericAlert(title, body, id),
|
|
46
53
|
|
|
47
|
-
// Added type: number to match the ID above
|
|
48
54
|
cancelGenericAlert: (id: number) => RNSensorModule.cancelGenericAlert(id),
|
|
49
55
|
|
|
50
56
|
isAvailable: () => RNSensorModule.isAvailable(),
|
|
51
57
|
|
|
52
|
-
//
|
|
58
|
+
// Listeners
|
|
53
59
|
addMotionListener: (cb: (event: any) => void) =>
|
|
54
60
|
emitter.addListener('onMotionStateChanged', cb),
|
|
55
61
|
|
|
@@ -58,4 +64,4 @@ export default {
|
|
|
58
64
|
|
|
59
65
|
addLocationErrorListener: (cb: (event: any) => void) =>
|
|
60
66
|
emitter.addListener('onLocationError', cb),
|
|
61
|
-
};
|
|
67
|
+
};
|