import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:geolocator/geolocator.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; /// Interval between background location updates. const _updateInterval = Duration(minutes: 15); /// Whether the Supabase client has been initialised inside the background /// isolate. We guard against double-init which would throw. bool _bgIsolateInitialised = false; /// Initialise Supabase inside the background isolate so we can call RPCs. /// Safe to call multiple times – only the first call performs real work. Future _ensureBgSupabase() async { if (_bgIsolateInitialised) { try { return Supabase.instance.client; } catch (_) { _bgIsolateInitialised = false; } } try { await dotenv.load(); } catch (_) { // .env may already be loaded } final url = dotenv.env['SUPABASE_URL'] ?? ''; final anonKey = dotenv.env['SUPABASE_ANON_KEY'] ?? ''; if (url.isEmpty || anonKey.isEmpty) return null; try { await Supabase.initialize(url: url, anonKey: anonKey); } catch (_) { // Already initialised – that's fine } _bgIsolateInitialised = true; return Supabase.instance.client; } /// Send location update to Supabase via the `update_live_position` RPC. Future _sendLocationUpdate() async { try { final client = await _ensureBgSupabase(); if (client == null) { debugPrint('[BgLoc] Supabase client unavailable'); return; } final session = client.auth.currentSession; if (session == null) { debugPrint('[BgLoc] No active session'); return; } final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { debugPrint('[BgLoc] Location service disabled'); return; } final permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { debugPrint('[BgLoc] Location permission denied'); 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}, ); debugPrint( '[BgLoc] Position updated: ${position.latitude}, ${position.longitude}', ); } catch (e) { debugPrint('[BgLoc] Error: $e'); } } // --------------------------------------------------------------------------- // Background service entry point (runs in a separate isolate on Android). // MUST be a PUBLIC top-level function — PluginUtilities.getCallbackHandle() // returns null for private (underscore-prefixed) functions. // --------------------------------------------------------------------------- /// Top-level entry point invoked by flutter_background_service in a new isolate. @pragma('vm:entry-point') Future onBackgroundServiceStart(ServiceInstance service) async { // Required for platform channel access (Geolocator, Supabase) in an isolate. WidgetsFlutterBinding.ensureInitialized(); debugPrint('[BgLoc] Background service started'); // Send initial location immediately on service start. await _sendLocationUpdate(); // Maintain a subscription to the position stream so the service remains // active when the device is moving. We still keep a periodic timer as a // backup so that updates fire even when stationary. StreamSubscription? positionSub; try { positionSub = Geolocator.getPositionStream( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 0, ), ).listen((pos) { debugPrint('[BgLoc] Stream position: $pos'); _sendLocationUpdate(); }); } catch (e) { debugPrint('[BgLoc] Position stream failed: $e'); } // Periodic 15-minute timer for location updates (backup when stream is // paused by Doze). Timer.periodic(_updateInterval, (_) async { debugPrint('[BgLoc] Periodic update triggered'); await _sendLocationUpdate(); }); // Listen for stop command from the foreground. service.on('stop').listen((_) { debugPrint('[BgLoc] Stop command received'); positionSub?.cancel(); service.stopSelf(); }); } /// iOS background entry point — must be a PUBLIC top-level function. @pragma('vm:entry-point') Future onIosBackground(ServiceInstance service) async { WidgetsFlutterBinding.ensureInitialized(); await _sendLocationUpdate(); return true; } // --------------------------------------------------------------------------- // Public API – same function signatures as the previous implementation. // --------------------------------------------------------------------------- /// Initialise the background service plugin. Call once at app startup. Future initBackgroundLocationService() async { if (kIsWeb) return; final service = FlutterBackgroundService(); await service.configure( iosConfiguration: IosConfiguration( autoStart: false, onForeground: onBackgroundServiceStart, onBackground: onIosBackground, ), androidConfiguration: AndroidConfiguration( onStart: onBackgroundServiceStart, isForegroundMode: true, autoStart: false, autoStartOnBoot: true, // Leave notificationChannelId null so the plugin will create the // default "FOREGROUND_DEFAULT" channel for us. Passing a non-null // value requires us to create the channel ourselves first, otherwise // the service crashes on API >=26 when startForeground is called. notificationChannelId: null, initialNotificationTitle: 'TasQ', initialNotificationContent: 'Location tracking is active', foregroundServiceNotificationId: 7749, foregroundServiceTypes: [AndroidForegroundType.location], ), ); } /// Start the background location service. Future startBackgroundLocationUpdates() async { if (kIsWeb) return; final service = FlutterBackgroundService(); final running = await service.isRunning(); if (running) return; await service.startService(); } /// Stop the background location service. Future stopBackgroundLocationUpdates() async { if (kIsWeb) return; final service = FlutterBackgroundService(); final running = await service.isRunning(); if (!running) return; service.invoke('stop'); } /// Check whether the background location service is currently running. Future isBackgroundLocationRunning() async { if (kIsWeb) return false; return FlutterBackgroundService().isRunning(); }