analyze method

SpoofDetectionEvent? analyze(
  1. Location location, {
  2. bool? isMockProvider,
})

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