locus 1.2.0 copy "locus: ^1.2.0" to clipboard
locus: ^1.2.0 copied to clipboard

Background geolocation SDK for Flutter. Native tracking, geofencing, activity recognition, and sync.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart' hide Config;
import 'package:locus/locus.dart';

void main() => runApp(const MotionRecognitionApp());

enum TrackingPreset {
  lowPower,
  balanced,
  tracking,
  trail,
}

class MotionRecognitionApp extends StatefulWidget {
  const MotionRecognitionApp({super.key});

  @override
  State<MotionRecognitionApp> createState() => _MotionRecognitionAppState();
}

class _MotionRecognitionAppState extends State<MotionRecognitionApp> {
  static const int _maxEventEntries = 250;

  StreamSubscription<Location>? _locationSubscription;
  StreamSubscription<Location>? _motionSubscription;
  StreamSubscription<Activity>? _activitySubscription;
  StreamSubscription<LocationAnomaly>? _anomalySubscription;
  StreamSubscription<TripEvent>? _tripSubscription;
  StreamSubscription<GeofenceWorkflowEvent>? _workflowSubscription;
  StreamSubscription<ProviderChangeEvent>? _providerSubscription;
  StreamSubscription<GeofenceEvent>? _geofenceSubscription;
  StreamSubscription<dynamic>? _geofencesChangeSubscription;
  StreamSubscription<Location>? _heartbeatSubscription;
  StreamSubscription<Location>? _scheduleSubscription;
  StreamSubscription<ConnectivityChangeEvent>? _connectivitySubscription;
  StreamSubscription<bool>? _powerSaveSubscription;
  StreamSubscription<bool>? _enabledSubscription;
  StreamSubscription<HttpEvent>? _httpSubscription;
  StreamSubscription<String>? _notificationActionSubscription;

  final List<String> _events = [];
  final Map<String, int> _eventCounts = {};
  Location? _latestLocation;
  Activity? _lastActivity;
  ProviderChangeEvent? _lastProvider;
  ConnectivityChangeEvent? _lastConnectivity;
  GeofenceEvent? _lastGeofence;
  HttpEvent? _lastHttp;
  GeolocationState? _lastState;
  String? _lastNotificationAction;
  List<LogEntry>? _lastLog;
  TripSummary? _lastTripSummary;

  PowerState? _powerState;
  BatteryStats? _batteryStats;
  bool _spoofDetectionEnabled = false;
  bool _significantChangesEnabled = false;
  String? _benchmarkStatus;

  bool _isRunning = false;
  bool _isReady = false;
  bool _scheduleEnabled = false;
  TrackingPreset _selectedPreset = TrackingPreset.tracking;
  TrackingProfile? _currentProfile;

  List<Location> _storedLocations = [];

  @override
  void initState() {
    super.initState();
    _configure();
  }

  Future<void> _configure() async {
    final isGranted = await Locus.requestPermission();
    if (!isGranted) {
      _showSnackbar('Location permission is required to use this app.');
      return;
    }

    final config = _buildConfig(_selectedPreset);

    await Locus.ready(config);
    await _configureTrackingProfiles();
    _configureWorkflow();
    _setupListeners();
    await _refreshState();

    setState(() {
      _isReady = true;
    });
  }

  Config _buildConfig(TrackingPreset preset) {
    final presetConfig = switch (preset) {
      TrackingPreset.lowPower => ConfigPresets.lowPower,
      TrackingPreset.balanced => ConfigPresets.balanced,
      TrackingPreset.tracking => ConfigPresets.tracking,
      TrackingPreset.trail => ConfigPresets.trail,
    };

    return presetConfig.copyWith(
      stationaryRadius: 25,
      motionTriggerDelay: 15000,
      activityRecognitionInterval: 10000,
      startOnBoot: true,
      stopOnTerminate: false,
      enableHeadless: true,
      disableAutoSyncOnCellular: true,
      maxBatchSize: 20,
      autoSyncThreshold: 10,
      maxRetry: 3,
      retryDelay: 5000,
      retryDelayMultiplier: 2.0,
      maxRetryDelay: 60000,
      persistMode: PersistMode.location,
      maxDaysToPersist: 7,
      maxRecordsToPersist: 200,
      maxMonitoredGeofences: 20,
      url: 'https://example.com/locations',
      logLevel: LogLevel.info,
      logMaxDays: 7,
      schedule: const ['08:00-12:00', '13:00-18:00'],
      notification: const NotificationConfig(
        title: 'Locus',
        text: 'Tracking location in background',
        actions: ['PAUSE', 'STOP'],
      ),
    );
  }

  String _presetLabel(TrackingPreset preset) {
    return switch (preset) {
      TrackingPreset.lowPower => 'Low Power',
      TrackingPreset.balanced => 'Balanced',
      TrackingPreset.tracking => 'Tracking',
      TrackingPreset.trail => 'Trail',
    };
  }

  Future<void> _applyPreset(TrackingPreset preset) async {
    final config = _buildConfig(preset);
    await Locus.setConfig(config);
    setState(() {
      _selectedPreset = preset;
    });
    _recordEvent('preset', 'preset ${_presetLabel(preset)} applied');
  }

  Future<void> _configureTrackingProfiles() async {
    await Locus.setTrackingProfiles(
      {
        TrackingProfile.offDuty: ConfigPresets.lowPower,
        TrackingProfile.standby: ConfigPresets.balanced,
        TrackingProfile.enRoute: ConfigPresets.tracking,
        TrackingProfile.arrived: ConfigPresets.trail,
      },
      initialProfile: TrackingProfile.standby,
      enableAutomation: false,
    );
    setState(() {
      _currentProfile = Locus.currentTrackingProfile;
    });
  }

  Future<void> _applyProfile(TrackingProfile profile) async {
    await Locus.setTrackingProfile(profile);
    setState(() {
      _currentProfile = Locus.currentTrackingProfile;
    });
    _recordEvent('profile', 'profile ${profile.name} applied');
  }

  void _configureWorkflow() {
    Locus.registerGeofenceWorkflows(const [
      GeofenceWorkflow(
        id: 'pickup_dropoff',
        steps: [
          GeofenceWorkflowStep(
            id: 'pickup',
            geofenceIdentifier: 'demo_geofence',
            action: GeofenceAction.enter,
          ),
          GeofenceWorkflowStep(
            id: 'dropoff',
            geofenceIdentifier: 'demo_geofence',
            action: GeofenceAction.exit,
          ),
        ],
      ),
    ]);
  }

  @override
  void dispose() {
    _locationSubscription?.cancel();
    _motionSubscription?.cancel();
    _activitySubscription?.cancel();
    _anomalySubscription?.cancel();
    _tripSubscription?.cancel();
    _workflowSubscription?.cancel();
    _providerSubscription?.cancel();
    _geofenceSubscription?.cancel();
    _geofencesChangeSubscription?.cancel();
    _heartbeatSubscription?.cancel();
    _scheduleSubscription?.cancel();
    _connectivitySubscription?.cancel();
    _powerSaveSubscription?.cancel();
    _enabledSubscription?.cancel();
    _httpSubscription?.cancel();
    _notificationActionSubscription?.cancel();
    super.dispose();
  }

  void _setupListeners() {
    _locationSubscription = Locus.onLocation((location) {
      _recordEvent(
        'location',
        _formatLocationEvent(location, 'location'),
        updateState: () => _latestLocation = location,
      );
    }, onError: _onError);

    _motionSubscription = Locus.onMotionChange((location) {
      _recordEvent(
        'motionchange',
        _formatLocationEvent(location, 'motionchange'),
        updateState: () => _latestLocation = location,
      );
    }, onError: _onError);

    _activitySubscription = Locus.onActivityChange((activity) {
      _recordEvent(
        'activitychange',
        'activity ${activity.type.name} (${activity.confidence}%)',
        updateState: () => _lastActivity = activity,
      );
    }, onError: _onError);

    _anomalySubscription = Locus.onLocationAnomaly(
      (anomaly) {
        _recordEvent(
          'anomaly',
          'anomaly ${anomaly.speedKph.toStringAsFixed(1)} kph over '
              '${anomaly.distanceMeters.toStringAsFixed(0)} m',
        );
      },
      config: const LocationAnomalyConfig(
        maxSpeedKph: 200,
        minDistanceMeters: 500,
      ),
      onError: _onError,
    );

    _tripSubscription = Locus.onTripEvent((event) {
      _recordEvent('trip', 'trip ${event.type.name}');
      if (event.summary != null) {
        setState(() {
          _lastTripSummary = event.summary;
        });
      }
    }, onError: _onError);

    _workflowSubscription = Locus.onWorkflowEvent((event) {
      _recordEvent(
        'workflow',
        'workflow ${event.workflowId} ${event.status.name}',
      );
    }, onError: _onError);

    _providerSubscription = Locus.onProviderChange((event) {
      _recordEvent(
        'providerchange',
        'provider enabled=${event.enabled} auth=${event.authorizationStatus.name}',
        updateState: () => _lastProvider = event,
      );
    }, onError: _onError);

    _geofenceSubscription = Locus.onGeofence((event) {
      _recordEvent(
        'geofence',
        'geofence ${event.geofence.identifier} ${event.action.name}',
        updateState: () => _lastGeofence = event,
      );
    }, onError: _onError);

    _geofencesChangeSubscription = Locus.onGeofencesChange((event) {
      _recordEvent('geofenceschange', 'geofences change: $event');
    }, onError: _onError);

    _heartbeatSubscription = Locus.onHeartbeat((location) {
      _recordEvent('heartbeat', _formatLocationEvent(location, 'heartbeat'));
    }, onError: _onError);

    _scheduleSubscription = Locus.onSchedule((location) {
      _recordEvent('schedule', _formatLocationEvent(location, 'schedule'));
    }, onError: _onError);

    _connectivitySubscription = Locus.onConnectivityChange((event) {
      _recordEvent(
        'connectivity',
        'connectivity ${event.networkType ?? 'unknown'} connected=${event.connected}',
        updateState: () => _lastConnectivity = event,
      );
    }, onError: _onError);

    _powerSaveSubscription = Locus.onPowerSaveChange((enabled) {
      _recordEvent('powersave', 'powersave enabled=$enabled');
    }, onError: _onError);

    _enabledSubscription = Locus.onEnabledChange((enabled) {
      _recordEvent(
        'enabledchange',
        'enabled=$enabled',
        updateState: () => _isRunning = enabled,
      );
    }, onError: _onError);

    _httpSubscription = Locus.onHttp((event) {
      _recordEvent(
        'http',
        'http status=${event.status} ok=${event.ok}',
        updateState: () => _lastHttp = event,
      );
    }, onError: _onError);

    _notificationActionSubscription = Locus.onNotificationAction((action) {
      _recordEvent(
        'notification',
        'notification action=$action',
        updateState: () => _lastNotificationAction = action,
      );
    }, onError: _onError);
  }

  Future<void> _refreshState() async {
    final state = await Locus.getState();
    setState(() {
      _lastState = state;
      _isRunning = state.enabled;
      _scheduleEnabled = state.schedulerEnabled ?? _scheduleEnabled;
    });
  }

  Future<void> _startOrStopTracking() async {
    if (!_isReady) {
      _showSnackbar('Call ready() first.');
      return;
    }
    if (_isRunning) {
      await Locus.stop();
    } else {
      await Locus.start();
    }
    await _refreshState();
  }

  Future<void> _getCurrentPosition() async {
    try {
      final location = await Locus.getCurrentPosition();
      _showSnackbar(
        'Position: ${location.coords.latitude.toStringAsFixed(5)}, ${location.coords.longitude.toStringAsFixed(5)}',
      );
      _recordEvent(
        'currentposition',
        _formatLocationEvent(location, 'getCurrentPosition'),
        updateState: () => _latestLocation = location,
      );
    } catch (e) {
      _onError(e);
    }
  }

  Future<void> _toggleSchedule() async {
    if (_scheduleEnabled) {
      await Locus.stopSchedule();
    } else {
      await Locus.startSchedule();
    }
    setState(() {
      _scheduleEnabled = !_scheduleEnabled;
    });
  }

  Future<void> _loadStoredLocations() async {
    final locations = await Locus.getLocations(limit: 50);
    setState(() {
      _storedLocations = locations;
    });
  }

  Future<void> _clearStoredLocations() async {
    await Locus.destroyLocations();
    setState(() {
      _storedLocations = [];
    });
  }

  Future<void> _startTrip() async {
    await Locus.startTrip(const TripConfig(
      startOnMoving: true,
      updateIntervalSeconds: 30,
      route: [
        RoutePoint(latitude: 37.4219983, longitude: -122.084),
        RoutePoint(latitude: 37.4279613, longitude: -122.0857497),
      ],
      routeDeviationThresholdMeters: 150,
    ));
    _recordEvent('trip', 'trip start requested');
  }

  Future<void> _stopTrip() async {
    final summary = Locus.stopTrip();
    setState(() {
      _lastTripSummary = summary;
    });
    _recordEvent('trip', 'trip stop requested');
  }

  Future<void> _addDemoGeofence() async {
    await Locus.addGeofence(const Geofence(
      identifier: 'demo_geofence',
      radius: 100,
      latitude: 37.4219983,
      longitude: -122.084,
      notifyOnEntry: true,
      notifyOnExit: true,
      notifyOnDwell: true,
      loiteringDelay: 300000,
      extras: {'source': 'example'},
    ));
    _showSnackbar('Geofence added');
  }

  Future<void> _removeAllGeofences() async {
    await Locus.removeGeofences();
    _showSnackbar('Geofences cleared');
  }

  Future<void> _loadLog() async {
    final log = await Locus.getLog();
    setState(() {
      _lastLog = log;
    });
  }

  Future<void> _emailLog() async {
    await Locus.emailLog('logs@example.com');
    _showSnackbar('Requested log email');
  }

  void _clearEvents() {
    setState(() {
      _events.clear();
      _eventCounts.clear();
    });
  }

  void _recordEvent(
    String type,
    String message, {
    VoidCallback? updateState,
  }) {
    final timestamp = _formatTimestamp(DateTime.now());
    setState(() {
      updateState?.call();
      _events.insert(0, '[$timestamp] $message');
      _eventCounts[type] = (_eventCounts[type] ?? 0) + 1;
      if (_events.length > _maxEventEntries) {
        _events.removeLast();
      }
    });
  }

  void _onError(Object error) {
    if (mounted) {
      _showSnackbar('Error: $error');
    }
  }

  void _showSnackbar(String message) {
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text(message)));
  }

  @override
  Widget build(BuildContext context) {
    final colorScheme = ColorScheme.fromSeed(
      seedColor: const Color(0xFF2F5D50),
      brightness: Brightness.light,
    );

    return MaterialApp(
      theme: ThemeData(
        colorScheme: colorScheme,
        useMaterial3: true,
        scaffoldBackgroundColor: const Color(0xFFF4F1EC),
        textTheme: GoogleFonts.spaceGroteskTextTheme(),
        cardTheme: CardThemeData(
          elevation: 0,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        ),
      ),
      home: DefaultTabController(
        length: 5,
        child: Scaffold(
          appBar: AppBar(
            title: const Text('Locus'),
            actions: [
              IconButton(
                onPressed: _refreshState,
                icon: const Icon(Icons.sync),
              ),
              IconButton(
                onPressed: _clearEvents,
                icon: const Icon(Icons.delete_outline),
              ),
            ],
            bottom: const TabBar(
              tabs: [
                Tab(text: 'Overview', icon: Icon(Icons.route)),
                Tab(text: 'Events', icon: Icon(Icons.timeline)),
                Tab(text: 'Storage', icon: Icon(Icons.storage)),
                Tab(text: 'Diagnostics', icon: Icon(Icons.tune)),
                Tab(text: 'Advanced', icon: Icon(Icons.science)),
              ],
            ),
          ),
          body: Container(
            decoration: const BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
                colors: [
                  Color(0xFFF4F1EC),
                  Color(0xFFE7EFEA),
                ],
              ),
            ),
            child: TabBarView(
              children: [
                _buildOverviewTab(),
                _buildEventsTab(),
                _buildStorageTab(),
                _buildDiagnosticsTab(),
                _buildAdvancedTab(),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildOverviewTab() {
    return SingleChildScrollView(
      child: Column(
        children: [
          _buildStatusPanel(),
          _buildControlPanel(),
          _buildQuickStats(),
        ],
      ),
    );
  }

  Widget _buildEventsTab() {
    return Column(
      children: [
        _buildEventSummary(),
        Expanded(child: _buildEventList()),
      ],
    );
  }

  Widget _buildStorageTab() {
    return SingleChildScrollView(
      child: Column(
        children: [
          _buildStoragePanel(),
          _buildStoredList(),
        ],
      ),
    );
  }

  Widget _buildDiagnosticsTab() {
    return SingleChildScrollView(
      child: Column(
        children: [
          _buildDiagnosticsPanel(),
          _buildLogPanel(),
        ],
      ),
    );
  }

  Widget _buildStatusPanel() {
    final location = _latestLocation;
    final provider = _lastProvider;
    final connectivity = _lastConnectivity;
    final state = _lastState;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Status',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                _buildStatusChip(
                  label: _isReady ? 'Ready' : 'Not Ready',
                  icon: Icons.flash_on,
                  active: _isReady,
                ),
                _buildStatusChip(
                  label: _isRunning ? 'Tracking' : 'Stopped',
                  icon: _isRunning ? Icons.play_arrow : Icons.pause,
                  active: _isRunning,
                ),
                _buildStatusChip(
                  label: state?.isMoving == true ? 'Moving' : 'Stationary',
                  icon: Icons.directions_walk,
                  active: state?.isMoving == true,
                ),
                _buildStatusChip(
                  label: _scheduleEnabled ? 'Schedule On' : 'Schedule Off',
                  icon: Icons.schedule,
                  active: _scheduleEnabled,
                ),
              ],
            ),
            const SizedBox(height: 12),
            Text('Odometer: ${state?.odometer?.toStringAsFixed(1) ?? '0'} m'),
            const SizedBox(height: 6),
            Text(
              'Latest: ${location != null ? location.event ?? 'location' : 'N/A'}',
            ),
            if (location != null)
              Text(
                'Lat ${location.coords.latitude.toStringAsFixed(5)}, '
                'Lng ${location.coords.longitude.toStringAsFixed(5)}',
              ),
            const SizedBox(height: 6),
            Text(
              'Provider: ${provider?.authorizationStatus.name ?? 'unknown'} | '
              'Availability: ${provider?.availability.name ?? 'unknown'}',
            ),
            Text(
              'Connectivity: ${connectivity?.networkType ?? 'unknown'} '
              '(${connectivity?.connected == true ? 'online' : 'offline'})',
            ),
            if (_lastActivity != null)
              Text(
                'Activity: ${_lastActivity!.type.name} '
                '(${_lastActivity!.confidence}%)',
              ),
            if (_lastHttp != null)
              Text('Last HTTP: ${_lastHttp!.status} ok=${_lastHttp!.ok}'),
            if (_lastGeofence != null)
              Text(
                'Last Geofence: ${_lastGeofence!.geofence.identifier} '
                '${_lastGeofence!.action.name}',
              ),
            if (_currentProfile != null)
              Text('Profile: ${_currentProfile!.name}'),
            if (_lastTripSummary != null)
              Text(
                'Last Trip: ${_lastTripSummary!.distanceMeters.toStringAsFixed(0)} m '
                'in ${_lastTripSummary!.durationSeconds}s',
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildStatusChip({
    required String label,
    required IconData icon,
    required bool active,
  }) {
    final color = active ? Theme.of(context).colorScheme.primary : Colors.grey;
    return Chip(
      avatar: Icon(icon, size: 16, color: color),
      label: Text(label),
      side: BorderSide(color: color.withValues(alpha: 0.4)),
      backgroundColor: color.withValues(alpha: 0.1),
    );
  }

  Widget _buildControlPanel() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Controls',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: DropdownButtonFormField<TrackingPreset>(
                    initialValue: _selectedPreset,
                    decoration: const InputDecoration(
                      labelText: 'Tracking preset',
                      border: OutlineInputBorder(),
                    ),
                    items: TrackingPreset.values
                        .map(
                          (preset) => DropdownMenuItem(
                            value: preset,
                            child: Text(_presetLabel(preset)),
                          ),
                        )
                        .toList(),
                    onChanged: (preset) {
                      if (preset != null) {
                        _applyPreset(preset);
                      }
                    },
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 12,
              runSpacing: 12,
              children: [
                FilledButton.icon(
                  onPressed: _startOrStopTracking,
                  icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow),
                  label: Text(_isRunning ? 'Stop Tracking' : 'Start Tracking'),
                  style: FilledButton.styleFrom(
                    backgroundColor: _isRunning
                        ? Colors.redAccent
                        : Theme.of(context).colorScheme.primary,
                  ),
                ),
                OutlinedButton.icon(
                  onPressed: _getCurrentPosition,
                  icon: const Icon(Icons.my_location),
                  label: const Text('Get Position'),
                ),
                FilledButton.tonalIcon(
                  onPressed: _toggleSchedule,
                  icon: const Icon(Icons.schedule),
                  label: Text(
                    _scheduleEnabled ? 'Stop Schedule' : 'Start Schedule',
                  ),
                ),
                FilledButton.tonalIcon(
                  onPressed: _addDemoGeofence,
                  icon: const Icon(Icons.add_location_alt),
                  label: const Text('Add Geofence'),
                ),
                OutlinedButton.icon(
                  onPressed: _removeAllGeofences,
                  icon: const Icon(Icons.delete_outline),
                  label: const Text('Clear Geofences'),
                ),
                FilledButton.tonalIcon(
                  onPressed: () => _applyProfile(TrackingProfile.offDuty),
                  icon: const Icon(Icons.bedtime_outlined),
                  label: const Text('Off Duty'),
                ),
                FilledButton.tonalIcon(
                  onPressed: () => _applyProfile(TrackingProfile.standby),
                  icon: const Icon(Icons.pause_circle_outline),
                  label: const Text('Standby'),
                ),
                FilledButton.tonalIcon(
                  onPressed: () => _applyProfile(TrackingProfile.enRoute),
                  icon: const Icon(Icons.navigation_outlined),
                  label: const Text('En Route'),
                ),
                FilledButton.tonalIcon(
                  onPressed: () => _applyProfile(TrackingProfile.arrived),
                  icon: const Icon(Icons.flag_outlined),
                  label: const Text('Arrived'),
                ),
                FilledButton.tonalIcon(
                  onPressed: _startTrip,
                  icon: const Icon(Icons.flag_outlined),
                  label: const Text('Start Trip'),
                ),
                OutlinedButton.icon(
                  onPressed: _stopTrip,
                  icon: const Icon(Icons.stop_circle_outlined),
                  label: const Text('Stop Trip'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildQuickStats() {
    final entries = _eventCounts.entries.toList()
      ..sort((a, b) => b.value.compareTo(a.value));

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Event Pulse',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 12),
            if (entries.isEmpty) const Text('No events recorded yet.'),
            if (entries.isNotEmpty)
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: entries
                    .map(
                      (entry) => Chip(
                        label: Text('${entry.key}: ${entry.value}'),
                      ),
                    )
                    .toList(),
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildEventSummary() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              'Total events: ${_events.length}',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            Text(
              'Max: $_maxEventEntries',
              style: Theme.of(context).textTheme.labelLarge,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildEventList() {
    return ListView.separated(
      itemCount: _events.length,
      separatorBuilder: (_, __) => const Divider(height: 1),
      itemBuilder: (_, int idx) {
        final entry = _events[idx];
        return ListTile(
          leading: const Icon(Icons.location_pin),
          title: Text(entry),
        );
      },
    );
  }

  Widget _buildStoragePanel() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Stored Locations',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 12,
              runSpacing: 12,
              children: [
                FilledButton.tonalIcon(
                  onPressed: _loadStoredLocations,
                  icon: const Icon(Icons.download),
                  label: const Text('Load'),
                ),
                OutlinedButton.icon(
                  onPressed: _clearStoredLocations,
                  icon: const Icon(Icons.delete_outline),
                  label: const Text('Clear'),
                ),
                Chip(
                  label: Text('Count: ${_storedLocations.length}'),
                ),
              ],
            ),
            if (_storedLocations.isNotEmpty) ...[
              const SizedBox(height: 8),
              Text(
                'Latest stored: ${_storedLocations.last.coords.latitude.toStringAsFixed(5)}, '
                '${_storedLocations.last.coords.longitude.toStringAsFixed(5)}',
              ),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildStoredList() {
    if (_storedLocations.isEmpty) {
      return const SizedBox.shrink();
    }

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ListView.separated(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          itemCount: _storedLocations.length,
          separatorBuilder: (_, __) => const Divider(height: 1),
          itemBuilder: (_, int index) {
            final location = _storedLocations[index];
            return ListTile(
              leading: const Icon(Icons.place_outlined),
              title: Text(
                '${location.coords.latitude.toStringAsFixed(5)}, '
                '${location.coords.longitude.toStringAsFixed(5)}',
              ),
              subtitle: Text(
                location.timestamp.toIso8601String(),
              ),
              trailing: Text(
                '${location.coords.accuracy.toStringAsFixed(1)}m',
              ),
            );
          },
        ),
      ),
    );
  }

  Widget _buildDiagnosticsPanel() {
    final state = _lastState;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Diagnostics',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 12),
            Text('Enabled: ${state?.enabled ?? false}'),
            Text('Moving: ${state?.isMoving ?? false}'),
            Text('Scheduler: ${state?.schedulerEnabled ?? false}'),
            Text('Odometer: ${state?.odometer?.toStringAsFixed(1) ?? '0'}'),
            const SizedBox(height: 8),
            if (_lastProvider != null)
              Text(
                'Provider status: ${_lastProvider!.authorizationStatus.name}',
              ),
            if (_lastConnectivity != null)
              Text(
                'Network: ${_lastConnectivity!.networkType ?? 'unknown'} '
                '(${_lastConnectivity!.connected ? 'online' : 'offline'})',
              ),
            if (_lastNotificationAction != null)
              Text('Notification action: $_lastNotificationAction'),
          ],
        ),
      ),
    );
  }

  Widget _buildLogPanel() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Logs',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 12,
              runSpacing: 12,
              children: [
                FilledButton.tonalIcon(
                  onPressed: _loadLog,
                  icon: const Icon(Icons.article_outlined),
                  label: const Text('Load Log'),
                ),
                OutlinedButton.icon(
                  onPressed: _emailLog,
                  icon: const Icon(Icons.email_outlined),
                  label: const Text('Email Log'),
                ),
              ],
            ),
            if (_lastLog != null) ...[
              const SizedBox(height: 12),
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Text(
                  _lastLog!.isEmpty
                      ? 'No logs yet.'
                      : _formatLogEntries(_lastLog!),
                  maxLines: 12,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildAdvancedTab() {
    return SingleChildScrollView(
      child: Column(
        children: [
          _buildBatteryOptimizationCard(),
          _buildSpoofDetectionCard(),
          _buildSignificantChangesCard(),
          _buildErrorRecoveryCard(),
        ],
      ),
    );
  }

  Widget _buildBatteryOptimizationCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Icon(Icons.battery_charging_full),
                const SizedBox(width: 8),
                Text(
                  'Battery Optimization',
                  style: Theme.of(context).textTheme.titleLarge,
                ),
              ],
            ),
            const SizedBox(height: 16),
            if (_powerState != null) ...[
              Text(
                'Battery: ${_powerState!.batteryLevel}% '
                '(${_powerState!.isCharging ? "Charging" : "Discharging"})',
              ),
              const SizedBox(height: 8),
            ],
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                FilledButton.tonal(
                  onPressed: _refreshBatteryStats,
                  child: const Text('Refresh Stats'),
                ),
                FilledButton.tonal(
                  onPressed: _benchmarkStatus == null ? _toggleBenchmark : null,
                  child: const Text('Start Benchmark'),
                ),
                if (_benchmarkStatus != null)
                  FilledButton(
                    onPressed: _toggleBenchmark,
                    style: FilledButton.styleFrom(backgroundColor: Colors.red),
                    child: const Text('Stop Benchmark'),
                  ),
              ],
            ),
            if (_batteryStats != null) ...[
              const SizedBox(height: 16),
              const Text('Stats:',
                  style: TextStyle(fontWeight: FontWeight.bold)),
              Text(
                  'GPS On Time: ${(_batteryStats!.gpsOnTimePercent * 100).toStringAsFixed(1)}%'),
              Text(
                  'Drain Rate: ${_batteryStats!.estimatedDrainPerHour?.toStringAsFixed(1) ?? "N/A"}%/hr'),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildSpoofDetectionCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Icon(Icons.security),
                const SizedBox(width: 8),
                Text(
                  'Spoof Detection',
                  style: Theme.of(context).textTheme.titleLarge,
                ),
              ],
            ),
            SwitchListTile(
              title: const Text('Enable Detection'),
              subtitle: const Text('Detect mock/spoofed locations'),
              value: _spoofDetectionEnabled,
              onChanged: (val) => _toggleSpoofDetection(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSignificantChangesCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Icon(Icons.compare_arrows),
                const SizedBox(width: 8),
                Text(
                  'Significant Changes',
                  style: Theme.of(context).textTheme.titleLarge,
                ),
              ],
            ),
            SwitchListTile(
              title: const Text('Monitor Changes'),
              subtitle: const Text('Ultra-low power (~500m)'),
              value: _significantChangesEnabled,
              onChanged: (val) => _toggleSignificantChanges(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildErrorRecoveryCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Icon(Icons.healing),
                const SizedBox(width: 8),
                Text(
                  'Error Recovery',
                  style: Theme.of(context).textTheme.titleLarge,
                ),
              ],
            ),
            const SizedBox(height: 16),
            FilledButton.tonal(
              onPressed: _simulateError,
              child: const Text('Simulate Network Error'),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _refreshBatteryStats() async {
    final state = await Locus.getPowerState();
    final stats = await Locus.getBatteryStats();
    setState(() {
      _powerState = state;
      _batteryStats = stats;
    });
  }

  Future<void> _toggleBenchmark() async {
    if (_benchmarkStatus == null) {
      await Locus.startBatteryBenchmark();
      setState(() => _benchmarkStatus = 'Running...');
      _showSnackbar('Benchmark started');
    } else {
      await Locus.stopBatteryBenchmark();
      setState(() => _benchmarkStatus = null);
      _showSnackbar('Benchmark stopped');
    }
  }

  Future<void> _toggleSpoofDetection() async {
    final newValue = !_spoofDetectionEnabled;
    await Locus.setSpoofDetection(
      newValue ? SpoofDetectionConfig.high : SpoofDetectionConfig.disabled,
    );
    setState(() => _spoofDetectionEnabled = newValue);
    _showSnackbar('Spoof detection ${newValue ? "enabled" : "disabled"}');
  }

  Future<void> _toggleSignificantChanges() async {
    final newValue = !_significantChangesEnabled;
    if (newValue) {
      await Locus.startSignificantChangeMonitoring(
        SignificantChangeConfig.defaults,
      );
    } else {
      await Locus.stopSignificantChangeMonitoring();
    }
    setState(() => _significantChangesEnabled = newValue);
  }

  Future<void> _simulateError() async {
    Locus.handleError(LocusError.networkError(
      message: 'Simulated connection failure',
      originalError: 'Simulated',
    ));
    // The ErrorRecoveryManager logs this, so check logs
    _showSnackbar('Simulated error injected');
  }

  String _formatLocationEvent(Location location, String label) {
    final lat = location.coords.latitude.toStringAsFixed(5);
    final lng = location.coords.longitude.toStringAsFixed(5);
    return '$label: $lat, $lng (acc ${location.coords.accuracy.toStringAsFixed(1)}m)';
  }

  String _formatTimestamp(DateTime time) {
    final hours = time.hour.toString().padLeft(2, '0');
    final minutes = time.minute.toString().padLeft(2, '0');
    final seconds = time.second.toString().padLeft(2, '0');
    return '$hours:$minutes:$seconds';
  }

  String _formatLogEntries(List<LogEntry> entries) {
    return entries.take(12).map((entry) {
      final timestamp = _formatTimestamp(entry.timestamp.toLocal());
      final tag = entry.tag;
      final level = tag == null ? entry.level : '${entry.level}/$tag';
      return '[$timestamp] $level ${entry.message}';
    }).join('\n');
  }
}
0
likes
140
points
250
downloads

Publisher

verified publisherweorbis.com

Weekly Downloads

Background geolocation SDK for Flutter. Native tracking, geofencing, activity recognition, and sync.

Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

unknown (license)

Dependencies

args, device_info_plus, flutter, permission_handler, url_launcher, xml

More

Packages that depend on locus

Packages that implement locus