Skip to content

[Help Wanted]: Tracking pauses mid-trip, timeout starts, and never resumes on movement (both iOS and Android) #2644

Description

@kkostadinov-wannabe

Required Reading

  • Confirmed

Plugin Version

"react-native-background-fetch": "4.4.2", "react-native-background-geolocation": "5.3.0",

Mobile operating-system(s)

  • iOS
  • Android

Device Manufacturer(s) and Model(s)

Iphone 15 Pro Max, Galaxy A16

Device operating-systems(s)

iOS 26.6, Android 16

React Native / Expo version

"react-native": "0.81.5", "expo": "54.0.33", Bare workflow — native projects generated by expo prebuild with a config plugin. New Architecture enabled (RN 0.81 default).

What do you require assistance about?

On both iOS and Android, location updates stop arriving in the middle of an active trip. They correctly engage the stop-timeout while stationary, while getState() still reports enabled: true. Once this happens, movement does not bring updates back: neither motion-activity transitions nor several hundred metres of travel reliably resume delivery. Updates typically return only much later — in our recordings, when the user has been walking again for a sustained period.

Our use case is recording continuous urban travel traces (walking + tram/bus/metro + car), so a multi-minute hole in the middle of a trip is data loss for us, not a battery optimisation. The pattern is especially damaging around public transport: the user walks (tracked fine), waits a few minutes at a stop, boards — and the entire vehicle leg is missing.

We would like help identifying what stops delivery, and what the correct configuration is for "never lose location during an active trip, but still allow the device to park after a long genuine stop" on both platforms.

Recorded evidence

The following is the plugin's own recorded stream (via getLocations()) from an iOS device.
The full JSON is attached. A comparable pattern is observed on Android; we can supply an
Android recording as well if useful.

Trip start — fixes arrive every 7–10 s as expected:

Time (UTC) lat, lon accuracy speed activity
13:38:54 42.673805, 23.330248 15.1 m on_foot 100%
13:40:34 42.674315, 23.329867 3.5 m 1.14 on_foot 100%
13:41:27 42.674887, 23.330046 2.5 m 1.53 on_foot 100%
13:42:25 42.674543, 23.329105 2.1 m 1.28 on_foot 100%
13:42:34 42.674509, 23.328986 2.1 m 0.84 on_foot 100%

→ Location delivery stops here, 3 min 39 s after the trip began.

Gap: 13:42:34 → 13:47:58 — 5 min 25 s with zero locations. During the gap the user waited
briefly at a tram stop, boarded, and rode one stop. Straight-line displacement across the gap is
370 m (42.674509,23.328986 → 42.675961,23.333060); the travelled path is longer.

Time (UTC) lat, lon accuracy speed activity
13:47:58 42.675961, 23.333060 4.9 m 1.0 on_foot 100%
13:48:08 42.675927, 23.332914 3.8 m 1.29 on_foot 100%
13:48:18 42.675866, 23.332804 3.2 m 1.14 on_foot 100%

Delivery resumes only once the user is walking again.

What is puzzling about this

  1. Delivery stopped 3 min 39 s into an active trip, far short of our stopTimeout: 15
    minutes. We do not understand what, other than stopTimeout, stops delivery this early.
  2. The motion API reported on_foot at 100 % confidence throughout — both before and after
    the gap. The device was never classified still, even at the moment delivery stopped.
  3. 370 m of displacement during the gap did not produce a motionchange. Given the
    documented ~200 m stationary-exit behaviour on iOS, we expected the stationary region to be
    exited during that leg.
  4. It happens on both platforms, which rules out the hypothesis (iOS
    pausesLocationUpdatesAutomatically) as the whole story — see below.

What we have already tried

  • iOS: ran with pausesLocationUpdatesAutomatically left at its default, then explicitly set to false. The symptom occurs in both cases. (We understand false implicitly sets disableStopDetection: true and disables preventSuspend; we compensate by parking GPS ourselves with changePace(false) after our own 15-minute idle watchdog.)
  • iOS: tried activityType: Other and ActivityType.OtherNavigation. No improvement observed with either.
  • Both platforms — app-level resume attempts, neither of which reliably recovers:
    • On onActivityChange with any moving class (on_foot, walking, running,
      on_bicycle, in_vehicle) at any confidence, when getState() reports
      enabled: true and either isMoving: false or no fix for 90 s → we call
      changePace(true).
    • On onHeartbeat, when fixes look stale → getCurrentPosition({samples: 3, timeout: 20}),
      compared against a stored "park anchor"; if displacement exceeds a threshold →
      changePace(true).

Because the activity-based path fires on any moving class at any confidence, and the
recorded stream shows on_foot @ 100 % throughout the gap, we would have expected it to fire.
It did not produce a resume. From the JS side we cannot distinguish between:

  • the activitychange / heartbeat events not being delivered to JS in this state, or
  • changePace(true) having no effect while the plugin is in this state.

Listeners registered

onLocation, onMotionChange, onActivityChange, onHeartbeat, onProviderChange,
onConnectivityChange. ready() is called once per JS context; start() is called when the
user enables tracking in-app. We never call stop() except when the user disables tracking.

Questions

  1. What can stop location delivery mid-trip, well before stopTimeout elapses, on either
    platform?
    This is our main question — a 3½-minute-old trip being cut with the motion API
    reporting on_foot @ 100 % is the part we cannot explain from the docs.
  2. On iOS with pausesLocationUpdatesAutomatically: false, our reading of the docs is that
    location-services should never be turned off by the plugin. Is that correct, and can
    anything override it (desiredAccuracy: Navigation, activityType, Low Power Mode, iOS
    suspending the app)? Is TSLocationManager 4.3.x validated against iOS 26?
  3. On Android, what should we check for this symptom — Doze / battery-optimisation
    exemption, OEM background restrictions, foreground-service state? Is there a way to detect
    from the plugin that the service was throttled rather than legitimately parked?
  4. What is the intended mechanism that resumes tracking after the plugin parks, on each
    platform? Should changePace(true) from JS always work in that state, and should
    onActivityChange continue to be delivered while parked?
  5. Does preventSuspend: true / heartbeatInterval: 60 keep the heartbeat firing in this
    state?
    We do not appear to receive heartbeats during the gaps. On iOS, if
    preventSuspend is inoperative when pausesLocationUpdatesAutomatically: false, is there
    any way to keep a JS watchdog alive?
  6. Is stopDetectionDelay expressed in minutes or milliseconds in 5.x? The typedef and the
    Config summary table say minutes, but the ActivityConfig example in the docs uses
    stopDetectionDelay: 10000 // 10s. We currently pass 15, intending 15 minutes.
  7. Could motionTriggerDelay: 30000 be implicated in the Android non-resume? We added it
    recently to avoid false trip starts from indoor pottering; we would like to confirm it
    cannot suppress a genuine resume.
  8. Are there known issues with React Native 0.81 / New Architecture where the native side
    keeps running but JS stops receiving plugin events?

[Optional] Plugin Code and/or Config

// Called exactly once per JS context. start() is called separately when the
// user enables tracking in the app.
await BackgroundGeolocation.ready({
  reset: true,

  geolocation: {
    desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.Navigation,
    stopTimeout: 15,                           // minutes
    showsBackgroundLocationIndicator: false,
    stationaryRadius: 25,
    pausesLocationUpdatesAutomatically: false, // iOS; symptom occurs with and without this
    activityType: BackgroundGeolocation.ActivityType.Other,
  },

  activity: {
    stopDetectionDelay: 15,                    // minutes (see question 6)
    disableMotionActivityUpdates: false,
    motionTriggerDelay: 30000,                 // Android only (see question 7)
  },

  persistence: {
    maxDaysToPersist: 1,
  },

  http: {
    autoSync: false,                           // we upload trips ourselves
    batchSync: false,
  },

  app: {
    stopOnTerminate: false,
    preventSuspend: true,
    heartbeatInterval: 60,
    startOnBoot: true,
    enableHeadless: true,
    notification: {
      title: "CityRhythm",
      text: "Enjoy your journey!",
      channelName: "CityRhythm",
      smallIcon: "drawable/ic_notification",
      largeIcon: "drawable/ic_notification",
      priority: BackgroundGeolocation.NotificationPriority.Default,
      sticky: false,
    },
  },

  logger: {
    debug: false,                              // true in dev builds
    logLevel: BackgroundGeolocation.LogLevel.Warning,  // Verbose in dev builds
  },
});

// NOT set anywhere (left at plugin defaults):
//   disableStopDetection, distanceFilter (so 10), disableElasticity,
//   elasticityMultiplier, useSignificantChangesOnly, locationAuthorizationRequest,
//   activityRecognitionInterval, minimumActivityRecognitionConfidence,
//   speedJumpFilter, stopOnStationary, desiredOdometerAccuracy


iOS `Info.plist` (relevant keys):


<key>UIBackgroundModes</key>
<array>
  <string>location</string>
  <string>fetch</string>
  <string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
  <string>com.transistorsoft.fetch</string>
  <string>com.transistorsoft.customtask</string>
</array>
<key>NSMotionUsageDescription</key>                      <string></string>
<key>NSLocationWhenInUseUsageDescription</key>           <string></string>
<key>NSLocationAlwaysUsageDescription</key>              <string></string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>  <string></string>
<key>TSLocationManagerLicense</key>                      <string></string>


Android: `tslocationmanagerVersion = "4.3.+"` (via Gradle ext vars), license key supplied.
Location permission granted as **Always** / **Allow all the time**; Motion & Fitness (iOS) and
Physical Activity (Android) permissions granted.

[Optional] Relevant log output

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions