Skip to content

Commit 8a1a969

Browse files
Merge pull request #18 from gabriel-sisjr/feature/sql-lite-storage
Feature - Adding Room Support for Persistent Data
2 parents 4a92101 + 423d53a commit 8a1a969

File tree

14 files changed

+1048
-336
lines changed

14 files changed

+1048
-336
lines changed

CHANGELOG.md

Lines changed: 370 additions & 178 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ A React Native library for tracking location in the background using TurboModule
2424

2525
-**Background location tracking** - Continues tracking when app is in background
2626
-**Real-time location updates** - Automatic event-driven location watching
27+
-**Crash recovery** - Automatic recovery of tracking sessions after app crash or restart
28+
-**Battery optimization** - Configurable accuracy levels and update intervals for efficient battery usage
2729
-**TurboModule** - Built with React Native's New Architecture for better performance
2830
-**Session-based tracking** - Organize location data by trip/session IDs
2931
-**TypeScript support** - Fully typed API
3032
-**Android support** - Native Kotlin implementation (iOS coming soon)
31-
-**Persistent storage** - Locations are stored and survive app restarts
33+
-**Persistent storage** - Locations are stored in Room Database and survive app restarts
3234
-**Foreground service** - Uses Android foreground service for reliable tracking
3335

3436
## Installation
@@ -65,29 +67,6 @@ yarn add @gabriel-sisjr/react-native-background-location
6567
</manifest>
6668
```
6769

68-
2. **Request permissions at runtime:** _(not recommended, should use hook instead)_
69-
70-
```typescript
71-
import { PermissionsAndroid, Platform } from 'react-native';
72-
73-
const requestLocationPermissions = async () => {
74-
if (Platform.OS === 'android') {
75-
const granted = await PermissionsAndroid.requestMultiple([
76-
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
77-
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
78-
PermissionsAndroid.PERMISSIONS.ACCESS_BACKGROUND_LOCATION,
79-
]);
80-
81-
return (
82-
granted['android.permission.ACCESS_FINE_LOCATION'] === 'granted' &&
83-
granted['android.permission.ACCESS_COARSE_LOCATION'] === 'granted' &&
84-
granted['android.permission.ACCESS_BACKGROUND_LOCATION'] === 'granted'
85-
);
86-
}
87-
return true;
88-
};
89-
```
90-
9170
### iOS
9271

9372
iOS support is coming in a future release.
@@ -556,14 +535,24 @@ See the [example app](example/src/App.tsx) for complete implementation examples.
556535

557536
## Battery Optimization
558537

559-
⚠️ **Important:** Background location tracking can significantly impact battery life. Consider:
538+
The library includes built-in battery optimization features:
539+
540+
- **Configurable accuracy levels** - Use `LocationAccuracy.LOW_POWER` or `BALANCED_POWER_ACCURACY` for better battery efficiency
541+
- **Adjustable update intervals** - Increase intervals to reduce battery consumption
542+
- **Smart location updates** - Only requests location when necessary
543+
- **Foreground service optimization** - Efficient service implementation
544+
545+
### Best Practices
546+
547+
- Use `LocationAccuracy.LOW_POWER` for long-term tracking
548+
- Increase `updateInterval` when high-frequency updates aren't needed
549+
- Stop tracking when not in use
550+
- Inform users about battery usage
551+
- Test on real devices (emulator GPS simulation is unreliable)
560552

561-
- Only tracking when necessary
562-
- Stopping tracking when done
563-
- Informing users about battery usage
564-
- Testing on real devices (not emulators)
553+
### Android Battery Optimization
565554

566-
On Android, some manufacturers (Xiaomi, Huawei, etc.) have aggressive battery optimization that may kill background services. Users may need to whitelist your app in battery settings.
555+
Some Android manufacturers (Xiaomi, Huawei, etc.) have aggressive battery optimization that may kill background services. Users may need to whitelist your app in battery settings for optimal performance.
567556

568557
## Simulator/Emulator Support
569558

@@ -714,12 +703,9 @@ Make sure your `tsconfig.json` includes:
714703
## Roadmap
715704

716705
- [ ] iOS implementation with Swift
717-
- [ ] Customizable location update intervals
718706
- [ ] Geofencing support
719707
- [ ] Distance filtering for GPS coordinates
720-
- [ ] SQLite storage option for large datasets
721708
- [ ] Configurable notification appearance
722-
- [ ] Battery optimization modes
723709
- [ ] Web support (Geolocation API)
724710

725711
## Contributing

android/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ buildscript {
1212
classpath "com.android.tools.build:gradle:8.7.2"
1313
// noinspection DifferentKotlinGradleVersion
1414
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15+
classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.1.20-1.0.32"
1516
}
1617
}
1718

1819

1920
apply plugin: "com.android.library"
2021
apply plugin: "kotlin-android"
2122
apply plugin: "kotlin-parcelize"
23+
apply plugin: "com.google.devtools.ksp"
2224

2325
apply plugin: "com.facebook.react"
2426

@@ -78,4 +80,10 @@ dependencies {
7880

7981
// Google Play Services for Location
8082
implementation "com.google.android.gms:play-services-location:21.3.0"
83+
84+
// Room
85+
def room = "2.6.1"
86+
implementation("androidx.room:room-runtime:$room")
87+
implementation("androidx.room:room-ktx:$room")
88+
ksp("androidx.room:room-compiler:$room")
8189
}

android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
BackgroundLocation_kotlinVersion=2.0.21
1+
BackgroundLocation_kotlinVersion=2.1.20
22
BackgroundLocation_minSdkVersion=24
33
BackgroundLocation_targetSdkVersion=34
44
BackgroundLocation_compileSdkVersion=35

android/src/main/java/com/backgroundlocation/BackgroundLocationModule.kt

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ class BackgroundLocationModule(reactContext: ReactApplicationContext) :
2121
init {
2222
// Set the React Context in LocationService for event emission
2323
LocationService.setReactContext(reactContext)
24+
25+
// Attempt to recover tracking session if app crashed/restarted
26+
recoverTrackingSession()
27+
}
28+
29+
override fun initialize() {
30+
super.initialize()
31+
// Re-set the React Context when module is initialized
32+
LocationService.setReactContext(reactApplicationContext)
2433
}
2534

2635
override fun getName(): String = NAME
@@ -59,9 +68,12 @@ class BackgroundLocationModule(reactContext: ReactApplicationContext) :
5968
// Parse options if provided
6069
val trackingOptions = parseTrackingOptions(options)
6170

62-
// Save tracking state
63-
storage.saveTrackingState(effectiveTripId, true)
71+
// Save tracking state with options for recovery
72+
storage.saveTrackingState(effectiveTripId, true, trackingOptions)
6473

74+
// Ensure React Context is set before starting service
75+
LocationService.setReactContext(reactApplicationContext)
76+
6577
// Start the foreground service with options
6678
val context = reactApplicationContext
6779
LocationService.startService(context, effectiveTripId, trackingOptions)
@@ -164,6 +176,44 @@ class BackgroundLocationModule(reactContext: ReactApplicationContext) :
164176
}
165177
}
166178

179+
/**
180+
* Recovers tracking session after app restart/crash
181+
* Restarts the LocationService if tracking was active
182+
*/
183+
private fun recoverTrackingSession() {
184+
try {
185+
val trackingState = storage.getTrackingState()
186+
187+
// If tracking was active and we have both tripId and options
188+
if (trackingState.isActive && trackingState.tripId != null) {
189+
// Check if we still have location permissions
190+
if (!hasLocationPermissions()) {
191+
// Clear tracking state if permissions were revoked
192+
storage.saveTrackingState(null, false)
193+
return
194+
}
195+
196+
// Ensure React Context is set before restarting service
197+
LocationService.setReactContext(reactApplicationContext)
198+
199+
// Restart the service with saved options
200+
val options = trackingState.options ?: TrackingOptions()
201+
val context = reactApplicationContext
202+
LocationService.startService(context, trackingState.tripId, options)
203+
}
204+
} catch (e: Exception) {
205+
// Log error but don't crash - recovery is best-effort
206+
e.printStackTrace()
207+
208+
// Clear tracking state if recovery fails
209+
try {
210+
storage.saveTrackingState(null, false)
211+
} catch (clearError: Exception) {
212+
clearError.printStackTrace()
213+
}
214+
}
215+
}
216+
167217
/**
168218
* Checks if the app has the necessary location permissions
169219
*/

android/src/main/java/com/backgroundlocation/LocationService.kt

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,34 @@ class LocationService : Service() {
3838
}
3939

4040
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
41-
currentTripId = intent?.getStringExtra(EXTRA_TRIP_ID)
41+
android.util.Log.d("LocationService", "onStartCommand called")
42+
// Try to get tripId from intent
43+
var tripId = intent?.getStringExtra(EXTRA_TRIP_ID)
4244

4345
// Parse tracking options from Bundle
4446
val optionsBundle = intent?.getBundleExtra(EXTRA_TRACKING_OPTIONS)
4547
trackingOptions = if (optionsBundle != null) {
4648
parseTrackingOptionsFromBundle(optionsBundle)
4749
} else {
48-
TrackingOptions()
50+
// If intent is null (service restarted by system), try to recover from storage
51+
if (intent == null) {
52+
val trackingState = storage.getTrackingState()
53+
if (trackingState.isActive && trackingState.tripId != null) {
54+
tripId = trackingState.tripId
55+
trackingState.options?.let { trackingOptions = it }
56+
}
57+
}
58+
trackingOptions
4959
}
5060

51-
if (currentTripId == null) {
61+
if (tripId == null) {
62+
// No valid tripId - stop the service
5263
stopSelf()
5364
return START_NOT_STICKY
5465
}
66+
67+
currentTripId = tripId
68+
android.util.Log.d("LocationService", "Starting location tracking for tripId: $tripId")
5569

5670
// Create notification channel with options
5771
createNotificationChannel()
@@ -60,9 +74,13 @@ class LocationService : Service() {
6074
val notification = createNotification()
6175
startForeground(NOTIFICATION_ID, notification)
6276

77+
// Check last known location to verify GPS is working
78+
checkLastKnownLocation()
79+
6380
// Start location updates
6481
startLocationUpdates()
6582

83+
// Return START_STICKY so the service is restarted if killed by system
6684
return START_STICKY
6785
}
6886

@@ -84,6 +102,24 @@ class LocationService : Service() {
84102
)
85103
}
86104

105+
@SuppressLint("MissingPermission")
106+
private fun checkLastKnownLocation() {
107+
try {
108+
android.util.Log.d("LocationService", "Checking last known location...")
109+
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
110+
if (location != null) {
111+
android.util.Log.d("LocationService", "Last known location: lat=${location.latitude}, lng=${location.longitude}")
112+
} else {
113+
android.util.Log.w("LocationService", "Last known location is NULL - GPS may not have a fix yet")
114+
}
115+
}.addOnFailureListener { e ->
116+
android.util.Log.e("LocationService", "Failed to get last known location", e)
117+
}
118+
} catch (e: Exception) {
119+
android.util.Log.e("LocationService", "Exception checking last known location", e)
120+
}
121+
}
122+
87123
@SuppressLint("MissingPermission")
88124
private fun startLocationUpdates() {
89125
val priority = when (trackingOptions.getAccuracyOrDefault()) {
@@ -94,30 +130,51 @@ class LocationService : Service() {
94130
LocationAccuracy.PASSIVE -> Priority.PRIORITY_PASSIVE
95131
}
96132

133+
val updateInterval = trackingOptions.getUpdateIntervalOrDefault()
134+
val fastestInterval = trackingOptions.getFastestIntervalOrDefault()
135+
val maxWaitTime = trackingOptions.getMaxWaitTimeOrDefault()
136+
137+
android.util.Log.d("LocationService", "Starting location updates with:")
138+
android.util.Log.d("LocationService", " Priority: $priority")
139+
android.util.Log.d("LocationService", " Update Interval: ${updateInterval}ms")
140+
android.util.Log.d("LocationService", " Fastest Interval: ${fastestInterval}ms")
141+
android.util.Log.d("LocationService", " Max Wait Time: ${maxWaitTime}ms")
142+
97143
val locationRequest = LocationRequest.Builder(
98144
priority,
99-
trackingOptions.getUpdateIntervalOrDefault()
145+
updateInterval
100146
).apply {
101-
setMinUpdateIntervalMillis(trackingOptions.getFastestIntervalOrDefault())
102-
setMaxUpdateDelayMillis(trackingOptions.getMaxWaitTimeOrDefault())
147+
setMinUpdateIntervalMillis(fastestInterval)
148+
setMaxUpdateDelayMillis(maxWaitTime)
103149
setWaitForAccurateLocation(trackingOptions.getWaitForAccurateLocationOrDefault())
104150
}.build()
105151

106152
try {
153+
android.util.Log.d("LocationService", "Requesting location updates...")
107154
fusedLocationClient.requestLocationUpdates(
108155
locationRequest,
109156
locationCallback,
110157
Looper.getMainLooper()
111-
)
158+
).addOnSuccessListener {
159+
android.util.Log.d("LocationService", "Location updates request SUCCESS")
160+
}.addOnFailureListener { e ->
161+
android.util.Log.e("LocationService", "Location updates request FAILED", e)
162+
stopSelf()
163+
}
112164
} catch (e: SecurityException) {
165+
android.util.Log.e("LocationService", "SecurityException when requesting location updates", e)
113166
e.printStackTrace()
114167
stopSelf()
168+
} catch (e: Exception) {
169+
android.util.Log.e("LocationService", "Exception when requesting location updates", e)
170+
e.printStackTrace()
115171
}
116172
}
117173

118174
private fun setupLocationCallback() {
119175
locationCallback = object : LocationCallback() {
120176
override fun onLocationResult(locationResult: LocationResult) {
177+
android.util.Log.d("LocationService", "Received ${locationResult.locations.size} location(s)")
121178
locationResult.locations.forEach { location ->
122179
handleLocation(location)
123180
}
@@ -126,6 +183,7 @@ class LocationService : Service() {
126183
}
127184

128185
private fun handleLocation(location: Location) {
186+
android.util.Log.d("LocationService", "Handling location: lat=${location.latitude}, lng=${location.longitude}, tripId=$currentTripId")
129187
currentTripId?.let { tripId ->
130188
// Extract all available location data
131189
val accuracy = if (location.hasAccuracy()) location.accuracy else null
@@ -183,17 +241,29 @@ class LocationService : Service() {
183241
* Sends a location update event to React Native with extended location data
184242
*/
185243
private fun sendLocationUpdateEvent(tripId: String, location: Location) {
186-
reactContext?.let { context ->
187-
try {
188-
val eventData = createLocationMap(tripId, location)
189-
190-
context
191-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
192-
.emit("onLocationUpdate", eventData)
193-
} catch (e: Exception) {
194-
// React Native context may not be available yet
195-
e.printStackTrace()
244+
val context = reactContext
245+
if (context == null) {
246+
android.util.Log.w("LocationService", "React context not available - cannot emit location update event")
247+
return
248+
}
249+
250+
try {
251+
// Check if the catalyst instance is active
252+
if (!context.hasActiveReactInstance()) {
253+
android.util.Log.w("LocationService", "React instance not active - skipping location update event")
254+
return
196255
}
256+
257+
val eventData = createLocationMap(tripId, location)
258+
259+
context
260+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
261+
.emit("onLocationUpdate", eventData)
262+
263+
android.util.Log.d("LocationService", "Location update event emitted for tripId: $tripId")
264+
} catch (e: Exception) {
265+
// React Native context may not be available yet or JS thread might be busy
266+
android.util.Log.e("LocationService", "Failed to emit location update event", e)
197267
}
198268
}
199269

@@ -325,6 +395,7 @@ class LocationService : Service() {
325395
* Sets the React Context for event emission
326396
*/
327397
fun setReactContext(context: ReactContext?) {
398+
android.util.Log.d("LocationService", "Setting React context: ${context != null}")
328399
serviceInstance?.reactContext = context
329400
}
330401

0 commit comments

Comments
 (0)