analyze method
Analyzes a location for spoofing indicators.
Returns null if no spoofing detected, or a SpoofDetectionEvent if suspicious.
Implementation
SpoofDetectionEvent? analyze(Location location, {bool? isMockProvider}) {
if (!config.enabled) return null;
final factors = <SpoofFactor>{};
final details = <String, dynamic>{};
// Check mock provider (Android)
if (config.checkMockProvider && isMockProvider == true) {
factors.add(SpoofFactor.mockProvider);
}
// Check for repeated identical coordinates
if (_previousLocation != null) {
final isSameLocation =
_previousLocation!.coords.latitude == location.coords.latitude &&
_previousLocation!.coords.longitude == location.coords.longitude;
if (isSameLocation) {
_repeatedCoordCount++;
if (_repeatedCoordCount >= _repeatedThreshold) {
factors.add(SpoofFactor.repeatedCoordinates);
details['repeatedCount'] = _repeatedCoordCount;
}
} else {
_repeatedCoordCount = 0;
}
// Check for impossible speed
final distance = LocationUtils.calculateDistance(
_previousLocation!.coords,
location.coords,
);
final duration =
location.timestamp.difference(_previousLocation!.timestamp);
final calculatedSpeedKph =
LocationUtils.calculateSpeedKph(distance, duration);
if (calculatedSpeedKph > config.maxPossibleSpeedKph) {
factors.add(SpoofFactor.impossibleSpeed);
details['calculatedSpeedKph'] = calculatedSpeedKph;
}
// Check for speed mismatch
final reportedSpeedKph = (location.coords.speed ?? 0) * 3.6;
if (calculatedSpeedKph > 10 && reportedSpeedKph > 0) {
final speedRatio = calculatedSpeedKph / reportedSpeedKph;
if (speedRatio < 0.1 || speedRatio > 10) {
factors.add(SpoofFactor.speedMismatch);
details['reportedSpeedKph'] = reportedSpeedKph;
details['calculatedSpeedKph'] = calculatedSpeedKph;
}
}
// Check for impossible altitude change
final prevAlt = _previousLocation!.coords.altitude;
final currAlt = location.coords.altitude;
if (prevAlt != null && currAlt != null) {
final duration = location.timestamp
.difference(_previousLocation!.timestamp)
.inSeconds;
if (duration > 0) {
final altChangePerSec = (currAlt - prevAlt).abs() / duration;
if (altChangePerSec > config.maxAltitudeChangePerSecond) {
factors.add(SpoofFactor.impossibleAltitudeChange);
details['altitudeChangePerSec'] = altChangePerSec;
}
}
}
}
// Check for suspicious accuracy (too perfect)
final accuracy = location.coords.accuracy;
if (config.sensitivity == SpoofSensitivity.high ||
config.sensitivity == SpoofSensitivity.maximum) {
if (accuracy > 0 && accuracy < 1) {
factors.add(SpoofFactor.suspiciousAccuracy);
details['accuracy'] = accuracy;
}
}
// Check for missing altitude (common in spoofed locations)
if (config.sensitivity == SpoofSensitivity.maximum) {
if (location.coords.altitude == null || location.coords.altitude == 0) {
factors.add(SpoofFactor.missingAltitude);
}
}
// Save previous location BEFORE updating for event creation
final oldPreviousLocation = _previousLocation;
_previousLocation = location;
// Determine if we should flag this as spoofed
if (factors.length >= config.minFactorsForDetection) {
// Calculate confidence based on number and type of factors
final confidence = _calculateConfidence(factors);
final event = SpoofDetectionEvent(
location: location,
previousLocation: oldPreviousLocation,
factors: factors,
confidence: confidence,
wasBlocked: config.blockMockLocations,
details: details,
);
// Trigger callback
config.onSpoofDetected?.call(event);
return event;
}
return null;
}