import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/live_position.dart'; import '../services/background_location_service.dart'; import 'profile_provider.dart'; import 'supabase_provider.dart'; import 'stream_recovery.dart'; import 'realtime_controller.dart'; /// All live positions of tracked users. final livePositionsProvider = StreamProvider>((ref) { final client = ref.watch(supabaseClientProvider); final wrapper = StreamRecoveryWrapper( stream: client.from('live_positions').stream(primaryKey: ['user_id']), onPollData: () async { final data = await client.from('live_positions').select(); return data.map(LivePosition.fromMap).toList(); }, fromMap: LivePosition.fromMap, channelName: 'live_positions', onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus, ); ref.onDispose(wrapper.dispose); return wrapper.stream.map((result) => result.data); }); final whereaboutsControllerProvider = Provider((ref) { final client = ref.watch(supabaseClientProvider); return WhereaboutsController(client); }); class WhereaboutsController { WhereaboutsController(this._client); final SupabaseClient _client; /// Upsert current position. Returns `in_premise` status. Future updatePosition(double lat, double lng) async { final data = await _client.rpc( 'update_live_position', params: {'p_lat': lat, 'p_lng': lng}, ); return data as bool? ?? false; } /// Toggle allow_tracking preference. Future setTracking(bool allow) async { final userId = _client.auth.currentUser?.id; if (userId == null) throw Exception('Not authenticated'); await _client .from('profiles') .update({'allow_tracking': allow}) .eq('id', userId); // Start or stop background location updates if (allow) { await startBackgroundLocationUpdates(); } else { await stopBackgroundLocationUpdates(); // Remove the live position entry await _client.from('live_positions').delete().eq('user_id', userId); } } } /// Background location reporting service. /// Starts a 1-minute periodic timer that reports position to the server. final locationReportingProvider = Provider.autoDispose((ref) { final client = ref.watch(supabaseClientProvider); final profileAsync = ref.watch(currentProfileProvider); final profile = profileAsync.valueOrNull; final service = LocationReportingService(client); // Auto-start if user has tracking enabled if (profile != null && profile.allowTracking) { service.start(); // Also ensure background task is registered startBackgroundLocationUpdates(); } ref.onDispose(service.stop); return service; }); class LocationReportingService { LocationReportingService(this._client); final SupabaseClient _client; Timer? _timer; bool _isRunning = false; bool get isRunning => _isRunning; void start() { if (_isRunning) return; _isRunning = true; // Report immediately, then every 60 seconds _reportPosition(); _timer = Timer.periodic(const Duration(seconds: 60), (_) { _reportPosition(); }); } void stop() { _isRunning = false; _timer?.cancel(); _timer = null; } Future _reportPosition() async { try { final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return; var permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { return; } final position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, ), ); await _client.rpc( 'update_live_position', params: {'p_lat': position.latitude, 'p_lng': position.longitude}, ); } catch (_) { // Silently ignore errors in background reporting } } }