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.
@@ -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
- </manifest>
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.Manifest
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.facebook.react.modules.core.DeviceEventManagerModule
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), SensorEventListener {
8
+ class SensorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
29
9
 
30
- private val sensorManager: SensorManager = reactContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
31
- private var accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
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
- @Volatile private var motionThreshold: Float = 0.8f
46
- @Volatile private var currentState: String = "STATIONARY"
47
- private var isMotionDetectorStarted: Boolean = false
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 fun createNotificationChannel() {
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
- private fun updateLocationRequest(priority: Int, intervalMs: Long) {
114
- if (locationRequest.priority == priority && locationRequest.interval == intervalMs) return
115
-
116
- Log.i("SensorModule", "Switching Location Mode -> Priority: $priority, Interval: $intervalMs")
117
-
118
- locationRequest = LocationRequest.create().apply {
119
- this.interval = intervalMs
120
- this.fastestInterval = intervalMs
121
- this.priority = priority
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
- override fun onSensorChanged(event: SensorEvent?) {
139
- event ?: return
140
- if (event.sensor.type == Sensor.TYPE_ACCELEROMETER && isMotionDetectorStarted) {
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
- gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0]
143
- gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1]
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
- potentialState = newState
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
- override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
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
- // --- CLEANUP FIX: Prevents "Zombie" listeners on hot-reload ---
195
- override fun onCatalystInstanceDestroy() {
196
- super.onCatalystInstanceDestroy()
67
+ @ReactMethod
68
+ fun updateServiceNotification(title: String, body: String, promise: Promise) {
197
69
  try {
198
- // Stop Sensors
199
- sensorManager.unregisterListener(this)
200
-
201
- // Stop Location Updates
202
- if (isLocationClientRunning) {
203
- fusedLocationClient.removeLocationUpdates(locationCallback)
204
- isLocationClientRunning = false
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
- Log.e("SensorModule", "Error during cleanup: ${e.message}")
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
- if (accelerometer == null) {
215
- promise.reject("NO_SENSOR", "Accelerometer not available on this device")
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
- updateLocationRequest(Priority.PRIORITY_BALANCED_POWER_ACCURACY, 180000)
227
- startLocationUpdates()
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
- isMotionDetectorStarted = false
235
- sensorManager.unregisterListener(this, accelerometer)
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
- try {
307
- startStabilityThreshold = startThreshold.coerceAtLeast(1)
308
- stopStabilityThreshold = stopThreshold.coerceAtLeast(1)
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
- samplingPeriodUs = ms.coerceAtLeast(100) * 1000
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", accelerometer != null)
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
- val pendingIntent = createPendingIntent(0)
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
- val pendingIntent = createPendingIntent(id)
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 { notificationManager.cancel(id); promise.resolve(true) } catch (e: Exception) { promise.reject("CANCEL_FAILED", e.message) }
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
+ }
@@ -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: (startThreshold = 20, stopThreshold = 3000) => RNSensorModule.setStabilityThresholds(startThreshold, stopThreshold),
22
- // Added types: string for text inputs
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
- // Added type: function that accepts an event (any)
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)
@@ -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","startThreshold","stopThreshold","fireGeofenceAlert","type","userName","fireGenericAlert","title","body","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;AACA,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;EACbK,mBAAmB,EAAEA,CAACC,SAAiB,GAAG,GAAG,KAC3CN,cAAc,CAACK,mBAAmB,CAACC,SAAS,CAAC;EAE/CC,kBAAkB,EAAEA,CAAA,KAAMP,cAAc,CAACO,kBAAkB,CAAC,CAAC;EAE7DC,iBAAiB,EAAEA,CAACC,EAAU,GAAG,GAAG,KAAKT,cAAc,CAACQ,iBAAiB,CAACC,EAAE,CAAC;EAE7EC,yBAAyB,EAAEA,CAACD,EAAU,GAAG,KAAK,KAC5CT,cAAc,CAACU,yBAAyB,CAACD,EAAE,CAAC;EAE9CE,sBAAsB,EAAEA,CACtBC,cAAsB,GAAG,EAAE,EAC3BC,aAAqB,GAAG,IAAI,KACzBb,cAAc,CAACW,sBAAsB,CAACC,cAAc,EAAEC,aAAa,CAAC;EAEzE;EACAC,iBAAiB,EAAEA,CAACC,IAAY,EAAEC,QAAgB,KAChDhB,cAAc,CAACc,iBAAiB,CAACC,IAAI,EAAEC,QAAQ,CAAC;EAElD;EACAC,gBAAgB,EAAEA,CAACC,KAAa,EAAEC,IAAY,EAAEC,EAAU,KACxDpB,cAAc,CAACiB,gBAAgB,CAACC,KAAK,EAAEC,IAAI,EAAEC,EAAE,CAAC;EAElD;EACAC,kBAAkB,EAAGD,EAAU,IAAKpB,cAAc,CAACqB,kBAAkB,CAACD,EAAE,CAAC;EAEzEE,WAAW,EAAEA,CAAA,KAAMtB,cAAc,CAACsB,WAAW,CAAC,CAAC;EAE/C;EACAC,iBAAiB,EAAGC,EAAwB,IAC1CpB,OAAO,CAACqB,WAAW,CAAC,sBAAsB,EAAED,EAAE,CAAC;EAEjDE,sBAAsB,EAAGF,EAAwB,IAC/CpB,OAAO,CAACqB,WAAW,CAAC,eAAe,EAAED,EAAE,CAAC;EAE1CG,wBAAwB,EAAGH,EAAwB,IACjDpB,OAAO,CAACqB,WAAW,CAAC,iBAAiB,EAAED,EAAE;AAC7C,CAAC","ignoreList":[]}
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: (startThreshold?: number, stopThreshold?: number) => any;
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":";sCAuBmC,MAAM;;6BAKf,MAAM;qCAEE,MAAM;8CAIpB,MAAM,kBACP,MAAM;8BAIG,MAAM,YAAY,MAAM;8BAIxB,MAAM,QAAQ,MAAM,MAAM,MAAM;6BAIjC,MAAM;;4BAKP,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI;iCAGf,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI;mCAGlB,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI;;AApCrD,wBAsCE"}
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.1.2",
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
- startThreshold: number = 20,
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
- // Added types: string for text inputs
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
- // Added type: function that accepts an event (any)
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
+ };