134 lines
5 KiB
Dart
134 lines
5 KiB
Dart
|
|
import 'dart:developer' as developer;
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
import 'package:firebase_crashlytics/firebase_crashlytics.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';
|
|
|
|
class GeolocationService {
|
|
static Future<void> init() async {
|
|
await bg.BackgroundGeolocation.ready(Preferences.geolocationConfig());
|
|
if (Platform.isAndroid) {
|
|
await bg.BackgroundGeolocation.registerHeadlessTask(headlessTask);
|
|
}
|
|
FirebaseCrashlytics.instance.log('geolocation_init');
|
|
bg.BackgroundGeolocation.onEnabledChange(onEnabledChange);
|
|
bg.BackgroundGeolocation.onMotionChange(onMotionChange);
|
|
bg.BackgroundGeolocation.onHeartbeat(onHeartbeat);
|
|
bg.BackgroundGeolocation.onLocation(onLocation, (bg.LocationError error) {
|
|
developer.log('Location error', error: error);
|
|
});
|
|
}
|
|
|
|
static Future<void> onEnabledChange(bool enabled) async {
|
|
FirebaseCrashlytics.instance.log('geolocation_enabled:$enabled');
|
|
if (Preferences.instance.getBool(Preferences.wakelock) ?? false) {
|
|
if (!enabled) {
|
|
await WakelockPartialAndroid.release();
|
|
}
|
|
}
|
|
}
|
|
|
|
static Future<void> onMotionChange(bg.Location location) async {
|
|
FirebaseCrashlytics.instance.log('geolocation_motion:${location.isMoving}');
|
|
if (Preferences.instance.getBool(Preferences.wakelock) ?? false) {
|
|
if (location.isMoving) {
|
|
await WakelockPartialAndroid.acquire();
|
|
} else {
|
|
await WakelockPartialAndroid.release();
|
|
}
|
|
}
|
|
}
|
|
|
|
static Future<void> onHeartbeat(bg.HeartbeatEvent event) async {
|
|
await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true, extras: {'heartbeat': true});
|
|
}
|
|
|
|
static Future<void> onLocation(bg.Location location) async {
|
|
if (_shouldDelete(location)) {
|
|
try {
|
|
await bg.BackgroundGeolocation.destroyLocation(location.uuid);
|
|
} catch(error) {
|
|
developer.log('Failed to delete location', error: error);
|
|
}
|
|
} else {
|
|
LocationCache.set(location);
|
|
try {
|
|
await bg.BackgroundGeolocation.sync();
|
|
} catch (error) {
|
|
developer.log('Failed to send location', error: error);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 isHighestAccuracy = Preferences.instance.getString(Preferences.accuracy) == 'highest';
|
|
final duration = DateTime.parse(location.timestamp).difference(DateTime.parse(lastLocation.timestamp)).inSeconds;
|
|
|
|
if (!isHighestAccuracy) {
|
|
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;
|
|
|
|
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 = 6371008.8; // meters
|
|
final dLat = _degToRad(to.coords.latitude - from.latitude);
|
|
final dLon = _degToRad(to.coords.longitude - from.longitude);
|
|
final sinLat = sin(dLat / 2);
|
|
final sinLon = sin(dLon / 2);
|
|
final a = sinLat * sinLat + cos(_degToRad(from.latitude)) * cos(_degToRad(to.coords.latitude)) * sinLon * sinLon;
|
|
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
|
return earthRadius * c;
|
|
}
|
|
|
|
static double _degToRad(double degree) => degree * pi / 180.0;
|
|
}
|
|
|
|
Future<void>? _firebaseInitialization;
|
|
|
|
@pragma('vm:entry-point')
|
|
void headlessTask(bg.HeadlessEvent headlessEvent) async {
|
|
await (_firebaseInitialization ??= Firebase.initializeApp());
|
|
await Preferences.init();
|
|
FirebaseCrashlytics.instance.log('geolocation_headless:${headlessEvent.name}');
|
|
switch (headlessEvent.name) {
|
|
case bg.Event.ENABLEDCHANGE:
|
|
await GeolocationService.onEnabledChange(headlessEvent.event);
|
|
case bg.Event.MOTIONCHANGE:
|
|
await GeolocationService.onMotionChange(headlessEvent.event);
|
|
case bg.Event.HEARTBEAT:
|
|
await GeolocationService.onHeartbeat(headlessEvent.event);
|
|
case bg.Event.LOCATION:
|
|
await GeolocationService.onLocation(headlessEvent.event);
|
|
}
|
|
}
|