flutter_background_geolocation icon indicating copy to clipboard operation
flutter_background_geolocation copied to clipboard

[Help Wanted]: Geofence getting added again and again

Open mohit-a21 opened this issue 2 months ago â€ĸ 4 comments

Required Reading

  • [x] Confirmed

Plugin Version

4.16.6

Mobile operating-system(s)

  • [ ] iOS
  • [x] Android

Device Manufacturer(s) and Model(s)

Samsun s22 ultra

Device operating-systems(s)

android

What do you require assistance about?

I am facing an issue where the geofences configured by the user gets persisted repeatedly, without user actions. Because of this the user gets geofence dwell events even though they never left the geofence. Following it the dart code of how we are managing configuration of the plugin on app open. I have verified that we are not configuring the geofence from the code as the if condition to see that the geofence exists, is working fine.

In the log I am seeing that first empty set of geofences are persisted and then again my configured set of geofences are persisted. In the debug builds, i have verified that the logs are generated before my configure method has been called.

location.log.txt

[Optional] Plugin Code and/or Config

@override
Future<void> configure({bool fetchCurrentLocation = true}) async {
  final captureMode = await _captureModeRepository.get();
  if (captureMode == LocationCaptureMode.disabled) {
    await _stop(stopConfig);
    return;
  }
  var isSharingOn = false;
  var foundAlwaysOn = false;
  Duration? timed;
  Set<String> destSharingPlaceIds = {};
  var isGeofenceActive = false;
  final now = DateTime.now();
  for (final ls in await _locationSharingRepository.getAllSend()) {
    if (!ls.isActive(now)) {
      continue;
    }
    isSharingOn = true;
    if (ls.sharingType == LocationSharingType.alwaysOn) {
      foundAlwaysOn = true;
      continue;
    }
    if (ls.sharingType == LocationSharingType.timed && ls.till != null) {
      final lsDuration = ls.till!.difference(now);

      if (lsDuration.inMinutes > (timed?.inMinutes ?? 0)) {
        timed = lsDuration;
      }
      continue;
    }

    if (ls.sharingType == LocationSharingType.dest &&
        ls.till != null &&
        ls.destId != null) {
      final lsDuration = ls.till!.difference(now);
      if (lsDuration.inMinutes > (timed?.inMinutes ?? 0)) {
        timed = lsDuration;
      }
      destSharingPlaceIds.add(ls.destId!);
      isGeofenceActive = true;
      continue;
    }
  }

  final placeSettings = await _placeNotifRepository.getAllActive();
  if (placeSettings.isNotEmpty) {
    isGeofenceActive = true;
  }
  if (!isGeofenceActive && !isSharingOn) {
    await _stop(stopConfig);
    return;
  }

  final params = await _locationParamsManager.get();
  Map<String, PlaceNotifRadiusType> activeGeofenceRadius = HashMap();
  for (final ps in placeSettings) {
    activeGeofenceRadius[ps.placeId] = ps.radiusType;
  }

  final allActiveGeofencePlaceIds =
      Set<String>.from(activeGeofenceRadius.keys)
        ..addAll(destSharingPlaceIds);

  final allActiveGeofencePlaces =
      await _placeRepository.getAllIn(allActiveGeofencePlaceIds);

  List<Geofence> existingConfiguredGeofences =
      await bg.BackgroundGeolocation.geofences;
  for (final geofence in existingConfiguredGeofences) {
    // Remove all geofences that are not active
    if (!allActiveGeofencePlaceIds.contains(geofence.identifier)) {
      await bg.BackgroundGeolocation.removeGeofence(geofence.identifier);
    }
  }
  List<bg.Geofence> geofencesToConfigure = [];
  for (final place in allActiveGeofencePlaces) {
    var radius =
        params.locationSharingParams.destSharingGeofence.radius.toDouble();
    final radiusType = activeGeofenceRadius[place.placeId];
    if (radiusType != null) {
      radius = params.geofenceParams
          .getLocationRadius(radiusType: radiusType)
          .radius
          .toDouble();
    }
    var found = false;
    var toConfigure = false;
    for (Geofence existingGeofence in existingConfiguredGeofences) {
      if (existingGeofence.identifier == place.placeId) {
        found = true;
        if (existingGeofence.latitude != place.latitude ||
            existingGeofence.longitude != place.longitude ||
            existingGeofence.radius != radius ||
            existingGeofence.loiteringDelay !=
                params.geofenceParams.loiteringDelay.inMilliseconds) {
          debugPrint("Geofence ${place.placeId} needs to be configured");
          toConfigure = true;
        }
      }
    }
    if (!found) {
      toConfigure = true;
    }
    if (toConfigure) {
      geofencesToConfigure.add(bg.Geofence(
        identifier: place.placeId,
        latitude: place.latitude,
        longitude: place.longitude,
        radius: radius,
        loiteringDelay: params.geofenceParams.loiteringDelay.inMilliseconds,
        notifyOnDwell: true,
        notifyOnExit: true,
      ));
    }
  }

  try {
    if (geofencesToConfigure.isNotEmpty) {
      await bg.BackgroundGeolocation.addGeofences(geofencesToConfigure);
    }
  } catch (e) {
    throw LocationException('[addGeofences] FAILURE: ${e.toString()}');
  }

  await _start(
    captureMode,
    params,
    isSharingOn,
    isGeofenceActive,
    foundAlwaysOn,
    timed,
  );
  if (fetchCurrentLocation && isSharingOn) {
    try {
      await bg.BackgroundGeolocation.getCurrentPosition(
          samples: 1, timeout: 5, extras: {"mode": captureMode.value});
    } catch (e) {
      log(e.toString());
    }
  }
}

Future<void> _start(
    LocationCaptureMode captureMode,
    LocationParams params,
    bool isSharingOn,
    bool isGeofenceActive,
    bool foundAlwaysOn,
    Duration? timed) async {
  var persistMode = bg.Config.PERSIST_MODE_NONE;
  if (isSharingOn && isGeofenceActive) {
    persistMode = bg.Config.PERSIST_MODE_ALL;
  } else if (isSharingOn) {
    persistMode = bg.Config.PERSIST_MODE_LOCATION;
  } else {
    persistMode = bg.Config.PERSIST_MODE_GEOFENCE;
  }

  final startConfig = bg.Config(
    debug: AppConfig.isStaging,
    logLevel: (AppConfig.isStaging) ? Config.LOG_LEVEL_VERBOSE : Config.LOG_LEVEL_OFF,
    url: getLocationUploadEndpoint(),
    headers: {appVersionHeader: AppMemCache.appVersion},
    authorization: Authorization(
      accessToken: await _loginRepository.getAuthToken(),
      refreshToken: await _loginRepository.getRefreshToken(),
      refreshUrl: "refresh_url",
      refreshPayload: {
        'refresh_token': "{refreshToken}",
        "use_access_token": "true"
      },
      refreshHeaders: {appVersionHeader: AppMemCache.appVersion},
    ),
    backgroundPermissionRationale: bg.PermissionRationale(
        title:
            "Allow {applicationName} to access your location even when you are not using the app?",
        message:
            "Background location access ensures accurate location sharing with your chosen family members, even when the app is not actively being used.",
        positiveAction: 'Change to "{backgroundPermissionOptionLabel}"',
        negativeAction: 'Cancel'),
    persistMode: persistMode,
    stopOnTerminate: false,
    startOnBoot: true,
    enableHeadless: true,
    extras: {"mode": captureMode.value},
    disableLocationAuthorizationAlert: true,

    desiredAccuracy: params.captureParams.desiredAccuracy(captureMode),
    distanceFilter:
        params.captureParams.distanceFilter(captureMode, isSharingOn),
    elasticityMultiplier:
        params.captureParams.elasticityMultiplier(captureMode, isSharingOn),
    stopAfterElapsedMinutes:
        !(foundAlwaysOn || isGeofenceActive) ? timed?.inMinutes : null,
    useSignificantChangesOnly: params.captureParams
        .useSignificantChangesOnly(captureMode, isSharingOn),
    maxDaysToPersist: params.captureParams.maxDaysToPersist,
    maxRecordsToPersist: params.captureParams.maxRecordsToPersist,
    disableStopDetection: params.captureParams.disableStopDetection(
      captureMode,
      isSharingOn,
    ),
    activityRecognitionInterval:
        params.captureParams.activityRecognitionInterval(captureMode),
    geofenceModeHighAccuracy: params.captureParams
        .geofenceModeHighAccuracy(captureMode, isGeofenceActive),
    stopOnStationary: false,
  );
  try {
    bg.State state;
    if (!isReadyCalled) {
      state = await bg.BackgroundGeolocation.ready(startConfig);
      isReadyCalled = true;
    } else {
      state = await bg.BackgroundGeolocation.setConfig(startConfig);
    }
    if (!state.enabled) {
      debugPrint("BackgroundGeolocation started");
      if (isSharingOn) {
        await bg.BackgroundGeolocation.start();
      } else {
        await bg.BackgroundGeolocation.startGeofences();
      }
    } else {
      if (state.trackingMode == 1 && !isSharingOn) {
        await bg.BackgroundGeolocation.startGeofences();
      } else if (state.trackingMode == 0 && isSharingOn) {
        await bg.BackgroundGeolocation.start();
      }
    }
    await _scheduleActiveModeNotificationHandler.schedule(captureMode);
  } catch (e) {
    throw LocationException('[start] FAILURE: ${e.toString()}');
  }
}

[Optional] Relevant log output

10-29 01:23:45.590 INFO [TSGeofenceManager start] 
  🎾  Start monitoring geofences
10-29 01:23:45.592 DEBUG [TSSQLiteAppender$c run] 
  â„šī¸  Cleared logs older than 168 hours
10-29 01:23:45.592 DEBUG [SQLiteLocationDAO prune] 
  â„šī¸  PRUNE -2 days
10-29 01:23:45.593 DEBUG [TSGeofenceManager d] â„šī¸  Persist monitored geofences: []
10-29 01:23:45.593 DEBUG [TSGeofenceManager e] â„šī¸  Persist monitored polygons: {}
10-29 01:23:45.598 DEBUG [HeadlessTask onHeadlessEvent] 💀 [HeadlessTask connectivitychange]
10-29 01:23:45.598 DEBUG [HeadlessTask onHeadlessEvent] 💀 [HeadlessTask providerchange]
10-29 01:23:45.598 ERROR [HeadlessTask$TaskRunner run] 
  â€ŧī¸  Invalid Headless Callback ids.  Cannot handle headless event
10-29 01:23:45.598 ERROR [HeadlessTask$TaskRunner run] 
  â€ŧī¸  Invalid Headless Callback ids.  Cannot handle headless event
10-29 01:23:45.605 INFO [TSLocationManager a] 
╔═════════════════════════════════════════════
║ motionchange LocationResult: 1 (76966ms old)
╠═════════════════════════════════════════════
╟─ 📍  Location[fused 26.086984,74.309749 hAcc=100.0 et=+5d20h33m36s786ms alt=431.5 vAcc=48.984005 {Bundle[{battery_level=0.73, is_charging=false, odometer=25405.318}]}], time: 1761681148639

10-29 01:23:45.634 DEBUG [TSLocationManager a] Median accuracy: 100.0
10-29 01:23:45.640 DEBUG [HeadlessTask onHeadlessEvent] 💀 [HeadlessTask location]
10-29 01:23:45.641 DEBUG [LocationAuthorization withPermission] 
  â„šī¸  LocationAuthorization: Permission granted
10-29 01:23:45.641 ERROR [HeadlessTask$TaskRunner run] 
  â€ŧī¸  Invalid Headless Callback ids.  Cannot handle headless event
10-29 01:23:45.641 DEBUG [TSGeofenceManager$e a] â„šī¸  GeofencingClient addGeofences SUCCESS
10-29 01:23:45.641 DEBUG [TSGeofenceManager d] â„šī¸  Persist monitored geofences: [0194b7d9-898c-e51e-0161-6e4d6f079aef, 0196e94a-51d7-a770-3293-c56c4669dd41, 019512e9-ba5e-23af-b0d2-f738dcf8c24a, 0194b32a-86d4-66fd-9745-61546c6d0cf7, 0195cba2-81f4-47c7-21e0-5d08b4a5d9eb]
10-29 01:23:45.641 DEBUG [TSGeofenceManager$e a] 
╔═════════════════════════════════════════════
║ TSGeofenceManager monitoring 5/5
╠═════════════════════════════════════════════
╟─ 🎾  0194b7d9-898c-e51e-0161-6e4d6f079aef
╟─ 🎾  0196e94a-51d7-a770-3293-c56c4669dd41
╟─ 🎾  019512e9-ba5e-23af-b0d2-f738dcf8c24a
╟─ 🎾  0194b32a-86d4-66fd-9745-61546c6d0cf7
╟─ 🎾  0195cba2-81f4-47c7-21e0-5d08b4a5d9eb
╚═════════════════════════════════════════════
10-29 01:23:45.642 DEBUG [LifecycleManager b] 
╔═════════════════════════════════════════════
║ â˜¯ī¸  HeadlessMode? true
╠═════════════════════════════════════════════

mohit-a21 avatar Oct 29 '25 13:10 mohit-a21

I also see this happening when ever i unlock the device after a while.

mohit-a21 avatar Oct 29 '25 15:10 mohit-a21

You're calling addGeofences multiple times

10-29 19:04:32.799 DEBUG [TSGeofenceManager$e a] â„šī¸ GeofencingClient addGeofences SUCCESS

christocracy avatar Oct 29 '25 17:10 christocracy

10-29 01:23:45.590 INFO [TSGeofenceManager start] 
  🎾  Start monitoring geofences
10-29 01:23:45.592 DEBUG [TSSQLiteAppender$c run] 
  â„šī¸  Cleared logs older than 168 hours
10-29 01:23:45.592 DEBUG [SQLiteLocationDAO prune] 
  â„šī¸  PRUNE -2 days
10-29 01:23:45.593 DEBUG [TSGeofenceManager d] â„šī¸  Persist monitored geofences: []
10-29 01:23:45.593 DEBUG [TSGeofenceManager e] â„šī¸  Persist monitored polygons: {}
10-29 01:23:45.598 DEBUG [HeadlessTask onHeadlessEvent] 💀 [HeadlessTask connectivitychange]
10-29 01:23:45.598 DEBUG [HeadlessTask onHeadlessEvent] 💀 [HeadlessTask providerchange]
10-29 01:23:45.598 ERROR [HeadlessTask$TaskRunner run] 
  â€ŧī¸  Invalid Headless Callback ids.  Cannot handle headless event
10-29 01:23:45.598 ERROR [HeadlessTask$TaskRunner run] 
  â€ŧī¸  Invalid Headless Callback ids.  Cannot handle headless event
10-29 01:23:45.605 INFO [TSLocationManager a] 

If you look at these logs, i can see the Persist monitored geofences: [] getting set to an empty list. I am not doing this in my code. There is no: GeofencingClient addGeofences SUCCESS before these logs.

In the attached log file, you can see the config "didDeviceReboot": true set.

It looks like we are getting this config set and resetting the geofences. This is happening on unlock, in background or opening the app after a while. Do you have any idea of why this might be happening?

mohit-a21 avatar Nov 06 '25 18:11 mohit-a21

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

github-actions[bot] avatar Dec 07 '25 02:12 github-actions[bot]