Compare commits
3 commits
f3ce66681a
...
a18baf9ae6
| Author | SHA1 | Date | |
|---|---|---|---|
| a18baf9ae6 | |||
| 33eb0cfa13 | |||
| 1980822919 |
10 changed files with 676 additions and 139 deletions
|
|
@ -15,7 +15,10 @@ Applies to the BackgroundGeolocation integration:
|
||||||
## Current implementation notes
|
## Current implementation notes
|
||||||
|
|
||||||
- Movement-driven recording only:
|
- Movement-driven recording only:
|
||||||
- IDLE uses `geolocation.distanceFilter: 200` (aim: no updates while not moving).
|
- IDLE relies on the SDK's **stop-detection + stationary geofence** (`geolocation.stopOnStationary` + `geolocation.stationaryRadius`) to avoid periodic stationary updates on Android.
|
||||||
|
- We explicitly exit moving mode on entry to IDLE (`changePace(false)`) to prevent drift-generated periodic locations.
|
||||||
|
- If the SDK later reports a real move (`onMotionChange(isMoving:true)`), JS may request **one** persisted fix as a fallback.
|
||||||
|
- We intentionally do not rely on time-based updates.
|
||||||
- ACTIVE uses `geolocation.distanceFilter: 25`.
|
- ACTIVE uses `geolocation.distanceFilter: 25`.
|
||||||
- JS may request a persisted fix when entering ACTIVE (see [`applyProfile()`](src/location/trackLocation.js:351)).
|
- JS may request a persisted fix when entering ACTIVE (see [`applyProfile()`](src/location/trackLocation.js:351)).
|
||||||
- Upload strategy is intentionally simple:
|
- Upload strategy is intentionally simple:
|
||||||
|
|
@ -27,8 +30,41 @@ Applies to the BackgroundGeolocation integration:
|
||||||
- Stationary noise suppression:
|
- Stationary noise suppression:
|
||||||
- Native accuracy gate for persisted/uploaded locations: `geolocation.filter.trackingAccuracyThreshold: 100`.
|
- Native accuracy gate for persisted/uploaded locations: `geolocation.filter.trackingAccuracyThreshold: 100`.
|
||||||
- Identical location suppression: `geolocation.allowIdenticalLocations: false`.
|
- Identical location suppression: `geolocation.allowIdenticalLocations: false`.
|
||||||
|
- IDLE primarily relies on stop-detection + stationary geofence (`stopOnStationary: true`) to eliminate periodic stationary updates.
|
||||||
|
- Elasticity disabled (`disableElasticity: true`) to avoid dynamic distanceFilter shrink.
|
||||||
- Extra safety: any JS-triggered persisted fix requests are tagged and ignored if accuracy > 100m.
|
- Extra safety: any JS-triggered persisted fix requests are tagged and ignored if accuracy > 100m.
|
||||||
|
|
||||||
|
## Concise testing checklist (Android + iOS)
|
||||||
|
|
||||||
|
### 1) Baseline setup
|
||||||
|
|
||||||
|
- App has foreground + background location permissions.
|
||||||
|
- Motion/Activity permission granted (iOS motion, Android activity-recognition if prompted).
|
||||||
|
- Logged-in (to validate native HTTP uploads).
|
||||||
|
|
||||||
|
### 2) IDLE (no open alert)
|
||||||
|
|
||||||
|
1. Launch app and confirm there is **no open alert** owned by the current user.
|
||||||
|
2. Leave phone stationary for 10+ minutes (screen on and screen off).
|
||||||
|
- Expect: no periodic server uploads.
|
||||||
|
3. Walk/drive ~250m.
|
||||||
|
- Expect: a movement-triggered persisted location + upload.
|
||||||
|
|
||||||
|
### 3) ACTIVE (open alert)
|
||||||
|
|
||||||
|
1. Open an alert owned by the current user.
|
||||||
|
2. Move ~30m.
|
||||||
|
- Expect: at least one persisted location reaches server quickly.
|
||||||
|
3. Continue moving.
|
||||||
|
- Expect: updates align with movement (distanceFilter-based), not time.
|
||||||
|
|
||||||
|
### 4) Lifecycle coverage
|
||||||
|
|
||||||
|
- Foreground → background: repeat IDLE and ACTIVE steps.
|
||||||
|
- Terminated:
|
||||||
|
- Android: swipe-away from recents, then move the above distances and verify server updates.
|
||||||
|
- iOS: swipe-kill, then move significantly and verify app relaunch + upload after relaunch.
|
||||||
|
|
||||||
## Basic preconditions
|
## Basic preconditions
|
||||||
|
|
||||||
- Location permissions: foreground + background granted.
|
- Location permissions: foreground + background granted.
|
||||||
|
|
@ -43,7 +79,7 @@ Applies to the BackgroundGeolocation integration:
|
||||||
2. Stay stationary for 5+ minutes.
|
2. Stay stationary for 5+ minutes.
|
||||||
- Expect: no repeated server updates.
|
- Expect: no repeated server updates.
|
||||||
3. Walk/drive ~250m.
|
3. Walk/drive ~250m.
|
||||||
- Expect: at least one location persisted + uploaded.
|
- Expect: `onMotionChange(isMoving:true)` then one persisted location + upload.
|
||||||
|
|
||||||
### ACTIVE (open alert)
|
### ACTIVE (open alert)
|
||||||
|
|
||||||
|
|
@ -61,7 +97,7 @@ Applies to the BackgroundGeolocation integration:
|
||||||
2. Stay stationary.
|
2. Stay stationary.
|
||||||
- Expect: no periodic uploads.
|
- Expect: no periodic uploads.
|
||||||
3. Move ~250m.
|
3. Move ~250m.
|
||||||
- Expect: a persisted record and upload.
|
- Expect: `onMotionChange(isMoving:true)` then one persisted record and upload.
|
||||||
|
|
||||||
### ACTIVE
|
### ACTIVE
|
||||||
|
|
||||||
|
|
@ -93,9 +129,9 @@ Applies to the BackgroundGeolocation integration:
|
||||||
|
|
||||||
| Platform | App state | Profile | Move | Expected signals |
|
| Platform | App state | Profile | Move | Expected signals |
|
||||||
|---|---|---|---:|---|
|
|---|---|---|---:|---|
|
||||||
| Android | foreground | IDLE | ~250m | [`onLocation`](src/location/trackLocation.js:693) (sample=false), then [`onHttp`](src/location/trackLocation.js:733) |
|
| Android | foreground | IDLE | ~250m | [`onMotionChange`](src/location/trackLocation.js:1192) then [`onLocation`](src/location/trackLocation.js:1085) (sample=false), then [`onHttp`](src/location/trackLocation.js:1150) |
|
||||||
| Android | background | IDLE | ~250m | same as above |
|
| Android | background | IDLE | ~250m | same as above |
|
||||||
| Android | swipe-away | IDLE | ~250m | native persists + uploads; verify server + `onHttp` when app relaunches |
|
| Android | swipe-away | IDLE | ~250m | native geofence triggers; verify server update; app may relaunch to deliver JS logs |
|
||||||
| Android | foreground | ACTIVE | ~30m | location + upload continues |
|
| Android | foreground | ACTIVE | ~30m | location + upload continues |
|
||||||
| iOS | background | IDLE | ~250m | movement-driven update; no periodic uploads while stationary |
|
| iOS | background | IDLE | ~250m | movement-driven update; no periodic uploads while stationary |
|
||||||
| iOS | swipe-killed | IDLE | significant | OS relaunch on movement; upload after relaunch |
|
| iOS | swipe-killed | IDLE | significant | OS relaunch on movement; upload after relaunch |
|
||||||
|
|
@ -103,9 +139,9 @@ Applies to the BackgroundGeolocation integration:
|
||||||
## What to look for in logs
|
## What to look for in logs
|
||||||
|
|
||||||
- App lifecycle tagging: [`updateTrackingContextExtras()`](src/location/trackLocation.js:63) should update `tracking_ctx.app_state` on AppState changes.
|
- App lifecycle tagging: [`updateTrackingContextExtras()`](src/location/trackLocation.js:63) should update `tracking_ctx.app_state` on AppState changes.
|
||||||
- No time-based uploads: heartbeat is disabled (`heartbeatInterval: 0`), so no `Heartbeat` logs from [`onHeartbeat`](src/location/trackLocation.js:762).
|
- No time-based uploads: heartbeat is disabled (`heartbeatInterval: 0`).
|
||||||
- Movement-only uploads:
|
- Movement-only uploads:
|
||||||
- IDLE distance threshold: `distanceFilter: 200` in [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:148).
|
- IDLE: look for `Motion change` (isMoving=true) and (in rare cases) `IDLE movement fallback fix`.
|
||||||
- ACTIVE distance threshold: `distanceFilter: 25` in [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:148).
|
- ACTIVE distance threshold: `distanceFilter: 25` in [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:148).
|
||||||
|
|
||||||
- Attribution for `getCurrentPosition`:
|
- Attribution for `getCurrentPosition`:
|
||||||
|
|
|
||||||
17
ios/Podfile
17
ios/Podfile
|
|
@ -129,5 +129,22 @@ target 'AlerteSecours' do
|
||||||
File.write(crop_toolbar_path, contents)
|
File.write(crop_toolbar_path, contents)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Fix CocoaPods xcconfig merge error for expo-dev-menu subspec targets.
|
||||||
|
# CocoaPods sometimes generates different DEFINES_MODULE values for these targets,
|
||||||
|
# which breaks `pod install` when it tries to merge xcconfigs.
|
||||||
|
expo_dev_menu_targets = %w[
|
||||||
|
expo-dev-menu
|
||||||
|
Main
|
||||||
|
ReactNativeCompatibles
|
||||||
|
SafeAreaView
|
||||||
|
Vendored
|
||||||
|
]
|
||||||
|
installer.pods_project.targets.each do |t|
|
||||||
|
next unless expo_dev_menu_targets.include?(t.name)
|
||||||
|
t.build_configurations.each do |config|
|
||||||
|
config.build_settings['DEFINES_MODULE'] = 'YES'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2178,11 +2178,32 @@ PODS:
|
||||||
- React-logger (= 0.79.6)
|
- React-logger (= 0.79.6)
|
||||||
- React-perflogger (= 0.79.6)
|
- React-perflogger (= 0.79.6)
|
||||||
- React-utils (= 0.79.6)
|
- React-utils (= 0.79.6)
|
||||||
- RNBackgroundFetch (4.2.8):
|
- RNBackgroundGeolocation (5.0.1):
|
||||||
- React-Core
|
|
||||||
- RNBackgroundGeolocation (4.19.0):
|
|
||||||
- CocoaLumberjack (~> 3.8.5)
|
- CocoaLumberjack (~> 3.8.5)
|
||||||
|
- DoubleConversion
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly (= 2024.11.18.00)
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-hermes
|
||||||
|
- React-ImageManager
|
||||||
|
- React-jsi
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-renderercss
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- TSLocationManager (~> 4.0.0)
|
||||||
|
- Yoga
|
||||||
- RNCAsyncStorage (2.1.2):
|
- RNCAsyncStorage (2.1.2):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNCMaskedView (0.3.2):
|
- RNCMaskedView (0.3.2):
|
||||||
|
|
@ -2436,6 +2457,9 @@ PODS:
|
||||||
- Sentry/HybridSDK (8.50.2)
|
- Sentry/HybridSDK (8.50.2)
|
||||||
- SocketRocket (0.7.1)
|
- SocketRocket (0.7.1)
|
||||||
- TOCropViewController (2.8.0)
|
- TOCropViewController (2.8.0)
|
||||||
|
- TSBackgroundFetch (4.0.6)
|
||||||
|
- TSLocationManager (4.0.9):
|
||||||
|
- TSBackgroundFetch (~> 4.0.6)
|
||||||
- UMAppLoader (5.1.3)
|
- UMAppLoader (5.1.3)
|
||||||
- Yoga (0.0.0)
|
- Yoga (0.0.0)
|
||||||
|
|
||||||
|
|
@ -2553,7 +2577,6 @@ DEPENDENCIES:
|
||||||
- ReactAppDependencyProvider (from `build/generated/ios`)
|
- ReactAppDependencyProvider (from `build/generated/ios`)
|
||||||
- ReactCodegen (from `build/generated/ios`)
|
- ReactCodegen (from `build/generated/ios`)
|
||||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||||
- RNBackgroundFetch (from `../node_modules/react-native-background-fetch`)
|
|
||||||
- RNBackgroundGeolocation (from `../node_modules/react-native-background-geolocation`)
|
- RNBackgroundGeolocation (from `../node_modules/react-native-background-geolocation`)
|
||||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
||||||
|
|
@ -2589,6 +2612,8 @@ SPEC REPOS:
|
||||||
- Sentry
|
- Sentry
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- TOCropViewController
|
- TOCropViewController
|
||||||
|
- TSBackgroundFetch
|
||||||
|
- TSLocationManager
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
boost:
|
boost:
|
||||||
|
|
@ -2814,8 +2839,6 @@ EXTERNAL SOURCES:
|
||||||
:path: build/generated/ios
|
:path: build/generated/ios
|
||||||
ReactCommon:
|
ReactCommon:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../node_modules/react-native/ReactCommon"
|
||||||
RNBackgroundFetch:
|
|
||||||
:path: "../node_modules/react-native-background-fetch"
|
|
||||||
RNBackgroundGeolocation:
|
RNBackgroundGeolocation:
|
||||||
:path: "../node_modules/react-native-background-geolocation"
|
:path: "../node_modules/react-native-background-geolocation"
|
||||||
RNCAsyncStorage:
|
RNCAsyncStorage:
|
||||||
|
|
@ -2972,8 +2995,7 @@ SPEC CHECKSUMS:
|
||||||
ReactAppDependencyProvider: ae0be24eb18014a031b4b220cb3973d07c3cbaf8
|
ReactAppDependencyProvider: ae0be24eb18014a031b4b220cb3973d07c3cbaf8
|
||||||
ReactCodegen: 3d5c9def468f7df3b90b05d3098ab8d47d76c62e
|
ReactCodegen: 3d5c9def468f7df3b90b05d3098ab8d47d76c62e
|
||||||
ReactCommon: 4c22936ee38ecf885efc95ac5832f430f27e0271
|
ReactCommon: 4c22936ee38ecf885efc95ac5832f430f27e0271
|
||||||
RNBackgroundFetch: e44c9e85d7fb3122c37d8a806278f62c7682d7ea
|
RNBackgroundGeolocation: a1a22b12ae92306d46150b9827cc1421ec03bd18
|
||||||
RNBackgroundGeolocation: 7e969292ffe21f6d757c2efbbb9cf58acb554dbd
|
|
||||||
RNCAsyncStorage: b9f5f78da5d16a853fe3dc22e8268d932fc45a83
|
RNCAsyncStorage: b9f5f78da5d16a853fe3dc22e8268d932fc45a83
|
||||||
RNCMaskedView: 473e5fa854913eff0eef18c09b1328e60b3bf3f7
|
RNCMaskedView: 473e5fa854913eff0eef18c09b1328e60b3bf3f7
|
||||||
RNFBApp: 8d27c7545e7e06d78974e204ff95f207a60a5cb2
|
RNFBApp: 8d27c7545e7e06d78974e204ff95f207a60a5cb2
|
||||||
|
|
@ -2989,9 +3011,11 @@ SPEC CHECKSUMS:
|
||||||
Sentry: d95f5f3b32d01324b3e27d3c52747005302cc026
|
Sentry: d95f5f3b32d01324b3e27d3c52747005302cc026
|
||||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||||
TOCropViewController: 797deaf39c90e6e9ddd848d88817f6b9a8a09888
|
TOCropViewController: 797deaf39c90e6e9ddd848d88817f6b9a8a09888
|
||||||
|
TSBackgroundFetch: 0afbeaae4e1132866e1d4b6e55265af26b5958ae
|
||||||
|
TSLocationManager: ba5648de9d7226abca875a06fbce7f50b3e9acf5
|
||||||
UMAppLoader: 55159b69750129faa7a51c493cb8ea55a7b64eb9
|
UMAppLoader: 55159b69750129faa7a51c493cb8ea55a7b64eb9
|
||||||
Yoga: b37eeaa6148984cac70ecaf8022bb209e03296cb
|
Yoga: b37eeaa6148984cac70ecaf8022bb209e03296cb
|
||||||
|
|
||||||
PODFILE CHECKSUM: b7512b313bae79574a1156b44cfc5fd6e73aeabf
|
PODFILE CHECKSUM: 5686979b0aa88a157955165df52d1be990eeb6e7
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,6 @@
|
||||||
"react-native": "0.79.6",
|
"react-native": "0.79.6",
|
||||||
"react-native-animatable": "^1.3.3",
|
"react-native-animatable": "^1.3.3",
|
||||||
"react-native-app-link": "^1.0.1",
|
"react-native-app-link": "^1.0.1",
|
||||||
"react-native-background-fetch": "^4.2.8",
|
|
||||||
"react-native-background-geolocation": "5.0.1",
|
"react-native-background-geolocation": "5.0.1",
|
||||||
"react-native-battery-optimization-check": "^1.0.8",
|
"react-native-battery-optimization-check": "^1.0.8",
|
||||||
"react-native-contact-pick": "^0.1.2",
|
"react-native-contact-pick": "^0.1.2",
|
||||||
|
|
|
||||||
27
plans/geolocation-periodic-uploads-investigation.md
Normal file
27
plans/geolocation-periodic-uploads-investigation.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Geolocation periodic uploads investigation
|
||||||
|
|
||||||
|
## Observations
|
||||||
|
|
||||||
|
From the app logs, we still see background IDLE uploads every ~5–10 minutes even when the device is stationary.
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
|
||||||
|
- The upload is happening with `sample: undefined` (ie, persisted location) and is followed by HTTP success.
|
||||||
|
- Motion state can flip to `isMovingState: true` even while `activity: still` and low `speed`.
|
||||||
|
- In at least one diagnostic snapshot, `stopOnStationary` appeared as `undefined`, suggesting the config either:
|
||||||
|
- is not being applied as expected, or
|
||||||
|
- is being overridden by another config surface, or
|
||||||
|
- is not exposed in `getState()` on that platform/version.
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
1. Confirm the currently running app build includes the latest config (`useSignificantChangesOnly`, `activity.stopOnStationary`).
|
||||||
|
2. Extract precise server-side event timing distribution from Explore logs to see whether bursts correlate to motionchange or other triggers.
|
||||||
|
3. If periodic uploads persist after significant-change mode:
|
||||||
|
- evaluate whether another mechanism triggers periodic sync (eg native retry/reconnect),
|
||||||
|
- consider disabling motion-activity updates in IDLE or increasing motion trigger delay,
|
||||||
|
- optionally introduce a server-side dedupe (ignore updates if within X meters and within Y minutes) as last-resort safety.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
The Explore log snippet currently contains only `action` and `timestamp` (no payload coordinates), so it can confirm frequency/bursts but not whether the same point is repeatedly uploaded.
|
||||||
|
|
@ -59,14 +59,23 @@ export const BASE_GEOLOCATION_CONFIG = {
|
||||||
// protect battery.
|
// protect battery.
|
||||||
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
||||||
|
|
||||||
// Default to the IDLE profile behaviour: we still want distance-based updates
|
// Default to the IDLE profile behaviour.
|
||||||
// even with no open alert (see TRACKING_PROFILES.idle).
|
|
||||||
distanceFilter: 200,
|
distanceFilter: 200,
|
||||||
|
|
||||||
|
// Prevent dynamic distanceFilter shrink.
|
||||||
|
// Elasticity can lower the effective threshold (eg while stationary), resulting in
|
||||||
|
// unexpected frequent updates.
|
||||||
|
disableElasticity: true,
|
||||||
|
|
||||||
// Stop-detection.
|
// Stop-detection.
|
||||||
// NOTE: historically we set this at top-level. In v5 the knob is under `geolocation`.
|
|
||||||
stopTimeout: 5,
|
stopTimeout: 5,
|
||||||
|
|
||||||
|
// True-stationary strategy: once stop-detection decides we're stationary, stop active
|
||||||
|
// tracking and rely on the stationary geofence to detect significant movement.
|
||||||
|
// This is intended to eliminate periodic stationary updates on Android.
|
||||||
|
stopOnStationary: true,
|
||||||
|
stationaryRadius: 200,
|
||||||
|
|
||||||
// Prevent identical/noise locations from being persisted.
|
// Prevent identical/noise locations from being persisted.
|
||||||
// This reduces DB churn and avoids triggering native HTTP uploads with redundant points.
|
// This reduces DB churn and avoids triggering native HTTP uploads with redundant points.
|
||||||
allowIdenticalLocations: false,
|
allowIdenticalLocations: false,
|
||||||
|
|
@ -130,7 +139,6 @@ export const BASE_GEOLOCATION_CONFIG = {
|
||||||
persistence: {
|
persistence: {
|
||||||
// Product requirement: keep only the latest geopoint.
|
// Product requirement: keep only the latest geopoint.
|
||||||
maxRecordsToPersist: 1,
|
maxRecordsToPersist: 1,
|
||||||
maxDaysToPersist: 1,
|
|
||||||
|
|
||||||
// Behavior tweaks
|
// Behavior tweaks
|
||||||
disableProviderChangeRecord: true,
|
disableProviderChangeRecord: true,
|
||||||
|
|
@ -156,7 +164,6 @@ export const BASE_GEOLOCATION_INVARIANTS = {
|
||||||
},
|
},
|
||||||
persistence: {
|
persistence: {
|
||||||
maxRecordsToPersist: 1,
|
maxRecordsToPersist: 1,
|
||||||
maxDaysToPersist: 1,
|
|
||||||
disableProviderChangeRecord: true,
|
disableProviderChangeRecord: true,
|
||||||
},
|
},
|
||||||
// NOTE: `speedJumpFilter` was a legacy Config knob; it is not part of v5 shared types.
|
// NOTE: `speedJumpFilter` was a legacy Config knob; it is not part of v5 shared types.
|
||||||
|
|
@ -167,15 +174,14 @@ export const BASE_GEOLOCATION_INVARIANTS = {
|
||||||
export const TRACKING_PROFILES = {
|
export const TRACKING_PROFILES = {
|
||||||
idle: {
|
idle: {
|
||||||
geolocation: {
|
geolocation: {
|
||||||
// Same rationale as BASE: prefer GPS-capable accuracy to avoid km-level coarse fixes
|
// IDLE runtime relies on the SDK's stop-detection + stationary geofence (stopOnStationary).
|
||||||
// that can trigger false motion/geofence transitions on Android.
|
// IMPORTANT: ACTIVE sets stopOnStationary:false.
|
||||||
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
// Ensure we restore it when transitioning back to IDLE, otherwise the SDK may
|
||||||
// Defensive: keep the distanceFilter conservative to avoid battery drain.
|
// continue recording while stationary.
|
||||||
distanceFilter: 200,
|
stopOnStationary: true,
|
||||||
|
|
||||||
// Keep filtering enabled across profile transitions.
|
// QA helper: allow easier validation in dev/staging while keeping production at 200m.
|
||||||
filter: DEFAULT_LOCATION_FILTER,
|
stationaryRadius: 200,
|
||||||
allowIdenticalLocations: false,
|
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
// Never use heartbeat-driven updates; only movement-driven.
|
// Never use heartbeat-driven updates; only movement-driven.
|
||||||
|
|
@ -183,21 +189,18 @@ export const TRACKING_PROFILES = {
|
||||||
},
|
},
|
||||||
activity: {
|
activity: {
|
||||||
// Android-only: reduce false-positive motion triggers due to screen-on/unlock.
|
// Android-only: reduce false-positive motion triggers due to screen-on/unlock.
|
||||||
// We keep Motion API enabled (battery-optimized) but add a large delay so brief
|
|
||||||
// activity-jitter cannot repeatedly toggle moving/stationary while the user is idle.
|
|
||||||
// (This is ignored on iOS.)
|
// (This is ignored on iOS.)
|
||||||
motionTriggerDelay: 300000,
|
motionTriggerDelay: 300000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
geolocation: {
|
geolocation: {
|
||||||
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
|
||||||
// ACTIVE target: frequent updates while moving.
|
// ACTIVE target: frequent updates while moving.
|
||||||
distanceFilter: 25,
|
distanceFilter: 25,
|
||||||
|
|
||||||
// Apply the same native filter while ACTIVE.
|
// While ACTIVE, do not stop updates simply because the device appears stationary.
|
||||||
filter: DEFAULT_LOCATION_FILTER,
|
// Motion-detection + distanceFilter should govern updates.
|
||||||
allowIdenticalLocations: false,
|
stopOnStationary: false,
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
// Never use heartbeat-driven updates; only movement-driven.
|
// Never use heartbeat-driven updates; only movement-driven.
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export function getLastReadyState() {
|
||||||
export function setBackgroundGeolocationEventHandlers({
|
export function setBackgroundGeolocationEventHandlers({
|
||||||
onLocation,
|
onLocation,
|
||||||
onLocationError,
|
onLocationError,
|
||||||
|
onGeofence,
|
||||||
onHttp,
|
onHttp,
|
||||||
onHeartbeat,
|
onHeartbeat,
|
||||||
onSchedule,
|
onSchedule,
|
||||||
|
|
@ -66,6 +67,7 @@ export function setBackgroundGeolocationEventHandlers({
|
||||||
// We use a simple signature so calling with identical functions is a no-op.
|
// We use a simple signature so calling with identical functions is a no-op.
|
||||||
const sig = [
|
const sig = [
|
||||||
onLocation ? "L1" : "L0",
|
onLocation ? "L1" : "L0",
|
||||||
|
onGeofence ? "G1" : "G0",
|
||||||
onHttp ? "H1" : "H0",
|
onHttp ? "H1" : "H0",
|
||||||
onHeartbeat ? "HB1" : "HB0",
|
onHeartbeat ? "HB1" : "HB0",
|
||||||
onSchedule ? "S1" : "S0",
|
onSchedule ? "S1" : "S0",
|
||||||
|
|
@ -87,6 +89,10 @@ export function setBackgroundGeolocationEventHandlers({
|
||||||
BackgroundGeolocation.onLocation(onLocation, onLocationError),
|
BackgroundGeolocation.onLocation(onLocation, onLocationError),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onGeofence) {
|
||||||
|
subscriptions.push(BackgroundGeolocation.onGeofence(onGeofence));
|
||||||
|
}
|
||||||
if (onHttp) {
|
if (onHttp) {
|
||||||
subscriptions.push(BackgroundGeolocation.onHttp(onHttp));
|
subscriptions.push(BackgroundGeolocation.onHttp(onHttp));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
132
src/location/buildBackgroundGeolocationSetConfigPayload.js
Normal file
132
src/location/buildBackgroundGeolocationSetConfigPayload.js
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BASE_GEOLOCATION_CONFIG,
|
||||||
|
BASE_GEOLOCATION_INVARIANTS,
|
||||||
|
} from "~/location/backgroundGeolocationConfig";
|
||||||
|
|
||||||
|
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
||||||
|
|
||||||
|
const isPlainObject = (value) =>
|
||||||
|
!!value && typeof value === "object" && !Array.isArray(value);
|
||||||
|
|
||||||
|
const mergeSection = (base, invariants, override) => ({
|
||||||
|
...(isPlainObject(base) ? base : {}),
|
||||||
|
...(isPlainObject(invariants) ? invariants : {}),
|
||||||
|
...(isPlainObject(override) ? override : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a deterministic `BackgroundGeolocation.setConfig()` payload.
|
||||||
|
*
|
||||||
|
* Goal: never rely on native deep-merge behavior for nested objects.
|
||||||
|
*
|
||||||
|
* Rules:
|
||||||
|
* - When a top-level section is touched (`geolocation|app|http|persistence`), send a complete
|
||||||
|
* section built from the base config + invariants + overrides.
|
||||||
|
* - Preserve existing runtime `http.headers` unless explicitly overridden.
|
||||||
|
* - `headers: {}` is treated as an explicit clear.
|
||||||
|
* - Preserve existing runtime `persistence.extras` unless explicitly overridden.
|
||||||
|
* - `extras: {}` is treated as an explicit clear.
|
||||||
|
*/
|
||||||
|
export default async function buildBackgroundGeolocationSetConfigPayload(
|
||||||
|
partialConfig = {},
|
||||||
|
) {
|
||||||
|
const partial = isPlainObject(partialConfig) ? partialConfig : {};
|
||||||
|
|
||||||
|
let state = null;
|
||||||
|
try {
|
||||||
|
state = await BackgroundGeolocation.getState();
|
||||||
|
} catch {
|
||||||
|
state = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevHttpHeaders = isPlainObject(state?.http?.headers)
|
||||||
|
? state.http.headers
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const prevPersistenceExtras = isPlainObject(state?.persistence?.extras)
|
||||||
|
? state.persistence.extras
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const payload = {};
|
||||||
|
|
||||||
|
if (isPlainObject(partial.geolocation)) {
|
||||||
|
payload.geolocation = mergeSection(
|
||||||
|
BASE_GEOLOCATION_CONFIG.geolocation,
|
||||||
|
null,
|
||||||
|
partial.geolocation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(partial.app)) {
|
||||||
|
payload.app = mergeSection(
|
||||||
|
BASE_GEOLOCATION_CONFIG.app,
|
||||||
|
BASE_GEOLOCATION_INVARIANTS.app,
|
||||||
|
partial.app,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(partial.http)) {
|
||||||
|
const http = mergeSection(
|
||||||
|
BASE_GEOLOCATION_CONFIG.http,
|
||||||
|
BASE_GEOLOCATION_INVARIANTS.http,
|
||||||
|
partial.http,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasOwn(partial.http, "headers")) {
|
||||||
|
const nextHeaders = isPlainObject(partial.http.headers)
|
||||||
|
? partial.http.headers
|
||||||
|
: {};
|
||||||
|
|
||||||
|
// Explicit reset: allow clearing headers (eg anonymous mode).
|
||||||
|
if (Object.keys(nextHeaders).length === 0) {
|
||||||
|
http.headers = {};
|
||||||
|
} else {
|
||||||
|
http.headers = { ...prevHttpHeaders, ...nextHeaders };
|
||||||
|
}
|
||||||
|
} else if (Object.keys(prevHttpHeaders).length > 0) {
|
||||||
|
// Preserve existing runtime headers (important when re-applying invariants).
|
||||||
|
http.headers = prevHttpHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.http = http;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(partial.persistence)) {
|
||||||
|
const persistence = mergeSection(
|
||||||
|
BASE_GEOLOCATION_CONFIG.persistence,
|
||||||
|
BASE_GEOLOCATION_INVARIANTS.persistence,
|
||||||
|
partial.persistence,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasOwn(partial.persistence, "extras")) {
|
||||||
|
const nextExtras = isPlainObject(partial.persistence.extras)
|
||||||
|
? partial.persistence.extras
|
||||||
|
: {};
|
||||||
|
|
||||||
|
// Explicit reset: allow clearing extras.
|
||||||
|
if (Object.keys(nextExtras).length === 0) {
|
||||||
|
persistence.extras = {};
|
||||||
|
} else {
|
||||||
|
persistence.extras = { ...prevPersistenceExtras, ...nextExtras };
|
||||||
|
}
|
||||||
|
} else if (Object.keys(prevPersistenceExtras).length > 0) {
|
||||||
|
// Preserve existing runtime extras (important when re-applying invariants).
|
||||||
|
persistence.extras = prevPersistenceExtras;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.persistence = persistence;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass-through any additional config sections (eg `activity` in tracking profiles).
|
||||||
|
for (const key of Object.keys(partial)) {
|
||||||
|
if (key === "geolocation") continue;
|
||||||
|
if (key === "app") continue;
|
||||||
|
if (key === "http") continue;
|
||||||
|
if (key === "persistence") continue;
|
||||||
|
payload[key] = partial[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,6 @@ import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
import { AppState } from "react-native";
|
import { AppState } from "react-native";
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
import jwtDecode from "jwt-decode";
|
|
||||||
import { initEmulatorMode } from "./emulatorService";
|
import { initEmulatorMode } from "./emulatorService";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,7 +15,7 @@ import {
|
||||||
} from "~/stores";
|
} from "~/stores";
|
||||||
|
|
||||||
import setLocationState from "~/location/setLocationState";
|
import setLocationState from "~/location/setLocationState";
|
||||||
import { storeLocation } from "~/location/storage";
|
import { getStoredLocation, storeLocation } from "~/location/storage";
|
||||||
|
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
|
|
||||||
|
|
@ -25,6 +24,7 @@ import {
|
||||||
BASE_GEOLOCATION_INVARIANTS,
|
BASE_GEOLOCATION_INVARIANTS,
|
||||||
TRACKING_PROFILES,
|
TRACKING_PROFILES,
|
||||||
} from "~/location/backgroundGeolocationConfig";
|
} from "~/location/backgroundGeolocationConfig";
|
||||||
|
import buildBackgroundGeolocationSetConfigPayload from "~/location/buildBackgroundGeolocationSetConfigPayload";
|
||||||
import {
|
import {
|
||||||
ensureBackgroundGeolocationReady,
|
ensureBackgroundGeolocationReady,
|
||||||
setBackgroundGeolocationEventHandlers,
|
setBackgroundGeolocationEventHandlers,
|
||||||
|
|
@ -32,6 +32,12 @@ import {
|
||||||
|
|
||||||
let trackLocationStartPromise = null;
|
let trackLocationStartPromise = null;
|
||||||
|
|
||||||
|
// Correlation ID to differentiate multiple JS runtimes (eg full `Updates.reloadAsync()`)
|
||||||
|
// from tree-level reloads (auth/account switch).
|
||||||
|
const TRACK_LOCATION_INSTANCE_ID = `${Date.now().toString(36)}-${Math.random()
|
||||||
|
.toString(16)
|
||||||
|
.slice(2, 8)}`;
|
||||||
|
|
||||||
export default function trackLocation() {
|
export default function trackLocation() {
|
||||||
if (trackLocationStartPromise) return trackLocationStartPromise;
|
if (trackLocationStartPromise) return trackLocationStartPromise;
|
||||||
|
|
||||||
|
|
@ -41,6 +47,11 @@ export default function trackLocation() {
|
||||||
feature: "tracking",
|
feature: "tracking",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
locationLogger.info("trackLocation() starting", {
|
||||||
|
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||||
|
appState: AppState.currentState,
|
||||||
|
});
|
||||||
|
|
||||||
let currentProfile = null;
|
let currentProfile = null;
|
||||||
let authReady = false;
|
let authReady = false;
|
||||||
let appState = AppState.currentState;
|
let appState = AppState.currentState;
|
||||||
|
|
@ -58,6 +69,160 @@ export default function trackLocation() {
|
||||||
const BAD_ACCURACY_THRESHOLD_M = 200;
|
const BAD_ACCURACY_THRESHOLD_M = 200;
|
||||||
const PERSISTED_ACCURACY_GATE_M = 100;
|
const PERSISTED_ACCURACY_GATE_M = 100;
|
||||||
|
|
||||||
|
// NOTE: IDLE previously experimented with `startGeofences()` + an app-managed exit geofence.
|
||||||
|
// That approach is now removed.
|
||||||
|
// Current design relies on the SDK's stop-detection + stationary geofence
|
||||||
|
// (`geolocation.stopOnStationary` + `geolocation.stationaryRadius`) because it is more
|
||||||
|
// reliable in background/locked scenarios.
|
||||||
|
|
||||||
|
// Fallback: if the OS fails to deliver geofence EXIT while the phone is locked, allow
|
||||||
|
// exactly one persisted fix when we get strong evidence of movement (motion+activity).
|
||||||
|
const IDLE_MOVEMENT_FALLBACK_COOLDOWN_MS = 15 * 60 * 1000;
|
||||||
|
let lastActivity = null;
|
||||||
|
let lastActivityConfidence = 0;
|
||||||
|
let lastIdleMovementFallbackAt = 0;
|
||||||
|
|
||||||
|
// Diagnostics fields retained so server-side correlation can continue to work.
|
||||||
|
// This is *not* a managed geofence anymore; it's a reference center for observability.
|
||||||
|
let lastEnsuredIdleReferenceAt = 0;
|
||||||
|
let lastIdleReferenceCenter = null;
|
||||||
|
let lastIdleReferenceCenterAccuracyM = null;
|
||||||
|
let lastIdleReferenceCenterTimestamp = null;
|
||||||
|
let lastIdleReferenceCenterSource = null;
|
||||||
|
|
||||||
|
// A) Safeguard: when entering IDLE, ensure we have a reasonably accurate and recent
|
||||||
|
// reference point. This does NOT persist/upload; it only updates our stored last-known
|
||||||
|
// location and tracking extras.
|
||||||
|
const IDLE_REFERENCE_TARGET_ACCURACY_M = 50;
|
||||||
|
const IDLE_REFERENCE_MAX_AGE_MS = 5 * 60 * 1000;
|
||||||
|
const ensureIdleReferenceFix = async () => {
|
||||||
|
try {
|
||||||
|
const stored = await getStoredLocation();
|
||||||
|
const storedCoords = stored?.coords;
|
||||||
|
const storedAcc =
|
||||||
|
typeof storedCoords?.accuracy === "number"
|
||||||
|
? storedCoords.accuracy
|
||||||
|
: null;
|
||||||
|
const storedTs = stored?.timestamp;
|
||||||
|
const storedAgeMs = storedTs
|
||||||
|
? Date.now() - new Date(storedTs).getTime()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const isRecentEnough =
|
||||||
|
typeof storedAgeMs === "number" && storedAgeMs >= 0
|
||||||
|
? storedAgeMs <= IDLE_REFERENCE_MAX_AGE_MS
|
||||||
|
: false;
|
||||||
|
const isAccurateEnough =
|
||||||
|
typeof storedAcc === "number"
|
||||||
|
? storedAcc <= IDLE_REFERENCE_TARGET_ACCURACY_M
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
storedCoords?.latitude &&
|
||||||
|
storedCoords?.longitude &&
|
||||||
|
isRecentEnough &&
|
||||||
|
isAccurateEnough
|
||||||
|
) {
|
||||||
|
lastIdleReferenceCenter = {
|
||||||
|
latitude: storedCoords.latitude,
|
||||||
|
longitude: storedCoords.longitude,
|
||||||
|
};
|
||||||
|
lastIdleReferenceCenterAccuracyM = storedAcc;
|
||||||
|
lastIdleReferenceCenterTimestamp = storedTs ?? null;
|
||||||
|
lastIdleReferenceCenterSource = "stored";
|
||||||
|
lastEnsuredIdleReferenceAt = Date.now();
|
||||||
|
void updateTrackingContextExtras("idle_reference_ok");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fix = await getCurrentPositionWithDiagnostics(
|
||||||
|
{
|
||||||
|
samples: 2,
|
||||||
|
timeout: 30,
|
||||||
|
maximumAge: 0,
|
||||||
|
desiredAccuracy: IDLE_REFERENCE_TARGET_ACCURACY_M,
|
||||||
|
extras: {
|
||||||
|
idle_reference_fix: true,
|
||||||
|
idle_ref_prev_acc: storedAcc,
|
||||||
|
idle_ref_prev_age_ms: storedAgeMs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ reason: "idle_reference_fix", persist: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fix?.coords?.latitude && fix?.coords?.longitude) {
|
||||||
|
storeLocation(fix.coords, fix.timestamp);
|
||||||
|
lastIdleReferenceCenter = {
|
||||||
|
latitude: fix.coords.latitude,
|
||||||
|
longitude: fix.coords.longitude,
|
||||||
|
};
|
||||||
|
lastIdleReferenceCenterAccuracyM =
|
||||||
|
typeof fix.coords.accuracy === "number"
|
||||||
|
? fix.coords.accuracy
|
||||||
|
: null;
|
||||||
|
lastIdleReferenceCenterTimestamp = fix.timestamp ?? null;
|
||||||
|
lastIdleReferenceCenterSource = "idle_reference_fix";
|
||||||
|
lastEnsuredIdleReferenceAt = Date.now();
|
||||||
|
void updateTrackingContextExtras("idle_reference_fixed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.debug("Failed to ensure IDLE reference fix", {
|
||||||
|
error: e?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const maybeRequestIdleMovementFallbackFix = async (trigger) => {
|
||||||
|
if (currentProfile !== "idle" || !authReady) return;
|
||||||
|
if (
|
||||||
|
Date.now() - lastIdleMovementFallbackAt <
|
||||||
|
IDLE_MOVEMENT_FALLBACK_COOLDOWN_MS
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 2: primary trigger is `onMotionChange(isMoving:true)`.
|
||||||
|
// Keep `onActivityChange` as a secondary signal (lower confidence threshold).
|
||||||
|
const movingActivities = new Set([
|
||||||
|
"walking",
|
||||||
|
"running",
|
||||||
|
"on_foot",
|
||||||
|
"in_vehicle",
|
||||||
|
"cycling",
|
||||||
|
]);
|
||||||
|
const hasSomeActivitySignal =
|
||||||
|
movingActivities.has(lastActivity) && lastActivityConfidence >= 50;
|
||||||
|
|
||||||
|
if (trigger === "activitychange" && !hasSomeActivitySignal) return;
|
||||||
|
|
||||||
|
lastIdleMovementFallbackAt = Date.now();
|
||||||
|
locationLogger.info("IDLE movement fallback fix", {
|
||||||
|
trigger,
|
||||||
|
lastActivity,
|
||||||
|
lastActivityConfidence,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getCurrentPositionWithDiagnostics(
|
||||||
|
{
|
||||||
|
samples: 2,
|
||||||
|
timeout: 30,
|
||||||
|
maximumAge: 0,
|
||||||
|
desiredAccuracy: 50,
|
||||||
|
extras: {
|
||||||
|
idle_movement_fallback: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ reason: `idle_movement_fallback:${trigger}`, persist: true },
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.warn("IDLE movement fallback fix failed", {
|
||||||
|
trigger,
|
||||||
|
error: e?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const shouldUseLocationForUi = (location) => {
|
const shouldUseLocationForUi = (location) => {
|
||||||
const acc = location?.coords?.accuracy;
|
const acc = location?.coords?.accuracy;
|
||||||
return !(typeof acc === "number" && acc > BAD_ACCURACY_THRESHOLD_M);
|
return !(typeof acc === "number" && acc > BAD_ACCURACY_THRESHOLD_M);
|
||||||
|
|
@ -106,7 +271,7 @@ export default function trackLocation() {
|
||||||
const updateTrackingContextExtras = async (reason) => {
|
const updateTrackingContextExtras = async (reason) => {
|
||||||
try {
|
try {
|
||||||
const { userId } = getSessionState();
|
const { userId } = getSessionState();
|
||||||
await BackgroundGeolocation.setConfig({
|
const payload = await buildBackgroundGeolocationSetConfigPayload({
|
||||||
persistence: {
|
persistence: {
|
||||||
extras: {
|
extras: {
|
||||||
tracking_ctx: {
|
tracking_ctx: {
|
||||||
|
|
@ -115,11 +280,21 @@ export default function trackLocation() {
|
||||||
profile: currentProfile,
|
profile: currentProfile,
|
||||||
auth_ready: authReady,
|
auth_ready: authReady,
|
||||||
session_user_id: userId || null,
|
session_user_id: userId || null,
|
||||||
|
// Diagnostics: helps correlate server-side "no update" reports with
|
||||||
|
// the last known good reference center when entering IDLE.
|
||||||
|
idle_reference: {
|
||||||
|
center: lastIdleReferenceCenter,
|
||||||
|
center_accuracy_m: lastIdleReferenceCenterAccuracyM,
|
||||||
|
center_timestamp: lastIdleReferenceCenterTimestamp,
|
||||||
|
center_source: lastIdleReferenceCenterSource,
|
||||||
|
ensured_at: lastEnsuredIdleReferenceAt || null,
|
||||||
|
},
|
||||||
at: new Date().toISOString(),
|
at: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await BackgroundGeolocation.setConfig(payload);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Non-fatal: extras are only for observability/debugging.
|
// Non-fatal: extras are only for observability/debugging.
|
||||||
locationLogger.debug("Failed to update BGGeo tracking extras", {
|
locationLogger.debug("Failed to update BGGeo tracking extras", {
|
||||||
|
|
@ -433,10 +608,80 @@ export default function trackLocation() {
|
||||||
// We only apply profile once auth headers are configured.
|
// We only apply profile once auth headers are configured.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentProfile === profileName) return;
|
|
||||||
|
// IMPORTANT:
|
||||||
|
// Do not assume the native SDK runtime mode still matches our JS `currentProfile`.
|
||||||
|
// During identity switch / tree reload, we can remain in the same logical profile
|
||||||
|
// while native state drifts (eg `trackingMode` remains geofence-only but geofences
|
||||||
|
// are missing, leading to "moving but no updates").
|
||||||
|
//
|
||||||
|
// If profile is unchanged, perform a lightweight runtime ensure.
|
||||||
|
// We no longer use geofence-only tracking; ensure we are not stuck in it.
|
||||||
|
if (currentProfile === profileName) {
|
||||||
|
try {
|
||||||
|
const state = await BackgroundGeolocation.getState();
|
||||||
|
|
||||||
|
if (profileName === "idle") {
|
||||||
|
// Ensure we are not stuck in geofence-only mode.
|
||||||
|
if (state?.trackingMode === 0) {
|
||||||
|
await BackgroundGeolocation.start();
|
||||||
|
}
|
||||||
|
locationLogger.debug("Profile unchanged; IDLE runtime ensured", {
|
||||||
|
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||||
|
trackingMode: state?.trackingMode,
|
||||||
|
enabled: state?.enabled,
|
||||||
|
});
|
||||||
|
void ensureIdleReferenceFix();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileName === "active") {
|
||||||
|
// If we previously called `startGeofences()`, the SDK can remain in geofence-only
|
||||||
|
// mode until we explicitly call `start()` again.
|
||||||
|
if (state?.trackingMode === 0) {
|
||||||
|
await BackgroundGeolocation.start();
|
||||||
|
}
|
||||||
|
locationLogger.debug("Profile unchanged; ACTIVE runtime ensured", {
|
||||||
|
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||||
|
trackingMode: state?.trackingMode,
|
||||||
|
enabled: state?.enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.debug(
|
||||||
|
"Failed to ensure runtime for unchanged profile",
|
||||||
|
{
|
||||||
|
profileName,
|
||||||
|
error: e?.message,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const applyStartedAt = Date.now();
|
const applyStartedAt = Date.now();
|
||||||
|
|
||||||
|
// Diagnostic snapshot (debug only) to help understand trackingMode transitions.
|
||||||
|
let preState = null;
|
||||||
|
try {
|
||||||
|
preState = await BackgroundGeolocation.getState();
|
||||||
|
locationLogger.debug("Applying tracking profile (pre-state)", {
|
||||||
|
profileName,
|
||||||
|
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||||
|
enabled: preState?.enabled,
|
||||||
|
isMoving: preState?.isMoving,
|
||||||
|
trackingMode: preState?.trackingMode,
|
||||||
|
distanceFilter: preState?.geolocation?.distanceFilter,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.debug(
|
||||||
|
"Failed to read BGGeo state before profile apply",
|
||||||
|
{
|
||||||
|
profileName,
|
||||||
|
error: e?.message,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const profile = TRACKING_PROFILES[profileName];
|
const profile = TRACKING_PROFILES[profileName];
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
locationLogger.warn("Unknown tracking profile", { profileName });
|
locationLogger.warn("Unknown tracking profile", { profileName });
|
||||||
|
|
@ -451,7 +696,10 @@ export default function trackLocation() {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await BackgroundGeolocation.setConfig(profile);
|
const payload = await buildBackgroundGeolocationSetConfigPayload(
|
||||||
|
profile,
|
||||||
|
);
|
||||||
|
await BackgroundGeolocation.setConfig(payload);
|
||||||
|
|
||||||
// Motion state strategy:
|
// Motion state strategy:
|
||||||
// - ACTIVE: force moving to begin aggressive tracking immediately.
|
// - ACTIVE: force moving to begin aggressive tracking immediately.
|
||||||
|
|
@ -462,6 +710,15 @@ export default function trackLocation() {
|
||||||
// transitions so we still get distance-based updates when the user truly moves.
|
// transitions so we still get distance-based updates when the user truly moves.
|
||||||
if (profileName === "active") {
|
if (profileName === "active") {
|
||||||
const state = await BackgroundGeolocation.getState();
|
const state = await BackgroundGeolocation.getState();
|
||||||
|
|
||||||
|
// If we were previously in geofence-only mode, switch back to standard tracking.
|
||||||
|
// Without this, calling `changePace(true)` is not sufficient on some devices,
|
||||||
|
// and the SDK can stay in `trackingMode: 0` (geofence-only), producing no
|
||||||
|
// distance-based updates while moving.
|
||||||
|
if (state?.trackingMode === 0) {
|
||||||
|
await BackgroundGeolocation.start();
|
||||||
|
}
|
||||||
|
|
||||||
if (!state?.isMoving) {
|
if (!state?.isMoving) {
|
||||||
await BackgroundGeolocation.changePace(true);
|
await BackgroundGeolocation.changePace(true);
|
||||||
}
|
}
|
||||||
|
|
@ -500,6 +757,9 @@ export default function trackLocation() {
|
||||||
longitude: fix?.coords?.longitude,
|
longitude: fix?.coords?.longitude,
|
||||||
timestamp: fix?.timestamp,
|
timestamp: fix?.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Prevent duplicated "moving-edge" persisted fix right after entering ACTIVE.
|
||||||
|
lastMovingEdgeAt = Date.now();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
locationLogger.warn("ACTIVE immediate fix failed", {
|
locationLogger.warn("ACTIVE immediate fix failed", {
|
||||||
error: error?.message,
|
error: error?.message,
|
||||||
|
|
@ -519,6 +779,41 @@ export default function trackLocation() {
|
||||||
// Update extras for observability (profile transitions are a key lifecycle change).
|
// Update extras for observability (profile transitions are a key lifecycle change).
|
||||||
updateTrackingContextExtras(`profile:${profileName}`);
|
updateTrackingContextExtras(`profile:${profileName}`);
|
||||||
|
|
||||||
|
// For IDLE, ensure we are NOT in geofence-only tracking mode.
|
||||||
|
if (profileName === "idle") {
|
||||||
|
try {
|
||||||
|
const s = await BackgroundGeolocation.getState();
|
||||||
|
if (s?.trackingMode === 0) {
|
||||||
|
await BackgroundGeolocation.start();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
void ensureIdleReferenceFix();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-state snapshot (debug) to detect unintended geofence-only mode.
|
||||||
|
try {
|
||||||
|
const post = await BackgroundGeolocation.getState();
|
||||||
|
locationLogger.debug("Tracking profile applied (post-state)", {
|
||||||
|
profileName,
|
||||||
|
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||||
|
enabled: post?.enabled,
|
||||||
|
isMoving: post?.isMoving,
|
||||||
|
trackingMode: post?.trackingMode,
|
||||||
|
distanceFilter: post?.geolocation?.distanceFilter,
|
||||||
|
prevTrackingMode: preState?.trackingMode ?? null,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.debug(
|
||||||
|
"Failed to read BGGeo state after profile apply",
|
||||||
|
{
|
||||||
|
profileName,
|
||||||
|
error: e?.message,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const state = await BackgroundGeolocation.getState();
|
const state = await BackgroundGeolocation.getState();
|
||||||
locationLogger.info("Tracking profile applied", {
|
locationLogger.info("Tracking profile applied", {
|
||||||
|
|
@ -557,8 +852,26 @@ export default function trackLocation() {
|
||||||
|
|
||||||
locationLogger.info("Handling auth token update", {
|
locationLogger.info("Handling auth token update", {
|
||||||
hasToken: !!userToken,
|
hasToken: !!userToken,
|
||||||
|
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Snapshot state early (debug only) to diagnose "no uploads" reports after auth refresh.
|
||||||
|
if (__DEV__ || env.IS_STAGING) {
|
||||||
|
try {
|
||||||
|
const s = await BackgroundGeolocation.getState();
|
||||||
|
locationLogger.debug("Auth-change BGGeo state snapshot", {
|
||||||
|
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||||
|
enabled: s?.enabled,
|
||||||
|
isMoving: s?.isMoving,
|
||||||
|
trackingMode: s?.trackingMode,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.debug("Auth-change BGGeo state snapshot failed", {
|
||||||
|
error: e?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compute identity from session store; this is our source of truth.
|
// Compute identity from session store; this is our source of truth.
|
||||||
// (A token refresh for the same user should not force a new persisted fix.)
|
// (A token refresh for the same user should not force a new persisted fix.)
|
||||||
let currentSessionUserId = null;
|
let currentSessionUserId = null;
|
||||||
|
|
@ -576,13 +889,14 @@ export default function trackLocation() {
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await BackgroundGeolocation.setConfig({
|
const payload = await buildBackgroundGeolocationSetConfigPayload({
|
||||||
http: {
|
http: {
|
||||||
url: "",
|
url: "",
|
||||||
autoSync: false,
|
autoSync: false,
|
||||||
headers: {},
|
headers: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await BackgroundGeolocation.setConfig(payload);
|
||||||
didDisableUploadsForAnonymous = true;
|
didDisableUploadsForAnonymous = true;
|
||||||
didSyncAfterAuth = false;
|
didSyncAfterAuth = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -638,9 +952,9 @@ export default function trackLocation() {
|
||||||
lastSessionUserId = null;
|
lastSessionUserId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// unsub();
|
|
||||||
locationLogger.debug("Updating background geolocation config");
|
locationLogger.debug("Updating background geolocation config");
|
||||||
await BackgroundGeolocation.setConfig({
|
{
|
||||||
|
const payload = await buildBackgroundGeolocationSetConfigPayload({
|
||||||
http: {
|
http: {
|
||||||
// Update the sync URL for when it's changed for staging
|
// Update the sync URL for when it's changed for staging
|
||||||
url: env.GEOLOC_SYNC_URL,
|
url: env.GEOLOC_SYNC_URL,
|
||||||
|
|
@ -654,6 +968,8 @@ export default function trackLocation() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await BackgroundGeolocation.setConfig(payload);
|
||||||
|
}
|
||||||
|
|
||||||
authReady = true;
|
authReady = true;
|
||||||
|
|
||||||
|
|
@ -669,14 +985,6 @@ export default function trackLocation() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const state = await BackgroundGeolocation.getState();
|
const state = await BackgroundGeolocation.getState();
|
||||||
try {
|
|
||||||
const decodedToken = jwtDecode(userToken);
|
|
||||||
locationLogger.debug("Decoded JWT token", { decodedToken });
|
|
||||||
} catch (error) {
|
|
||||||
locationLogger.error("Failed to decode JWT token", {
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.enabled) {
|
if (!state.enabled) {
|
||||||
locationLogger.info("Starting location tracking");
|
locationLogger.info("Starting location tracking");
|
||||||
|
|
@ -772,12 +1080,14 @@ export default function trackLocation() {
|
||||||
setBackgroundGeolocationEventHandlers({
|
setBackgroundGeolocationEventHandlers({
|
||||||
onLocation: async (location) => {
|
onLocation: async (location) => {
|
||||||
locationLogger.debug("Location update received", {
|
locationLogger.debug("Location update received", {
|
||||||
coords: location.coords,
|
uuid: location?.uuid,
|
||||||
timestamp: location.timestamp,
|
sample: location?.sample,
|
||||||
activity: location.activity,
|
accuracy: location?.coords?.accuracy,
|
||||||
battery: location.battery,
|
latitude: location?.coords?.latitude,
|
||||||
sample: location.sample,
|
longitude: location?.coords?.longitude,
|
||||||
extras: location.extras,
|
timestamp: location?.timestamp,
|
||||||
|
activity: location?.activity,
|
||||||
|
extras: location?.extras,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ignore sampling locations (eg, emitted during getCurrentPosition) to avoid UI/storage churn.
|
// Ignore sampling locations (eg, emitted during getCurrentPosition) to avoid UI/storage churn.
|
||||||
|
|
@ -802,6 +1112,22 @@ export default function trackLocation() {
|
||||||
setLocationState(location.coords);
|
setLocationState(location.coords);
|
||||||
// Also store in AsyncStorage for last known location fallback
|
// Also store in AsyncStorage for last known location fallback
|
||||||
storeLocation(location.coords, location.timestamp);
|
storeLocation(location.coords, location.timestamp);
|
||||||
|
|
||||||
|
// If we're IDLE, update reference center for later correlation.
|
||||||
|
if (currentProfile === "idle") {
|
||||||
|
lastIdleReferenceCenter = {
|
||||||
|
latitude: location.coords.latitude,
|
||||||
|
longitude: location.coords.longitude,
|
||||||
|
};
|
||||||
|
lastIdleReferenceCenterAccuracyM =
|
||||||
|
typeof location.coords.accuracy === "number"
|
||||||
|
? location.coords.accuracy
|
||||||
|
: null;
|
||||||
|
lastIdleReferenceCenterTimestamp = location.timestamp ?? null;
|
||||||
|
lastIdleReferenceCenterSource = "onLocation";
|
||||||
|
lastEnsuredIdleReferenceAt = Date.now();
|
||||||
|
void updateTrackingContextExtras("idle_reference_updated");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLocationError: (error) => {
|
onLocationError: (error) => {
|
||||||
|
|
@ -818,84 +1144,43 @@ export default function trackLocation() {
|
||||||
responseText: response?.responseText,
|
responseText: response?.responseText,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Instrumentation: when we see periodic HTTP without a corresponding location event,
|
// Lightweight instrumentation only when useful:
|
||||||
// we want to know if BGGeo is retrying an upload queue or flushing new records.
|
// - non-success responses
|
||||||
// This helps diagnose reports like "server receives updates every ~5 minutes while stationary".
|
// - dev/staging visibility
|
||||||
|
const shouldInstrumentHttp =
|
||||||
|
!response?.success || __DEV__ || env.IS_STAGING;
|
||||||
|
if (!shouldInstrumentHttp) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [state, count] = await Promise.all([
|
const [state, count] = await Promise.all([
|
||||||
BackgroundGeolocation.getState(),
|
BackgroundGeolocation.getState(),
|
||||||
BackgroundGeolocation.getCount(),
|
BackgroundGeolocation.getCount(),
|
||||||
]);
|
]);
|
||||||
locationLogger.debug("HTTP instrumentation", {
|
locationLogger.debug("HTTP instrumentation", {
|
||||||
|
success: response?.success,
|
||||||
|
status: response?.status,
|
||||||
enabled: state?.enabled,
|
enabled: state?.enabled,
|
||||||
isMoving: state?.isMoving,
|
isMoving: state?.isMoving,
|
||||||
trackingMode: state?.trackingMode,
|
trackingMode: state?.trackingMode,
|
||||||
schedulerEnabled: state?.schedulerEnabled,
|
|
||||||
pendingCount: count,
|
pendingCount: count,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
locationLogger.warn("Failed HTTP instrumentation", {
|
locationLogger.debug("Failed HTTP instrumentation", {
|
||||||
error: e?.message,
|
error: e?.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHeartbeat: (event) => {
|
|
||||||
// If heartbeat is configured, it can trigger sync attempts even without new locations.
|
|
||||||
locationLogger.info("Heartbeat", {
|
|
||||||
enabled: event?.state?.enabled,
|
|
||||||
isMoving: event?.state?.isMoving,
|
|
||||||
location: event?.location?.coords,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSchedule: (event) => {
|
|
||||||
locationLogger.info("Schedule", {
|
|
||||||
state: event?.state,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onMotionChange: (event) => {
|
onMotionChange: (event) => {
|
||||||
// Diagnostic snapshot to understand periodic motion-change loops (eg Android ~5min).
|
// Essential motion diagnostics (avoid spam; keep it one log per edge).
|
||||||
// Keep it cheap: avoid heavy calls unless motion-change fires.
|
|
||||||
// NOTE: This is safe to run in background because it does not request a new location.
|
|
||||||
locationLogger.info("Motion change", {
|
locationLogger.info("Motion change", {
|
||||||
isMoving: event?.isMoving,
|
instanceId: TRACK_LOCATION_INSTANCE_ID,
|
||||||
location: event?.location?.coords,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Async snapshot of BGGeo internal state/config at the time of motion-change.
|
|
||||||
// This helps correlate native behavior with our current profile + config.
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const state = await BackgroundGeolocation.getState();
|
|
||||||
|
|
||||||
locationLogger.info("Motion change diagnostic", {
|
|
||||||
isMoving: event?.isMoving,
|
|
||||||
appState: appState,
|
|
||||||
profile: currentProfile,
|
profile: currentProfile,
|
||||||
|
appState,
|
||||||
authReady,
|
authReady,
|
||||||
// Time correlation
|
isMoving: event?.isMoving,
|
||||||
at: new Date().toISOString(),
|
|
||||||
// Core BGGeo runtime state
|
|
||||||
enabled: state?.enabled,
|
|
||||||
trackingMode: state?.trackingMode,
|
|
||||||
isMovingState: state?.isMoving,
|
|
||||||
schedulerEnabled: state?.schedulerEnabled,
|
|
||||||
// Critical config knobs related to periodic updates
|
|
||||||
distanceFilter: state?.geolocation?.distanceFilter,
|
|
||||||
heartbeatInterval: state?.app?.heartbeatInterval,
|
|
||||||
motionTriggerDelay: state?.activity?.motionTriggerDelay,
|
|
||||||
disableMotionActivityUpdates:
|
|
||||||
state?.activity?.disableMotionActivityUpdates,
|
|
||||||
stopTimeout: state?.geolocation?.stopTimeout,
|
|
||||||
// Location quality signal
|
|
||||||
accuracy: event?.location?.coords?.accuracy,
|
accuracy: event?.location?.coords?.accuracy,
|
||||||
speed: event?.location?.coords?.speed,
|
speed: event?.location?.coords?.speed,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
locationLogger.warn("Motion change diagnostic failed", {
|
|
||||||
error: e?.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Moving-edge strategy: when we enter moving state, force one persisted high-quality
|
// Moving-edge strategy: when we enter moving state, force one persisted high-quality
|
||||||
// point + sync so the server gets a quick update.
|
// point + sync so the server gets a quick update.
|
||||||
|
|
@ -948,12 +1233,25 @@ export default function trackLocation() {
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IDLE fallback: if we get a real motion transition while locked but geofence EXIT
|
||||||
|
// is not delivered reliably, request one persisted fix (gated + cooled down).
|
||||||
|
if (event?.isMoving && currentProfile === "idle" && authReady) {
|
||||||
|
void maybeRequestIdleMovementFallbackFix("motionchange");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onActivityChange: (event) => {
|
onActivityChange: (event) => {
|
||||||
locationLogger.info("Activity change", {
|
locationLogger.debug("Activity change", {
|
||||||
activity: event?.activity,
|
activity: event?.activity,
|
||||||
confidence: event?.confidence,
|
confidence: event?.confidence,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
lastActivity = event?.activity;
|
||||||
|
lastActivityConfidence = event?.confidence ?? 0;
|
||||||
|
|
||||||
|
if (currentProfile === "idle" && authReady) {
|
||||||
|
void maybeRequestIdleMovementFallbackFix("activitychange");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onProviderChange: (event) => {
|
onProviderChange: (event) => {
|
||||||
locationLogger.info("Provider change", {
|
locationLogger.info("Provider change", {
|
||||||
|
|
@ -965,7 +1263,7 @@ export default function trackLocation() {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onConnectivityChange: (event) => {
|
onConnectivityChange: (event) => {
|
||||||
locationLogger.info("Connectivity change", {
|
locationLogger.debug("Connectivity change", {
|
||||||
connected: event?.connected,
|
connected: event?.connected,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -999,7 +1297,10 @@ export default function trackLocation() {
|
||||||
// Ensure critical config cannot drift due to persisted plugin state.
|
// Ensure critical config cannot drift due to persisted plugin state.
|
||||||
// (We intentionally keep auth headers separate and set them in handleAuth.)
|
// (We intentionally keep auth headers separate and set them in handleAuth.)
|
||||||
try {
|
try {
|
||||||
await BackgroundGeolocation.setConfig(BASE_GEOLOCATION_INVARIANTS);
|
const payload = await buildBackgroundGeolocationSetConfigPayload(
|
||||||
|
BASE_GEOLOCATION_INVARIANTS,
|
||||||
|
);
|
||||||
|
await BackgroundGeolocation.setConfig(payload);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
locationLogger.warn("Failed to apply BGGeo base invariants", {
|
locationLogger.warn("Failed to apply BGGeo base invariants", {
|
||||||
error: e?.message,
|
error: e?.message,
|
||||||
|
|
|
||||||
|
|
@ -7140,7 +7140,6 @@ __metadata:
|
||||||
react-native: "npm:0.79.6"
|
react-native: "npm:0.79.6"
|
||||||
react-native-animatable: "npm:^1.3.3"
|
react-native-animatable: "npm:^1.3.3"
|
||||||
react-native-app-link: "npm:^1.0.1"
|
react-native-app-link: "npm:^1.0.1"
|
||||||
react-native-background-fetch: "npm:^4.2.8"
|
|
||||||
react-native-background-geolocation: "npm:5.0.1"
|
react-native-background-geolocation: "npm:5.0.1"
|
||||||
react-native-battery-optimization-check: "npm:^1.0.8"
|
react-native-battery-optimization-check: "npm:^1.0.8"
|
||||||
react-native-clean-project: "npm:^4.0.3"
|
react-native-clean-project: "npm:^4.0.3"
|
||||||
|
|
@ -16720,13 +16719,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-native-background-fetch@npm:^4.2.8":
|
|
||||||
version: 4.2.8
|
|
||||||
resolution: "react-native-background-fetch@npm:4.2.8"
|
|
||||||
checksum: 10/0950fbc553bca05d424591fbea7ffe534b51068a66497a0cac36aee7341c3aac9ec06932164572753a0a1b8c0c0f5d8d8df01a34b5288c81cdf4db555cf5024d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-native-background-geolocation@npm:5.0.1":
|
"react-native-background-geolocation@npm:5.0.1":
|
||||||
version: 5.0.1
|
version: 5.0.1
|
||||||
resolution: "react-native-background-geolocation@npm:5.0.1"
|
resolution: "react-native-background-geolocation@npm:5.0.1"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue