flutter_background_geolocation icon indicating copy to clipboard operation
flutter_background_geolocation copied to clipboard

onGeofence not firing on startup if the location is the same as the last known location

Open AlanJereb opened this issue 1 year ago • 12 comments

Your Environment

  • Plugin version: ^4.11.1
  • Platform: iOS and Android
  • iOS OS version: 17.2
  • Android OS version: 11
  • Device manufacturer / model: Pixel 4 / iPhone 13 Pro Max
  • Flutter info (flutter doctor):
[√] Flutter (Channel stable, 3.16.7, on Microsoft Windows [Version 10.0.19043.985], locale sl-SI)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.7.4)
[√] Android Studio (version 2021.2)
[√] VS Code, 64-bit edition (version 1.85.1)
[√] Connected device (4 available)
[√] Network resources
  • Plugin config:
        desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
        distanceFilter: 0,
        stopOnTerminate: true,
        startOnBoot: true,
        autoSync: false,
        reset: true,
        isMoving: true,
        stopTimeout: 30000,
        disableStopDetection: true,
        geofenceProximityRadius: 200,
        foregroundService: true,
        debug: true,
        logLevel: bg.Config.LOG_LEVEL_VERBOSE

Expected Behavior

bg.BackgroundGeolocation.onGeofence should also fire on startup, even though logs show that the location is the same as last known location, and not only on transitions entering and exiting geofences.

Actual Behavior

bg.BackgroundGeolocation.onGeofence doesn't fire on startup if the location is the same as the last known location.

The main issue is this doesn't always happen across all of the devices. It usually works with my iPhone 13 Pro Max but rarely on my colleague's iPhone 13 Pro. (same OS version, same permissions, same location service precision)

Context

I have a time tracking app, that should display to user options based on whether it is onGeofence or not. The app works correctly if the user enters or exits the geofence, but not on app startup if the user is in the same location as when it was closed.

Debug logs

Logs ``` 01-18 10:20:37.144 12285 14415 D TSLocationManager: ╔═════════════════════════════════════════════ 01-18 10:20:37.144 12285 14415 D TSLocationManager: ║ Process LocationResult 01-18 10:20:37.144 12285 14415 D TSLocationManager: ╠═════════════════════════════════════════════ 01-18 10:20:37.144 12285 14415 D TSLocationManager: [c.t.l.l.TSLocationManager onLocationResult] 01-18 10:20:37.144 12285 14415 D TSLocationManager: ℹ️ IGNORED: same as last location 01-18 10:20:38.148 12285 12285 D TSLocationManager: [c.t.l.service.AbstractService a] 01-18 10:20:38.148 12285 12285 D TSLocationManager: 🎾 start [TrackingService startId: 1015, eventCount: 1] 01-18 10:20:38.151 12285 12285 D TSLocationManager: [c.t.l.service.TrackingService c] 01-18 10:20:38.151 12285 12285 D TSLocationManager: ╔═════════════════════════════════════════════ 01-18 10:20:38.151 12285 12285 D TSLocationManager: ║ TrackingService: LocationResult 01-18 10:20:38.151 12285 12285 D TSLocationManager: ╠═════════════════════════════════════════════ 01-18 10:20:38.151 12285 12285 D TSLocationManager: ╟─ 📍 Location[fused 45.968327,13.639567 hAcc=5.012 et=+23h7m48s549ms alt=0.0 vAcc=0.5 vel=0.0 sAcc=0.5] 01-18 10:20:38.151 12285 12285 D TSLocationManager: ╟─ Age: 24ms, time: 1705569638124 01-18 10:20:38.152 12285 12285 D TSLocationManager: [c.t.l.service.AbstractService a] 01-18 10:20:38.152 12285 12285 D TSLocationManager: ⚙️︎ FINISH [TrackingService startId: 1015, eventCount: 0, sticky: true] 01-18 10:20:38.153 12285 14415 D TSLocationManager: [c.t.l.l.TSLocationManager onLocationResult] 01-18 10:20:38.153 12285 14415 D TSLocationManager: ╔═════════════════════════════════════════════ 01-18 10:20:38.153 12285 14415 D TSLocationManager: ║ Process LocationResult 01-18 10:20:38.153 12285 14415 D TSLocationManager: ╠═════════════════════════════════════════════ 01-18 10:20:38.154 12285 14415 D TSLocationManager: [c.t.l.l.TSLocationManager onLocationResult] 01-18 10:20:38.154 12285 14415 D TSLocationManager: ℹ️ IGNORED: same as last location 01-18 10:20:40.017 12285 12285 D TSLocationManager: [c.t.l.service.AbstractService a] 01-18 10:20:40.017 12285 12285 D TSLocationManager: 🎾 start [TrackingService startId: 1016, eventCount: 1] 01-18 10:20:40.018 12285 12285 D TSLocationManager: [c.t.l.service.TrackingService c] 01-18 10:20:40.018 12285 12285 D TSLocationManager: ╔═════════════════════════════════════════════ 01-18 10:20:40.018 12285 12285 D TSLocationManager: ║ TrackingService: LocationResult 01-18 10:20:40.018 12285 12285 D TSLocationManager: ╠═════════════════════════════════════════════ 01-18 10:20:40.018 12285 12285 D TSLocationManager: ╟─ 📍 Location[fused 45.968327,13.639567 hAcc=5.394 et=+23h7m49s602ms alt=0.0 vAcc=0.5 vel=0.0 sAcc=0.5] 01-18 10:20:40.018 12285 12285 D TSLocationManager: ╟─ Age: 840ms, time: 1705569639177 01-18 10:20:40.018 12285 12285 D TSLocationManager: [c.t.l.service.AbstractService a] 01-18 10:20:40.018 12285 12285 D TSLocationManager: ⚙️︎ FINISH [TrackingService startId: 1016, eventCount: 0, sticky: true] 01-18 10:20:40.019 12285 14415 D TSLocationManager: [c.t.l.l.TSLocationManager onLocationResult] 01-18 10:20:40.019 12285 14415 D TSLocationManager: ╔═════════════════════════════════════════════ 01-18 10:20:40.019 12285 14415 D TSLocationManager: ║ Process LocationResult 01-18 10:20:40.019 12285 14415 D TSLocationManager: ╠═════════════════════════════════════════════ 01-18 10:20:40.021 12285 14415 D TSLocationManager: [c.t.l.l.TSLocationManager onLocationResult] 01-18 10:20:40.021 12285 14415 D TSLocationManager: ℹ️ IGNORED: same as last location 01-18 10:20:40.140 12285 12285 D TSLocationManager: [c.t.l.service.AbstractService a] 01-18 10:20:40.140 12285 12285 D TSLocationManager: 🎾 start [TrackingService startId: 1017, eventCount: 1] 01-18 10:20:40.142 12285 12285 D TSLocationManager: [c.t.l.service.TrackingService c] 01-18 10:20:40.142 12285 12285 D TSLocationManager: ╔═════════════════════════════════════════════ 01-18 10:20:40.142 12285 12285 D TSLocationManager: ║ TrackingService: LocationResult 01-18 10:20:40.142 12285 12285 D TSLocationManager: ╠═════════════════════════════════════════════ 01-18 10:20:40.142 12285 12285 D TSLocationManager: ╟─ 📍 Location[fused 45.968327,13.639567 hAcc=5.315 et=+23h7m50s547ms alt=0.0 vAcc=0.5 vel=0.0 sAcc=0.5] 01-18 10:20:40.142 12285 12285 D TSLocationManager: ╟─ Age: 18ms, time: 1705569640122 01-18 10:20:40.143 12285 12285 D TSLocationManager: [c.t.l.service.AbstractService a] 01-18 10:20:40.143 12285 12285 D TSLocationManager: ⚙️︎ FINISH [TrackingService startId: 1017, eventCount: 0, sticky: true] 01-18 10:20:40.146 12285 14415 D TSLocationManager: [c.t.l.l.TSLocationManager onLocationResult] 01-18 10:20:40.146 12285 14415 D TSLocationManager: ╔═════════════════════════════════════════════ 01-18 10:20:40.146 12285 14415 D TSLocationManager: ║ Process LocationResult 01-18 10:20:40.146 12285 14415 D TSLocationManager: ╠═════════════════════════════════════════════ 01-18 10:20:40.147 12285 14415 D TSLocationManager: [c.t.l.l.TSLocationManager onLocationResult] 01-18 10:20:40.147 12285 14415 D TSLocationManager: ℹ️ IGNORED: same as last location 01-18 10:20:41.137 12285 12285 D TSLocationManager: [c.t.l.service.AbstractService a] 01-18 10:20:41.137 12285 12285 D TSLocationManager: 🎾 start [TrackingService startId: 1018, eventCount: 1] 01-18 10:20:41.138 12285 12285 D TSLocationManager: [c.t.l.service.TrackingService c] 01-18 10:20:41.138 12285 12285 D TSLocationManager: ╔═════════════════════════════════════════════ 01-18 10:20:41.138 12285 12285 D TSLocationManager: ║ TrackingService: LocationResult 01-18 10:20:41.138 12285 12285 D TSLocationManager: ╠═════════════════════════════════════════════ 01-18 10:20:41.138 12285 12285 D TSLocationManager: ╟─ 📍 Location[fused 45.968327,13.639567 hAcc=5.315 et=+23h7m51s547ms alt=0.0 vAcc=0.5 vel=0.0 sAcc=0.5] 01-18 10:20:41.138 12285 12285 D TSLocationManager: ╟─ Age: 15ms, time: 1705569641122 01-18 10:20:41.138 12285 12285 D TSLocationManager: [c.t.l.service.AbstractService a] 01-18 10:20:41.138 12285 12285 D TSLocationManager: ⚙️︎ FINISH [TrackingService startId: 1018, eventCount: 0, sticky: true]
PASTE_YOUR_LOGS_HERE

AlanJereb avatar Jan 18 '24 09:01 AlanJereb

Geofence events are handled completely by the OS. There's nothing the plug-in can do to induce the OS to fire a geofence event. The geofencing api does use the motion api internally, so sometimes you can get it to fire by shaking the device or walking around the room you're in.

christocracy avatar Jan 18 '24 13:01 christocracy

Thank you for your prompt reply.

Is there a method on the API we can call to check if the current location is inside of a polygon geofence? I see the isInPolygon in the debug output.

AlanJereb avatar Jan 18 '24 13:01 AlanJereb

Is there a method on the API we can call to check if the current location is inside of a polygon geofence?

No. Circular Geofences are completely handled by the OS. The best way to test geofences is outside with real movement, walking/driving in/out of geofences.

I see the isInPolygon in the debug output.

Are you using polygon geofences?

christocracy avatar Jan 18 '24 13:01 christocracy

Are you using polygon geofences?

Yes, we have the requirement for the use of polygon geofences.

Geofence fallbackGeofence = bg.Geofence(
      identifier: "BS - Solkan",
      vertices: [
        [45.96834346690925, 13.639118639857761], // JZ vogal
        [45.96847346901493, 13.639169235194373], // SZ vogal
        [45.96834483352678, 13.639932323625937], // SV vogal
        [45.96821433345091, 13.63988270276132], // JV vogal
      ],
      notifyOnEntry: true,
      notifyOnExit: true,
    );

    List<Geofence> generateGeofences() {
      if (config.instance.geofenceLocations != null && config.instance.geofenceLocations!.isNotEmpty) {

        return config.instance.geofenceLocations!.map((e) {
          if (e.gpsPoints != null && e.gpsPoints!.length == 1) {
            // Single point
            return bg.Geofence(
                  identifier: e.title ?? "",
                  radius: 200,
                  longitude: e.gpsPoints![0].longitude,
                  latitude: e.gpsPoints![0].latitude,
                  notifyOnEntry: true,
                  notifyOnExit: true,
                );
          } else {
            // Polygon points
            return bg.Geofence(
                  identifier: e.title ?? "",
                  vertices: e.gpsPoints?.map((g) => [g.longitude!, g.latitude!]).toList(),
                  notifyOnEntry: true,
                  notifyOnExit: true,
                );
          }
        }).toList();
      }
      return [fallbackGeofence];
    }

this is a function we use to send required data to addGeofences

AlanJereb avatar Jan 18 '24 13:01 AlanJereb

Are you expecting an already entered geofence (where onGeofence has fired once) to fire again just because the app was terminated / restarted?

christocracy avatar Jan 18 '24 13:01 christocracy

I am.

The time-management app provides user with set of events (buttons) based on his location.

For example:

  • If a user is on the company's premises, he gets the button: Start work
  • if a user is outside of the company's premises, he gets the button: Start remote work

We get the set of all events (buttons) from the API, each containing rendering instructions.

For rendering and rerendering of said buttons, we listen to onGeofence which works well when user has the app open and moves in and out of geofences, we would however need an additional check when the app starts, if the user is already inside a geofence, else only the location-unrestricted buttons will be always rendered.

AlanJereb avatar Jan 18 '24 13:01 AlanJereb

In your onGeofence handler, you simply need to persist your geofence state in shared_preferences, with the identifier as the key and a Boolean whether the device is inside.

christocracy avatar Jan 18 '24 13:01 christocracy

What happens if the device is opened and used only inside the geofence and the user never exits it to trigger the onGeofence event? I have nothing to log locally in this case.

AlanJereb avatar Jan 18 '24 14:01 AlanJereb

By default, Geofences initially fire the ENTER event when initially added with .addGeofence.

See https://pub.dev/documentation/flutter_background_geolocation/latest/flt_background_geolocation/Config/geofenceInitialTriggerEntry.html

christocracy avatar Jan 18 '24 14:01 christocracy

Looking through the debug logs, not always is the ENTER event triggered on start (I'm always adding the same polygons for testing, old copies are removed and new ones added right?).

And when I cannot see the ENTER event in the debug, button rendering fails accordingly.

AlanJereb avatar Jan 18 '24 14:01 AlanJereb

When testing while sitting still at your desk, it's not unusual for just-added geofences to not initially fire until some movement is detected. It's best to test outside with real movement, walking in/out of geofences.

old copies are removed and new ones added right?

yes.

christocracy avatar Jan 18 '24 14:01 christocracy

For anyone having the same issue, in the end, I've implemented a custom isCurrentLocationInGeofence. I run it on initialization, but you can run it whenever needed.

I've implemented it for both cases:

  • single GPS location with radius (Haversine algorithm)
  • polygon geofences (modified algorithm found here https://stackoverflow.com/a/13951139/13712319).

Be wary, it will work well on small polygons. If you have bigger polygons, find the midpoint of two adjacent vertices on the sphere, and insert a new vertex. Do this repeatedly until the polygon is within your required tolerances. Alternatively, modify the algorithm to perform the intersection test in a way that accounts for the curvature of the earth.

Future<bool> isCurrentLocationInGeofence() async {
    bg.Location location = await getDevicePosition();

    bool isSinglePoint = false;

    List<GeoPoint> vertices = [];

    for (var geofenceLocation in config.instance.geofenceLocations!) {
      if (geofenceLocation.gpsPoints != null) {
        // Single point
        if (geofenceLocation.gpsPoints!.length == 1) {
          isSinglePoint = true;
        }
        for (var gpsPoint in geofenceLocation.gpsPoints!) {
          vertices.add(GeoPoint(
            gpsPoint.longitude!,
            gpsPoint.latitude!,
          ));
        }
      }
    }

    var isInside = false;
    // ------
    // SINGLE GPS POINT with defined radius - Haversine algorithm
    // ------
    if (isSinglePoint) {
      const double earthRadius = 6372.8; // in kilometers

      double toRadians(double degree) {
        return (degree * pi) / 180;
      };

      var lat1 = location.coords.latitude;
      var lon1 = location.coords.longitude;
      var lat2 = vertices[0].latitude;
      var lon2 = vertices[0].longitude;
      double dLat = toRadians(lat2 - lat1);
      double dLon = toRadians(lon2 - lon1);
      lat1 = toRadians(lat1);
      lat2 = toRadians(lat2);
      double a = pow(sin(dLat / 2), 2) + pow(sin(dLon / 2), 2) * cos(lat1) * cos(lat2);
      double c = 2 * asin(sqrt(a));

      isInside = (earthRadius * c * 1000) <= Common.geofenceProximityRadius; // in meters
      // ------
      // POLYGON
      // ------
    } else {
      var lastPoint = vertices[vertices.length - 1];
      var x = location.coords.longitude;

      for (var point in vertices) {
        double x1 = lastPoint.longitude;
        double x2 = point.longitude;
        var dx = x2 - x1;

        if (dx.abs() > 180.0) {
          // we have, most likely, just jumped the dateline (could do further validation to this effect if needed).  normalise the numbers.
          if (x > 0) {
            while (x1 < 0) {
              x1 += 360;
            }
            while (x2 < 0) {
              x2 += 360;
            }
          } else {
            while (x1 > 0) {
              x1 -= 360;
            }
            while (x2 > 0) {
              x2 -= 360;
            }
          }
          dx = x2 - x1;
        }

        if ((x1 <= x && x2 > x) || (x1 >= x && x2 < x)) {
          var grad = (point.latitude - lastPoint.latitude) / dx;
          var intersectAtLat = lastPoint.latitude + ((x - x1) * grad);

          if (intersectAtLat > location.coords.latitude) {
            isInside = !isInside;
          }
        }
        lastPoint = point;
      }
    }
    return isInside;
  }
}
  // Get Device position
  Future<bg.Location> getDevicePosition() async {
    return await bg.BackgroundGeolocation.getCurrentPosition(
            timeout: 30,
            // 30 second timeout to fetch location
            maximumAge: 5000,
            // Accept the last-known-location if not older than 5000 ms.
            desiredAccuracy: 1,
            // Try to fetch a location with an accuracy of `1` meters.
            samples: 3,
            // How many location samples to attempt.
            persist: false,
        )
        .catchError((error) {
          print('[getCurrentPosition] ERROR: $error');
        }
    );
  }

AlanJereb avatar Jan 31 '24 08:01 AlanJereb

This issue is stale because it has been open for 30 days with no activity.

github-actions[bot] avatar Apr 17 '24 01:04 github-actions[bot]

thanks for the solution. i also need to trigger the geofence on enter event and i couldn't make it to happen.. calculating it myself out of the geofences list solved the problem

ralon99 avatar Apr 27 '24 14:04 ralon99