From 5226e4dd90164a42f51099364952c5ac16f7f8b7 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Mon, 16 Jun 2025 22:43:21 -0700 Subject: [PATCH] Custom filtering logic --- ios/Podfile.lock | 4 +-- lib/geolocation_service.dart | 66 ++++++++++++++++++++++++++++++++++++ lib/location_cache.dart | 51 ++++++++++++++++++++++++++++ lib/main_screen.dart | 3 +- lib/preferences.dart | 23 +++++++++---- lib/quick_actions.dart | 1 - 6 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 lib/location_cache.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6d34852..28eb8f7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -92,7 +92,7 @@ PODS: - nanopb (~> 3.30910.0) - PromisesSwift (~> 2.1) - Flutter (1.0.0) - - flutter_background_geolocation (4.16.10): + - flutter_background_geolocation (4.16.11): - CocoaLumberjack (~> 3.8.5) - Flutter - GoogleAppMeasurement (11.13.0): @@ -234,7 +234,7 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: 7b74ceaa54e28863ed17fa39da8951692725eced FirebaseSessions: eaa8ec037e7793769defe4201c20bd4d976f9677 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_background_geolocation: a67777b118f342ae7e59d42f5afafff42a131aaf + flutter_background_geolocation: defe705fe7c50b1be772e0d298ed3765fa17c022 GoogleAppMeasurement: 0dfca1a4b534d123de3945e28f77869d10d0d600 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 diff --git a/lib/geolocation_service.dart b/lib/geolocation_service.dart index 8319a96..8e0f1ca 100644 --- a/lib/geolocation_service.dart +++ b/lib/geolocation_service.dart @@ -1,7 +1,11 @@ +import 'dart:developer' as developer; import 'dart:io'; +import 'dart:math'; +import 'package:flutter/material.dart'; import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg; +import 'package:traccar_client/location_cache.dart'; import 'package:traccar_client/preferences.dart'; import 'package:wakelock_partial_android/wakelock_partial_android.dart'; @@ -13,6 +17,7 @@ class GeolocationService { } bg.BackgroundGeolocation.onEnabledChange(onEnabledChange); bg.BackgroundGeolocation.onMotionChange(onMotionChange); + bg.BackgroundGeolocation.onLocation(onLocation); bg.BackgroundGeolocation.onHeartbeat(onHeartbeat); } @@ -34,9 +39,67 @@ class GeolocationService { } } + static Future onLocation(bg.Location location) async { + if (_shouldDelete(location)) { + await bg.BackgroundGeolocation.destroyLocation(location.uuid); + } else { + LocationCache.set(location); + try { + await bg.BackgroundGeolocation.sync(); + } catch (error) { + developer.log('Failed to send location', error: error); + } + } + } + static Future onHeartbeat(bg.HeartbeatEvent event) async { await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true, extras: {'heartbeat': true}); } + + static bool _shouldDelete(bg.Location location) { + if (!location.isMoving) return false; + if (location.extras?.isNotEmpty == true) return false; + + final lastLocation = LocationCache.get(); + if (lastLocation == null) return false; + + final duration = DateTime.parse(location.timestamp).difference(DateTime.parse(lastLocation.timestamp)).inSeconds; + + final fastestInterval = Preferences.instance.getInt(Preferences.fastestInterval); + if (fastestInterval != null && duration < fastestInterval) return true; + + final distance = _distance(lastLocation, location); + + final distanceFilter = Preferences.instance.getInt(Preferences.distance) ?? 0; + if (distanceFilter > 0 && distance >= distanceFilter) return false; + + final isHighestAccuracy = Preferences.instance.getString(Preferences.accuracy) == 'highest'; + + if (distanceFilter == 0 || isHighestAccuracy) { + final intervalFilter = Preferences.instance.getInt(Preferences.interval) ?? 0; + if (intervalFilter > 0 && duration >= intervalFilter) return false; + } + + if (isHighestAccuracy && lastLocation.heading >= 0 && location.coords.heading > 0) { + final angle = (location.coords.heading - lastLocation.heading).abs(); + final angleFilter = Preferences.instance.getInt(Preferences.angle) ?? 0; + if (angleFilter > 0 && angle >= angleFilter) return false; + } + + return true; + } + + static double _distance(Location from, bg.Location to) { + const earthRadius = 6371000; // meters + final dLat = _degToRad(to.coords.latitude - from.latitude); + final dLon = _degToRad(to.coords.longitude - from.longitude); + final a = sin(dLat / 2) * sin(dLat / 2) + + cos(_degToRad(from.latitude)) * cos(_degToRad(to.coords.latitude)) * sin(dLon / 2) * sin(dLon / 2); + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + return earthRadius * c; + } + + static double _degToRad(double degree) => degree * pi / 180.0; } @pragma('vm:entry-point') @@ -49,6 +112,9 @@ void headlessTask(bg.HeadlessEvent headlessEvent) async { case bg.Event.MOTIONCHANGE: await GeolocationService.onMotionChange(headlessEvent.event); break; + case bg.Event.LOCATION: + await GeolocationService.onLocation(headlessEvent.event); + break; case bg.Event.HEARTBEAT: await GeolocationService.onHeartbeat(headlessEvent.event); break; diff --git a/lib/location_cache.dart b/lib/location_cache.dart new file mode 100644 index 0000000..53279cf --- /dev/null +++ b/lib/location_cache.dart @@ -0,0 +1,51 @@ +import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg; +import 'package:traccar_client/preferences.dart'; + +class Location { + final String timestamp; + final double latitude; + final double longitude; + final double heading; + const Location({ + required this.timestamp, + required this.latitude, + required this.longitude, + required this.heading, + }); +} + +class LocationCache { + static Location? _last; + + static Location? get() { + if (_last == null) { + final timestamp = Preferences.instance.getString(Preferences.lastTimestamp); + final latitude = Preferences.instance.getDouble(Preferences.lastLatitude); + final longitude = Preferences.instance.getDouble(Preferences.lastLongitude); + final heading = Preferences.instance.getDouble(Preferences.lastHeading); + if (timestamp != null && latitude != null && longitude != null && heading != null) { + _last = Location( + timestamp: timestamp, + latitude: latitude, + longitude: longitude, + heading: heading, + ); + } + } + return _last; + } + + static Future set(bg.Location location) async { + final last = Location( + timestamp: location.timestamp, + latitude: location.coords.latitude, + longitude: location.coords.longitude, + heading: location.coords.heading, + ); + Preferences.instance.setString(Preferences.lastTimestamp, last.timestamp); + Preferences.instance.setDouble(Preferences.lastLatitude, last.latitude); + Preferences.instance.setDouble(Preferences.lastLongitude, last.longitude); + Preferences.instance.setDouble(Preferences.lastHeading, last.heading); + _last = last; + } +} diff --git a/lib/main_screen.dart b/lib/main_screen.dart index f39afcf..333bbfc 100644 --- a/lib/main_screen.dart +++ b/lib/main_screen.dart @@ -119,8 +119,7 @@ class _MainScreenState extends State { FilledButton.tonal( onPressed: () async { try { - await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true); - await bg.BackgroundGeolocation.sync(); + await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true, extras: {'manual': true}); } catch (error) { developer.log('Failed to fetch location', error: error); } diff --git a/lib/preferences.dart b/lib/preferences.dart index 689837a..06c7db4 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -8,6 +8,7 @@ import 'package:shared_preferences_android/shared_preferences_android.dart'; class Preferences { static late SharedPreferencesWithCache instance; + static const String id = 'id'; static const String url = 'url'; static const String accuracy = 'accuracy'; @@ -20,6 +21,11 @@ class Preferences { static const String wakelock = 'wakelock'; static const String stopDetection = 'stop_detection'; + static const String lastTimestamp = 'lastTimestamp'; + static const String lastLatitude = 'lastLatitude'; + static const String lastLongitude = 'lastLongitude'; + static const String lastHeading = 'lastHeading'; + static Future init() async { instance = await SharedPreferencesWithCache.create( sharedPreferencesOptions: Platform.isAndroid @@ -29,6 +35,7 @@ class Preferences { allowList: { id, url, accuracy, distance, interval, angle, heartbeat, fastestInterval, buffer, wakelock, stopDetection, + lastTimestamp, lastLatitude, lastLongitude, lastHeading, 'device_id_preference', 'server_url_preference', 'accuracy_preference', 'frequency_preference', 'distance_preference', 'buffer_preference', }, @@ -66,6 +73,7 @@ class Preferences { } static bg.Config geolocationConfig() { + final isHighestAccuracy = instance.getString(accuracy) == 'highest'; final locationUpdateInterval = (instance.getInt(interval) ?? 0) * 1000; final fastestLocationUpdateInterval = (instance.getInt(fastestInterval) ?? 30) * 1000; final heartbeatInterval = instance.getInt(heartbeat) ?? 0; @@ -79,12 +87,13 @@ class Preferences { 'low' => bg.Config.DESIRED_ACCURACY_LOW, _ => bg.Config.DESIRED_ACCURACY_MEDIUM, }, + autoSync: false, url: _formatUrl(instance.getString(url)), params: { - "device_id": instance.getString(id), + 'device_id': instance.getString(id), }, - distanceFilter: instance.getInt(distance)?.toDouble(), - locationUpdateInterval: locationUpdateInterval > 0 ? locationUpdateInterval : null, + distanceFilter: isHighestAccuracy ? 0 : instance.getInt(distance)?.toDouble(), + locationUpdateInterval: isHighestAccuracy ? 0 : (locationUpdateInterval > 0 ? locationUpdateInterval : null), heartbeatInterval: heartbeatInterval > 0 ? heartbeatInterval : null, maxRecordsToPersist: instance.getBool(buffer) != false ? -1 : 1, logLevel: bg.Config.LOG_LEVEL_VERBOSE, @@ -97,10 +106,10 @@ class Preferences { pausesLocationUpdatesAutomatically: instance.getBool(stopDetection) == false, fastestLocationUpdateInterval: fastestLocationUpdateInterval > 0 ? fastestLocationUpdateInterval : null, backgroundPermissionRationale: bg.PermissionRationale( - title: "Allow {applicationName} to access this device's location in the background", - message: "For reliable tracking, please enable {backgroundPermissionOptionLabel} location access.", - positiveAction: "Change to {backgroundPermissionOptionLabel}", - negativeAction: "Cancel" + title: 'Allow {applicationName} to access this device\'s location in the background', + message: 'For reliable tracking, please enable {backgroundPermissionOptionLabel} location access.', + positiveAction: 'Change to {backgroundPermissionOptionLabel}', + negativeAction: 'Cancel' ), ); } diff --git a/lib/quick_actions.dart b/lib/quick_actions.dart index c17d768..ba8aee1 100644 --- a/lib/quick_actions.dart +++ b/lib/quick_actions.dart @@ -30,7 +30,6 @@ class _QuickActionsInitializerState extends State { case 'sos': try { await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true, extras: {'alarm': 'sos'}); - await bg.BackgroundGeolocation.sync(); } catch (error) { developer.log('Failed to send alert', error: error); }