transistorsoft/flutter_background_geolocation

Unable to restart localization automatically after a long period of inactivity

Closed this issue · 9 comments

@christocracy

Your Environment

  • Plugin version: 4.15.5
  • Platform: iOS and Android
  • OS version: MacOS Sequoia 15.0
  • Device manufacturer / model: Apple Macbook Pro m3
  • Flutter info (flutter doctor):
  • Plugin config:
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.0)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.1)
[✓] VS Code (version 1.94.1)
[✓] Connected device (4 available)
[✓] Network resources
• No issues found!

Expected Behavior

ANDROID

In my application, when the user is working, he presses a button. This button starts the localization. I want to stop the location service when the user is stopped and restart the service when the user is back on the road, until he clicks the button again to log out.

Actual Behavior

I was testing the lib to start when the user click on a button 'START'. But I ran into a problem. When I press the start button, I call the changePace(true) function. After 1min on stationary mode, the device has not detected any movement so it trigger a scheduled Oneshot.

A Scheduled OneShot: STOP_TIMEOUT in 300000ms (5min) is triggered.

After these 5min, if the device is still detected as 'stationary', then all services are disabled (heartbeat, TrackingService) and the Pace is set to false.

The problem is that if I decide to move after these 5min, nothing is triggered anymore. It's as if the lib was disabled. Would you know how to fix this?

What happens instead is that the lib does not restart as it should. In an emergency situation this is problematic.

Also what is the difference between .start() and .changePace(true).

Steps to Reproduce

For test:

  void _setupBackgroundGeolocation() {
    bg.BackgroundGeolocation.onLocation(_onLocation, _onLocationError);
    bg.BackgroundGeolocation.onMotionChange(_onMotionChange);
    bg.BackgroundGeolocation.onActivityChange(_onActivityChange);
    bg.BackgroundGeolocation.onHeartbeat(_onHeartbeat);
    bg.BackgroundGeolocation.onGeofence(_onGeofence);

    bg.BackgroundGeolocation.ready(bg.Config(
      // reset: true,
      debug: true,
      logLevel: bg.Config.LOG_LEVEL_VERBOSE,
      desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
      distanceFilter: 10.0,
      disableElasticity: false,
      backgroundPermissionRationale: bg.PermissionRationale(
          title:
              "Allow {applicationName} to access this device's location even when the app is closed or not in use.",
          message:
              "This app collects location data to enable recording your trips to work and calculate distance-travelled.",
          positiveAction: 'Change to "{backgroundPermissionOptionLabel}"',
          negativeAction: 'Cancel'),
      stopOnTerminate: true,
      startOnBoot: true,
    )).then((bg.State state) {
      print("[ready] ${state.toMap()}");
      bg.BackgroundGeolocation.start().then((bg.State state) {
        print('🚀🚀🚀🚀🚀🚀🚀 [start] success $state');
        setState(() {
          _enabled = state.enabled;
          _isMoving = state.isMoving!;
        });
        bg.BackgroundGeolocation.changePace(true);
      });
    }).catchError((error) {
      print('[ready] ERROR: $error');
    });
  }

  void _onLocation(bg.Location location) {
    print(
        "🟣 🟣 🟣 ====> LAT: ${location.coords.latitude}, LNG: ${location.coords.longitude}");
    // storeUserLocation(location);
  }

  void _onMotionChange(bg.Location location) {
    print('🔴 🔴 🔴 [${bg.Event.MOTIONCHANGE}] - $location');

    setState(() {
      _isMoving = location.isMoving;
    });
  }

  void _onActivityChange(bg.ActivityChangeEvent event) {
    print('🟠 🟠 🟠 [${bg.Event.ACTIVITYCHANGE}] - $event');

    setState(() {});
  }

  void _onLocationError(bg.LocationError error) {
    // Handle the error here
    if (kDebugMode) {
      print(
          "======================> [onLocation] ERROR: ${error.code}, ${error.message}");
    }
  }

  void _onHeartbeat(bg.HeartbeatEvent event) async {
    print("🟡🟡🟡 HEARBEAT Event by our function");
  }

Context

I was trying to restart localization after a long period of inactivity.

Debug logs

Logs

AFTER 1 MIN OF INACTIVITY


D/TSLocationManager(21701):   🎾  start [ActivityRecognitionService  startId: 1, eventCount: 1]
D/TSLocationManager(21701): [c.t.l.s.ActivityRecognitionService a] 
D/TSLocationManager(21701):   🚘 ️DetectedActivity [type=STILL, confidence=100]
D/TSLocationManager(21701): [c.t.l.service.AbstractService a] 
D/TSLocationManager(21701):   🎾  STOP_TIMEOUT [TrackingService  startId: 38, eventCount: 1]
D/TSLocationManager(21701): [c.t.l.service.AbstractService a] 
D/TSLocationManager(21701):   ⚙️︎   FINISH [ActivityRecognitionService startId: 1, eventCount: 0, sticky: false]
I/TSLocationManager(21701): [c.t.l.s.TSScheduleManager oneShot] 
I/TSLocationManager(21701):   ⏰ Scheduled OneShot: STOP_TIMEOUT in 300000ms (jobID: 2059034116)
I/TSLocationManager(21701): [c.t.l.s.TSScheduleManager cancelOneShot] 
I/TSLocationManager(21701):   ⏰ Cancel OneShot: MOTION_ACTIVITY_CHECK
D/TSLocationManager(21701): [c.t.l.service.AbstractService a] 
D/TSLocationManager(21701):   ⚙️︎   FINISH [TrackingService startId: 38, eventCount: 0, sticky: true]
D/TSLocationManager(21701): [c.t.l.service.AbstractService f] 
D/TSLocationManager(21701):   ⚙️︎  ActivityRecognitionService.stopSelfResult(1): true
D/TSLocationManager(21701): [c.t.l.service.AbstractService onDestroy] 
D/TSLocationManager(21701):   🔴  ActivityRecognitionService stopped

AFTER 5MIN OF INACTIVITY (total deactivation of services)

I/TSLocationManager(21701): ╔═════════════════════════════════════════════
I/TSLocationManager(21701): ║ ⏰ OneShot event fired: STOP_TIMEOUT
I/TSLocationManager(21701): ╠═════════════════════════════════════════════
D/TSLocationManager(21701): [c.t.l.adapter.TSConfig e] ℹ️   Persist config, dirty: [isMoving]
D/EGL_emulation(21701): app_time_stats: avg=2.32ms min=1.16ms max=4.09ms count=60
I/TSLocationManager(21701): [c.t.l.service.HeartbeatService stop] 
I/TSLocationManager(21701):   🔴  Stop heartbeat
I/TSLocationManager(21701): [c.t.l.l.TSLocationManager d] 
I/TSLocationManager(21701):   🔴  Location-services: OFF
I/TSLocationManager(21701): [c.t.l.service.TrackingService changePace] 
I/TSLocationManager(21701):   🔵  setPace: true → false
I/TSLocationManager(21701): [c.t.l.l.TSLocationManager a] 
I/TSLocationManager(21701): ╔═════════════════════════════════════════════
I/TSLocationManager(21701): ║ motionchange LocationResult: 3 (65ms old)
I/TSLocationManager(21701): ╠═════════════════════════════════════════════
I/TSLocationManager(21701): ╟─ 📍  Location[fused 48.833630,2.377956 hAcc=7.505 et=+8h51m4s476ms alt=0.0 vAcc=0.5 vel=15.360889 sAcc=0.5 bear=143.87012 bAcc=30.0], time: 1728762234391
I/TSLocationManager(21701): [c.t.l.l.TSLocationManager onSingleLocationResult] 
I/TSLocationManager(21701):   🔵  Acquired motionchange position, isMoving: false
D/TSLocationManager(21701): [c.t.l.l.TSLocationManager a] Median accuracy: 5.0
I/TSLocationManager(21701): [c.t.l.d.s.SQLiteLocationDAO persist] 
I/TSLocationManager(21701):   ✅  INSERT: 59943f8c-623c-4279-877c-827606b9b02d
I/flutter (21701): 🟣 🟣 🟣 ====> LAT: 48.8336297, LNG: 2.3779561
I/flutter (21701): 🔴 🔴 🔴 [motionchange] - [Location {odometer: 34485.1015625, activity: {confidence: 100, type: still}, extras: {}, event: motionchange, battery: {level: 1.0, is_charging: false}, uuid: 59943f8c-623c-4279-877c-827606b9b02d, age: 71, coords: {altitude: 0.0, heading: 143.87, latitude: 48.8336297, accuracy: 7.51, heading_accuracy: 30.0, altitude_accuracy: 0.5, speed_accuracy: 0.5, speed: 15.36, age: 103, longitude: 2.3779561, ellipsoidal_altitude: 0.0}, is_moving: false, timestamp: 2024-10-12T19:43:54.391Z}]
D/TSLocationManager(21701): [c.t.l.g.TSGeofenceManager startMonitoringStationaryRegion] 
D/TSLocationManager(21701):   🎾  Start monitoring stationary region (radius: 150.0m 48.8336297,2.3779561 hAcc=7.505)
D/TSLocationManager(21701): [c.t.l.service.AbstractService a] 
D/TSLocationManager(21701):   🎾  motionchange [TrackingService  startId: 74, eventCount: 1]
I/TSLocationManager(21701): [c.t.l.service.TrackingService k] 
I/TSLocationManager(21701): ╔═════════════════════════════════════════════
I/TSLocationManager(21701): ║ TrackingService motionchange: false
I/TSLocationManager(21701): ╠═════════════════════════════════════════════
D/TSLocationManager(21701): [c.t.l.service.AbstractService a] 
D/TSLocationManager(21701):   ⚙️︎   FINISH [TrackingService startId: 74, eventCount: 0, sticky: false]
D/TSLocationManager(21701): [c.t.l.service.AbstractService f] 
D/TSLocationManager(21701):   ⚙️︎  TrackingService.stopSelfResult(74): true
D/TSLocationManager(21701): [c.t.l.service.AbstractService onDestroy] 
D/TSLocationManager(21701):   🔴  TrackingService stopped
D/EGL_emulation(21701): app_time_stats: avg=3.57ms min=1.11ms max=62.95ms count=58
V/MediaPlayer(21701): resetDrmState:  mDrmInfo=null mDrmProvisioningThread=null mPrepareDrmInProgress=false mActiveDrmScheme=false

I do not have any sort of problem with the demo app tracking me everywhere I go, regardless how long it stops (length of time stopped does not matter).

start by consulting https://dontkillmyapp.com

@christocracy Ok, I think I understand the logic. In your /example folder, I was trying the Hello World app. I need some clarification:

When you set bg.BackgroundGeolocation.ready(CONFIG), you don't call .start(). You only call it when you press the PLAY button, and it switches from green to red. The thing is that I don't understand changePace(). Why use changePace() when you already have .start()?

I understand that changePace() starts aggressive location tracking, but I don't understand the difference between it and .start(). Is changePace(true) using the battery-conscious motion-detection intelligence, or does it just track without pause?

For more context, let's take a cab. When the cab presses the start button, it is now online (so .start() was called). For example, when the cab accepts a ride, would we then call changePace(true) to ensure we have continuous access to the location during the ride?

That's how I understand it.

When i try both, i have the same result, i just notice some change on battery-conscious that why i need your answer. Thanks in advance chris.

[CODE SAMPLE]

bg.BackgroundGeolocation.ready(bg.Config(
            reset: true,
            debug: true,
            logLevel: bg.Config.LOG_LEVEL_VERBOSE,
            desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
            distanceFilter: 10.0,
            backgroundPermissionRationale: bg.PermissionRationale(
                title:
                    "Allow {applicationName} to access this device's location even when the app is closed or not in use.",
                message:
                    "This app collects location data to enable recording your trips to work and calculate distance-travelled.",
                positiveAction: 'Change to "{backgroundPermissionOptionLabel}"',
                negativeAction: 'Cancel'),
            url: "${ENV.TRACKER_HOST}/api/locations",
            authorization: bg.Authorization(
                // <-- demo server authenticates with JWT
                strategy: bg.Authorization.STRATEGY_JWT,
                accessToken: token.accessToken,
                refreshToken: token.refreshToken,
                refreshUrl: "${ENV.TRACKER_HOST}/api/refresh_token",
                refreshPayload: {'refresh_token': '{refreshToken}'}),
            stopOnTerminate: false,
            startOnBoot: true,
            enableHeadless: true))
        .then((bg.State state) {
      print("[ready] ${state.toMap()}");
      bg.BackgroundGeolocation.start().then((bg.State state) {
        print('[start] success $state');
        setState(() {
          _enabled = state.enabled;
          _isMoving = state.isMoving!;
        });
      }).catchError((error) {
        print('[start] ERROR: $error');
      });
      setState(() {
        _enabled = state.enabled;
        _isMoving = state.isMoving!;
      });
    }).catchError((error) {
      print('[ready] ERROR: $error');
    });
void _onClickEnable(enabled) {
    if (enabled) {
      // Reset odometer.
      bg.BackgroundGeolocation.start().then((bg.State state) {
        print('[start] success $state');
        setState(() {
          _enabled = state.enabled;
          _isMoving = state.isMoving!;
        });
      }).catchError((error) {
        print('[start] ERROR: $error');
      });
    } else {
      bg.BackgroundGeolocation.stop().then((bg.State state) {
        print('[stop] success: $state');

        setState(() {
          _enabled = state.enabled;
          _isMoving = state.isMoving!;
        });
      });
    }
  }
// Manually toggle the tracking state:  moving vs stationary
  void _onClickChangePace() {
    setState(() {
      _isMoving = !_isMoving;
    });
    print("[onClickChangePace] -> $_isMoving");

    bg.BackgroundGeolocation.changePace(_isMoving).then((bool isMoving) {
      print('[changePace] success $isMoving');
    }).catchError((e) {
      print('[changePace] ERROR: ' + e.code.toString());
    });
  }

If the plug-in were an electronic device, such as a stereo receiver:

  • .ready(config) is like plugging the A/C cord
  • .start() / .stop() is the [Power] button
  • .changePace starts / stops playing.
  • adding event-listeners, such as .onLocation is like wiring up speakers.

the plug-in automatically calls .changePace upon itself when the device is detected to be moving / stationary.

Executing .changePace manually toggles the tracking state.

read the wiki here “Philosophy of Operation” for more information.

Ok i understand now, thanks a lot!

So we can use .start() without changePace() write? we only need to put the .start() call into .ready() callback ?

As I said above, the plug-in automatically calls .changePace upon itself when the device is detected to be moving.

a jogging app would manually call .changePace(true) (after calling .start()) with a [Start Workout] button.

Ok thanks @christocracy . After waiting to switch stationnary i receive these logs:

I/TSLocationManager(31055): ╔═════════════════════════════════════════════
I/TSLocationManager(31055): ║ ⏰ OneShot event fired: STOP_TIMEOUT
I/TSLocationManager(31055): ╠═════════════════════════════════════════════
D/TSLocationManager(31055): [c.t.l.adapter.TSConfig e] ℹ️   Persist config, dirty: [isMoving]
I/TSLocationManager(31055): [c.t.l.service.HeartbeatService stop] 
I/TSLocationManager(31055):   🔴  Stop heartbeat
I/TSLocationManager(31055): [c.t.l.l.TSLocationManager d] 
I/TSLocationManager(31055):   🔴  Location-services: OFF
I/TSLocationManager(31055): [c.t.l.service.TrackingService changePace] 
I/TSLocationManager(31055):   🔵  setPace: true → false
D/EGL_emulation(31055): app_time_stats: avg=14.78ms min=4.70ms max=42.47ms count=52
I/TSLocationManager(31055): [c.t.l.l.TSLocationManager a] 
I/TSLocationManager(31055): ╔═════════════════════════════════════════════
I/TSLocationManager(31055): ║ motionchange LocationResult: 2 (481ms old)
I/TSLocationManager(31055): ╠═════════════════════════════════════════════
I/TSLocationManager(31055): ╟─ 📍  Location[fused 48.871459,2.322279 hAcc=5.0 et=+8h32m42s429ms alt=0.0 vAcc=0.5 vel=3.9321966 sAcc=0.5 bear=155.0677 bAcc=30.0], time: 1729204342579
I/TSLocationManager(31055): [c.t.l.l.TSLocationManager onSingleLocationResult] 
I/TSLocationManager(31055):   🔵  Acquired motionchange position, isMoving: false
D/TSLocationManager(31055): [c.t.l.l.TSLocationManager a] Median accuracy: 5.0
I/flutter (31055): ======================> ✅ ✅ ✅ LAT: 48.8714587, LNG: 2.3222791
D/TSLocationManager(31055): [c.t.l.g.TSGeofenceManager startMonitoringStationaryRegion] 
D/TSLocationManager(31055):   🎾  Start monitoring stationary region (radius: 150.0m 48.8714587,2.3222791 hAcc=5.0)
I/flutter (31055): ======================> ✅ ✅ ✅ LAT: 48.8714587, LNG: 2.3222791
I/TSLocationManager(31055): [c.t.l.d.s.SQLiteLocationDAO persist] 
I/TSLocationManager(31055):   ✅  INSERT: d02cb8e2-6b23-4f8b-a974-82bf41adf342
D/TSLocationManager(31055): [c.t.l.service.AbstractService a] 
D/TSLocationManager(31055):   🎾  motionchange [TrackingService  startId: 112, eventCount: 1]
I/TSLocationManager(31055): [c.t.l.service.TrackingService k] 
I/TSLocationManager(31055): ╔═════════════════════════════════════════════
I/TSLocationManager(31055): ║ TrackingService motionchange: false
I/TSLocationManager(31055): ╠═════════════════════════════════════════════
D/TSLocationManager(31055): [c.t.l.service.AbstractService a] 
D/TSLocationManager(31055):   ⚙️︎   FINISH [TrackingService startId: 112, eventCount: 0, sticky: false]
D/TSLocationManager(31055): [c.t.l.service.AbstractService f] 
D/TSLocationManager(31055):   ⚙️︎  TrackingService.stopSelfResult(112): true
D/TSLocationManager(31055): [c.t.l.service.AbstractService onDestroy] 
D/TSLocationManager(31055):   🔴  TrackingService stopped

This is good, because it was what i want to try. As you can see, it says that the app starts monitoring a stationary region around the last known location (with a 150-meter radius),. But when i restart location from the stationary positition, to go to the Eiffel Tour, the location didn't restart.

Screenshot 2024-10-18 at 00 40 14 Screenshot 2024-10-18 at 00 47 42

This is exaclty the same problem i had i my previous case. Everytime the location tracking is stopped, it don't want to restart after exit the 150m radius..
I only use the configuration below, but everything looks okay. I don't use changePace(), i only use .start(). Do you have some advice ?

Future _initPlatformState() async {
    // 1.  Listen to events (See docs for all 12 available events).
    // bg.BackgroundGeolocation.onLocation(_onLocation, _onLocationError);
    // bg.BackgroundGeolocation.onMotionChange(_onMotionChange);
    // bg.BackgroundGeolocation.onActivityChange(_onActivityChange);
    // bg.BackgroundGeolocation.onProviderChange(_onProviderChange);
    // bg.BackgroundGeolocation.onConnectivityChange(_onConnectivityChange);
    // bg.BackgroundGeolocation.onHttp(_onHttp);
    // bg.BackgroundGeolocation.onAuthorization(_onAuthorization);

    // 2.  Configure the plugin
    bg.BackgroundGeolocation.ready(bg.Config(
            reset: true,
            debug: true,
            logLevel: bg.Config.LOG_LEVEL_VERBOSE,
            desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
            distanceFilter: 10.0,
            isMoving: true,
            stopOnTerminate: true,
            startOnBoot: false,
            enableHeadless: false))
        .then((bg.State state) {
      print("================> 🟢 [ready] ");

      bg.BackgroundGeolocation.start().then((bg.State state) {
        print(
            '================> 🟢🟢 [start] success : Enabled? :${state.enabled} | IsMoving? :${state.isMoving}');
        setState(() {
          // _enabled = state.enabled;
          // _isMoving = state.isMoving!;
        });
      }).catchError((error) {
        print('===============> 🔴 [start] ERROR: $error');
      });

      setState(() {
        // _enabled = state.enabled;
        // _isMoving = state.isMoving!;
      });
    }).catchError((error) {
      print('================> [ready] ERROR: $error');
    });
  }

Test on Medium Phone - Android 15 API35 | arm64

It’s well-known that Android geofences almost never work in the emulator with simulated location.

Yes i just notice that, everything good in my side now 👍🏽