Custom filtering logic

This commit is contained in:
Anton Tananaev 2025-06-16 22:43:21 -07:00
parent 00599d7b1e
commit 5226e4dd90
6 changed files with 136 additions and 12 deletions

View file

@ -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

View file

@ -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<void> 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<void> 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;

51
lib/location_cache.dart Normal file
View file

@ -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<void> 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;
}
}

View file

@ -119,8 +119,7 @@ class _MainScreenState extends State<MainScreen> {
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);
}

View file

@ -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<void> 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'
),
);
}

View file

@ -30,7 +30,6 @@ class _QuickActionsInitializerState extends State<QuickActionsInitializer> {
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);
}