At times, the user may experience check-out transactions from various branches included in their package, excluding the branch where they are currently located. For instance, if a user has access to three branches and checks in at one of them, the system may occasionally log check-out transactions for the other two branches. This occurs even though the user's current location is recorded with the latitude and longitude of the active branch, while the transactions are associated with the branch IDs of the other two locations.
bg.BackgroundGeolocation.onGeofence((bg.GeofenceEvent event) async {
LocationServiceHandler.onGeofenceAction(event);
});
bg.BackgroundGeolocation.onConnectivityChange(
(bg.ConnectivityChangeEvent event) async {
LocationServiceHandler.onProviderChangeAction(event: event);
});
bg.BackgroundGeolocation.ready(
bg.Config(
desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
backgroundPermissionRationale: bg.PermissionRationale(
title: "allow_app_access_to_your_location"
.tr(namedArgs: {"appName": "We Attend"}),
message: "location_message".tr()),
enableHeadless: true,
autoSync: false,
distanceFilter: 20,
stopOnTerminate: false,
startOnBoot: Platform.isAndroid,
debug: false,
logLevel: bg.Config.LOG_LEVEL_VERBOSE,
geofenceModeHighAccuracy: true,
),
);
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart'
as bg;
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart';
import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart';
import 'package:ntp/ntp.dart';
import 'package:people_mate_flutter/main.dart';
import 'package:people_mate_flutter/modules/attendance/model/attendance_cash_model.dart';
import 'package:safe_device/safe_device.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../modules/attendance/cubit/cubit.dart';
import '../di/di.dart';
import '../local/cache.dart';
import '../local/data_base/attendance_data_base_model/attendance_database_helper.dart';
import '../network/repository.dart';
abstract class LocationServiceHandler {
static Timer? _intensiveTrackingTimer;
static DateTime? _timerStartTime;
static Timer? _checkInResolutionTimer; // ADDED: Timer for check-in resolution
// SharedPreferences keys
static const String _remainingDurationKey =
'intensive_tracking_remaining_duration';
static const String _lastUpdateDateKey =
'intensive_tracking_last_update_date';
static const String _pendingCheckInsKey = 'pending_check_ins';
/// triggers when client within range or out of range
static onGeofenceAction(GeofenceEvent event) async {
bool developerMode = await SafeDevice.isDevelopmentModeEnable;
bool isMockLocation = await SafeDevice.isMockLocation;
if (developerMode == false && isMockLocation == false) {
GeofenceEvent geofenceEvent = event;
if (Platform.isIOS) {
if (geofenceEvent.action == "ENTER") {
// Cancel any existing timer before starting with remaining duration
_cancelIntensiveTrackingTimer();
// Get remaining duration from SharedPreferences
Duration remainingDuration = await _getRemainingDuration();
// Check if it's a new day, reset if needed
if (await _isNewDay()) {
remainingDuration = Duration(hours: 8);
await _saveRemainingDuration(remainingDuration);
await _updateLastUpdateDate();
}
// Only proceed if there's remaining time
if (remainingDuration > Duration.zero) {
// Entered work location - enable intensive tracking
await bg.BackgroundGeolocation.setConfig(bg.Config(
preventSuspend: true,
distanceFilter: 20,
heartbeatInterval: 60,
));
// Start timer with remaining duration from SharedPreferences
_timerStartTime = DateTime.now();
_intensiveTrackingTimer = Timer(remainingDuration, () async {
await _disableIntensiveTracking();
_intensiveTrackingTimer = null;
// Reset for next day
await _saveRemainingDuration(Duration(hours: 8));
await _updateLastUpdateDate();
});
}
} else if (geofenceEvent.action == "EXIT") {
// Calculate remaining time and save to SharedPreferences
await _updateAndSaveRemainingDuration();
_cancelIntensiveTrackingTimer();
// Left work location - return to battery-friendly mode
await _disableIntensiveTracking();
}
}
if (event.identifier.contains('Branch_ID:')) {
try {
await onProviderChangeAction();
} catch (e) {
print('Error in onProviderChangeAction:');
print(e.toString());
}
if (event.action == 'ENTER') {
// Handle check-in with consecutive check-in logic
await _handleCheckIn(event);
} else if (event.action == 'EXIT') {
// Handle check-out and process any pending check-ins
await _handleCheckOut(event);
}
}
}
}
// MODIFIED: Handle check-in events with a 1-minute delay
static Future<void> _handleCheckIn(GeofenceEvent event) async {
debugPrint(
"Check-in event received: ${event.identifier} at ${event.timestamp}");
final prefs = await SharedPreferences.getInstance();
final pendingCheckInsJson = prefs.getStringList(_pendingCheckInsKey) ?? [];
List<Map<String, dynamic>> pendingCheckIns = pendingCheckInsJson
.map((json) => Map<String, dynamic>.from(jsonDecode(json)))
.toList();
Map<String, dynamic> currentCheckIn = {
'branchId': event.identifier.split(":").last,
'latitude': event.location.coords.latitude,
'longitude': event.location.coords.longitude,
'timestamp': event.timestamp,
'identifier': event.identifier,
};
pendingCheckIns.add(currentCheckIn);
await _savePendingCheckIns(pendingCheckIns);
// Cancel any existing timer because a new check-in has occurred
_checkInResolutionTimer?.cancel();
// Start a new 1-minute timer. This will fire if no other check-in/out
// happens, resolving the check-in to the most recent one.
_checkInResolutionTimer = Timer(const Duration(minutes: 1), () {
_resolvePendingCheckIns();
});
}
// ADDED: New method to process check-ins after the 1-minute delay
static Future<void> _resolvePendingCheckIns() async {
final prefs = await SharedPreferences.getInstance();
final pendingCheckInsJson = prefs.getStringList(_pendingCheckInsKey) ?? [];
if (pendingCheckInsJson.isNotEmpty) {
List<Map<String, dynamic>> pendingCheckIns = pendingCheckInsJson
.map((json) => Map<String, dynamic>.from(jsonDecode(json)))
.toList();
// After the wait, process only the MOST RECENT check-in
final lastCheckIn = pendingCheckIns.last;
await _processCheckIn(lastCheckIn);
// Clear the pending list
await prefs.remove(_pendingCheckInsKey);
}
}
// MODIFIED: Handle check-out events and resolve pending check-ins
static Future<void> _handleCheckOut(GeofenceEvent event) async {
debugPrint(
"Check-out event received: ${event.identifier} at ${event.timestamp}");
// Cancel the resolution timer, as a checkout event provides immediate resolution.
_checkInResolutionTimer?.cancel();
final prefs = await SharedPreferences.getInstance();
final pendingCheckInsJson = prefs.getStringList(_pendingCheckInsKey) ?? [];
// Only proceed if there are pending check-ins to resolve.
if (pendingCheckInsJson.isNotEmpty) {
List<Map<String, dynamic>> pendingCheckIns = pendingCheckInsJson
.map((json) => Map<String, dynamic>.from(jsonDecode(json)))
.toList();
String checkOutBranchId = event.identifier.split(":").last;
// Find check-ins that DON'T match the branch ID of the current check-out event.
// These are considered the "valid" ones.
List<Map<String, dynamic>> validCheckIns = pendingCheckIns
.where((checkIn) => checkIn['branchId'] != checkOutBranchId)
.toList();
// If we found at least one valid check-in...
if (validCheckIns.isNotEmpty) {
// ...process each valid check-in.
for (Map<String, dynamic> checkIn in validCheckIns) {
await _processCheckIn(checkIn);
}
// Clear the list of pending check-ins.
await prefs.remove(_pendingCheckInsKey);
// Exit the function here. Do NOT process the checkout event itself.
// Its job of resolving the ambiguity is done.
return;
}
}
// Process the checkout ONLY if there were no pending check-ins to resolve.
// This handles the simple case: Enter A -> Exit A.
await _processCheckOut(event);
}
// Process individual check-in
static Future<void> _processCheckIn(Map<String, dynamic> checkInData) async {
try {
final f = await di<Repository>().recordAutoAttendance(
latitude: checkInData['latitude'],
longitude: checkInData['longitude'],
branchId: int.parse(checkInData['branchId']),
type: "check_in",
isFromHome: false,
isAutomatic: true,
);
f.fold((l) async {
// Log transaction for debugging, cached transaction for offline
AttendanceDBHelper.addLogTransactionModel(
model: TransactionModel(
branchId: checkInData['branchId'],
worksPlaceLatitude: checkInData['latitude'],
worksPlaceLongtitude: checkInData['longitude'],
date: checkInData['timestamp'],
checkType: "check_in",
statusMessage: "Server Error === > ${l.toString()}",
workPlaceName: checkInData['identifier'],
),
);
if (l.toString().contains("internet connection")) {
DateTime? dateTimeNow = await getServerTime();
AttendanceDBHelper.addCachedTransactionModel(
model: TransactionModel(
branchId: checkInData['branchId'],
worksPlaceLatitude: checkInData['latitude'],
worksPlaceLongtitude: checkInData['longitude'],
date: dateTimeNow != null
? dateTimeNow.toString()
: checkInData['timestamp'],
checkType: "check_in",
statusMessage: "Server Error === > ${l.toString()}",
workPlaceName: checkInData['identifier']));
}
}, (r) async {
AttendanceDBHelper.addLogTransactionModel(
model: TransactionModel(
branchId: checkInData['branchId'],
worksPlaceLatitude: checkInData['latitude'],
worksPlaceLongtitude: checkInData['longitude'],
date: checkInData['timestamp'],
checkType: "check_in",
statusMessage: "Success ${r.message.toString()}",
workPlaceName: checkInData['identifier']));
await AttendanceCubit.get(navigatorKey.currentState!.context)
.getAttendanceDayHistory(dateTimeOfDay: DateTime.now());
});
} catch (e) {
AttendanceDBHelper.addLogTransactionModel(
model: TransactionModel(
branchId: checkInData['branchId'],
worksPlaceLatitude: checkInData['latitude'],
worksPlaceLongtitude: checkInData['longitude'],
date: checkInData['timestamp'],
checkType: "check_in",
statusMessage: "Server Error TRY === > ${e.toString()}",
));
}
}
// Process check-out
static Future<void> _processCheckOut(GeofenceEvent event) async {
try {
final f = await di<Repository>().recordAutoAttendance(
latitude: event.location.coords.latitude,
longitude: event.location.coords.longitude,
branchId: int.parse(event.identifier.split(":").last),
type: "check_out",
isFromHome: false,
isAutomatic: true,
);
f.fold((l) async {
AttendanceDBHelper.addLogTransactionModel(
model: TransactionModel(
branchId: event.identifier.split(":").last,
worksPlaceLatitude: event.location.coords.latitude,
worksPlaceLongtitude: event.location.coords.longitude,
date: event.timestamp,
checkType: "check_out",
statusMessage: "Server Error === > ${l.toString()}",
workPlaceName: event.identifier,
),
);
if (l.toString().contains("internet connection")) {
DateTime? dateTimeNow = await getServerTime();
AttendanceDBHelper.addCachedTransactionModel(
model: TransactionModel(
branchId: event.identifier.split(":").last,
worksPlaceLatitude: event.location.coords.latitude,
worksPlaceLongtitude: event.location.coords.longitude,
date: dateTimeNow != null
? dateTimeNow.toString()
: event.timestamp,
checkType: "check_out",
statusMessage: "Server Error === > ${l.toString()}",
workPlaceName: event.identifier));
}
}, (r) async {
AttendanceDBHelper.addLogTransactionModel(
model: TransactionModel(
branchId: event.identifier.split(":").last,
worksPlaceLatitude: event.location.coords.latitude,
worksPlaceLongtitude: event.location.coords.longitude,
date: event.timestamp,
checkType: "check_out",
statusMessage: "Success ${r.message.toString()}",
workPlaceName: event.identifier));
await AttendanceCubit.get(navigatorKey.currentState!.context)
.getAttendanceDayHistory(dateTimeOfDay: DateTime.now());
});
} catch (e) {
AttendanceDBHelper.addLogTransactionModel(
model: TransactionModel(
branchId: event.identifier.split(":").last,
worksPlaceLatitude: event.location.coords.latitude,
worksPlaceLongtitude: event.location.coords.longitude,
date: event.timestamp,
checkType: "check_out",
statusMessage: "Server Error TRY === > ${e.toString()}",
));
}
}
// Save pending check-ins to SharedPreferences
static Future<void> _savePendingCheckIns(
List<Map<String, dynamic>> checkIns) async {
final prefs = await SharedPreferences.getInstance();
final jsonList = checkIns.map((checkIn) => jsonEncode(checkIn)).toList();
await prefs.setStringList(_pendingCheckInsKey, jsonList);
}
// Get remaining duration from SharedPreferences
static Future<Duration> _getRemainingDuration() async {
final prefs = await SharedPreferences.getInstance();
final hours = prefs.getDouble(_remainingDurationKey) ?? 8.0;
return Duration(milliseconds: (hours * 3600000).round());
}
// Save remaining duration to SharedPreferences
static Future<void> _saveRemainingDuration(Duration duration) async {
final prefs = await SharedPreferences.getInstance();
final hours = duration.inMilliseconds / 3600000.0;
await prefs.setDouble(_remainingDurationKey, hours);
}
// Update last update date
static Future<void> _updateLastUpdateDate() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_lastUpdateDateKey, DateTime.now().toIso8601String());
}
// Check if it's a new day
static Future<bool> _isNewDay() async {
final prefs = await SharedPreferences.getInstance();
final lastUpdateString = prefs.getString(_lastUpdateDateKey);
if (lastUpdateString == null) {
return true; // First time usage
}
final lastUpdate = DateTime.parse(lastUpdateString);
final today = DateTime.now();
// Check if the date has changed
return lastUpdate.day != today.day ||
lastUpdate.month != today.month ||
lastUpdate.year != today.year;
}
// Update remaining duration when exiting and save to SharedPreferences
static Future<void> _updateAndSaveRemainingDuration() async {
if (_timerStartTime != null) {
final elapsedTime = DateTime.now().difference(_timerStartTime!);
final currentRemaining = await _getRemainingDuration();
final newRemaining = currentRemaining - elapsedTime;
// Ensure remaining duration doesn't go below zero
final finalRemaining =
newRemaining.isNegative ? Duration.zero : newRemaining;
// Save to SharedPreferences
await _saveRemainingDuration(finalRemaining);
}
}
// Cancel the intensive tracking timer
static void _cancelIntensiveTrackingTimer() {
if (_intensiveTrackingTimer != null && _intensiveTrackingTimer!.isActive) {
_intensiveTrackingTimer!.cancel();
_intensiveTrackingTimer = null;
}
}
// Disable intensive tracking
static Future<void> _disableIntensiveTracking() async {
await BackgroundGeolocation.setConfig(bg.Config(
preventSuspend: false,
distanceFilter: 50,
heartbeatInterval: -1,
));
}
// // Get pending check-ins (for debugging/monitoring)
// static Future<List<Map<String, dynamic>>> getPendingCheckIns() async {
// final prefs = await SharedPreferences.getInstance();
// final pendingCheckInsJson = prefs.getStringList(_pendingCheckInsKey) ?? [];
// return pendingCheckInsJson
// .map((json) => Map<String, dynamic>.from(jsonDecode(json)))
// .toList();
// }
/// onProviderChangeAction: triggers when GPS or WIFI switched on or off
/// This method fetch cached List of transactions and send them when user become online using recordMultiPleAutoAttendance Method
static onProviderChangeAction({ConnectivityChangeEvent? event}) async {
bool developerMode = await SafeDevice.isDevelopmentModeEnable;
bool isMockLocation = await SafeDevice.isMockLocation;
if (developerMode == false && isMockLocation == false) {
List<TransactionModel> cachedTransactions = [];
bool enabled = true;
await BackgroundGeolocation.state.then((value) {
enabled = value.enabled;
});
// if (event?.connected == false && !enabled) {
// await AttendanceDBHelper.getAttendanceModel().then((value) {
// if (value?.isAutomaticServiceEnabled == 1) {
// onEnabledChangeAction(val: false);
// }
// });
// }
if (await AttendanceDBHelper.getCachedTransactions() != null) {
cachedTransactions =
(await AttendanceDBHelper.getCachedTransactions())!;
}
if (cachedTransactions.isNotEmpty &&
event?.connected == true &&
enabled) {
late StreamSubscription<InternetStatus> subscription;
subscription = InternetConnection()
.onStatusChange
.listen((InternetStatus status) async {
if (status == InternetStatus.connected) {
final f = await di<Repository>().recordMultibleAutoAttendance(
transactionList: cachedTransactions);
f.fold((l) {
// Do nothing on failure, keep cached transactions
debugPrint(
"Failed to send cached transactions after switching to online : ${l.toString()}");
AttendanceDBHelper.addLogTransactionModel(
model: TransactionModel(
statusMessage:
"Failed to send cached transactions after switching to online: ${l.toString()}",
));
if (!l.toString().contains("internet connection")) {
AttendanceDBHelper.deleteCachedTransactionDatabase();
subscription.cancel();
}
}, (r) {
AttendanceDBHelper.deleteCachedTransactionDatabase();
subscription.cancel();
});
}
});
}
}
}
/// triggers when Geofencing service switched on or off
// static onEnabledChangeAction({required bool val}) async {
// String? isAutomatic = await AppDatabaseHelper().getAutomaticServiceFlag();
// bool developerMode = await SafeDevice.isDevelopmentModeEnable;
// bool isMockLocation = await SafeDevice.isMockLocation;
// if (developerMode == false &&
// isMockLocation == false &&
// isAutomatic == "true") {
// AttendanceCacheModel? attendanceModel =
// await AttendanceDBHelper.getAttendanceModel();
// if (attendanceModel != null && !val) {
// print("Geofencing service disabled");
// if (attendanceModel.isAutomaticServiceEnabled == 1) {
// try {
// await BackgroundGeolocation.removeGeofences();
// List<Geofence> geofenceList = [];
// for (var element in attendanceModel.branchList!) {
// geofenceList.add(Geofence(
// identifier: 'Branch_ID:${element.id.toString()}',
// radius: element.radius ?? 100,
// latitude: element.latitude,
// longitude: element.longitude,
// notifyOnEntry: true,
// notifyOnExit: true,
// ));
// }
// await BackgroundGeolocation.addGeofences(
// geofenceList,
// );
// } catch (e) {
// debugPrint("Error in onEnabledChangeAction:");
// debugPrint(e.toString());
// AttendanceDBHelper.addLogTransactionModel(
// model: TransactionModel(
// statusMessage: "Error in onEnabledChangeAction: ${e.toString()}",
// ));
// }
// }
// }
// }
// }
}
Future<DateTime?> getServerTime() async {
DateTime? currentDateTime;
try {
final bool isConnected = await InternetConnection().hasInternetAccess;
if (isConnected) {
currentDateTime = await NTP.now();
// Cache the time offset between NTP and device time
final timeOffset = currentDateTime.difference(DateTime.now());
di<CacheHelper>().put('time_offset', timeOffset.inMilliseconds);
} else {
// When offline, use device time + last known offset
final timeOffset = di<CacheHelper>().get<int>('time_offset') ?? 0;
currentDateTime = DateTime.now().add(Duration(milliseconds: timeOffset));
}
} catch (e) {
debugPrint(e.toString());
}
return currentDateTime;
}
Required Reading
Plugin Version
5.0.2
Flutter Doctor
Mobile operating-system(s)
Device Manufacturer(s) and Model(s)
Samsung A56
Device operating-systems(s)
android 16
What happened?
At times, the user may experience check-out transactions from various branches included in their package, excluding the branch where they are currently located. For instance, if a user has access to three branches and checks in at one of them, the system may occasionally log check-out transactions for the other two branches. This occurs even though the user's current location is recorded with the latitude and longitude of the active branch, while the transactions are associated with the branch IDs of the other two locations.
Plugin Code and/or Config
Relevant log output